795 lines
27 KiB
Markdown
795 lines
27 KiB
Markdown
# Architecture Research: SummerCMS
|
|
|
|
**Domain:** Scala/ZIO-based Content Management Framework (CMF)
|
|
**Researched:** 2026-02-04
|
|
**Overall Confidence:** MEDIUM-HIGH
|
|
|
|
This document defines the recommended architecture for SummerCMS, a Scala rewrite of WinterCMS using the ZIO ecosystem. The architecture leverages ZIO's effect system and layer-based dependency injection to create a modular, extensible CMF.
|
|
|
|
---
|
|
|
|
## High-Level Architecture
|
|
|
|
```
|
|
+------------------+
|
|
| HTMX/Vue |
|
|
| Frontend |
|
|
+--------+---------+
|
|
|
|
|
v
|
|
+-----------------------------------------------------------------------------------+
|
|
| HTTP LAYER (ZIO HTTP) |
|
|
| +-------------+ +-------------+ +-------------+ +------------------+ |
|
|
| | Public | | Admin | | API | | Static Assets | |
|
|
| | Routes | | Routes | | Routes | | Handler | |
|
|
| +------+------+ +------+------+ +------+------+ +------------------+ |
|
|
| | | | |
|
|
| +----------------+----------------+ |
|
|
| | |
|
|
| v |
|
|
| +--------------------------------------------------------------------+ |
|
|
| | MIDDLEWARE STACK | |
|
|
| | Auth | CORS | Logging | Metrics | Error | Rate Limit | Plugin | |
|
|
| +--------------------------------------------------------------------+ |
|
|
+-----------------------------------------------------------------------------------+
|
|
|
|
|
v
|
|
+-----------------------------------------------------------------------------------+
|
|
| REQUEST PIPELINE |
|
|
| +-------------+ +-------------+ +-------------+ +------------------+ |
|
|
| | Route | | Theme | | Component | | Response | |
|
|
| | Resolver | | Resolver | | Resolver | | Builder | |
|
|
| +------+------+ +------+------+ +------+------+ +------------------+ |
|
|
+-----------------------------------------------------------------------------------+
|
|
|
|
|
v
|
|
+-----------------------------------------------------------------------------------+
|
|
| CORE SERVICES LAYER |
|
|
| +-----------+ +-----------+ +-----------+ +-----------+ +-----------+ |
|
|
| | Plugin | | Theme | | Config | | Event | | Cache | |
|
|
| | Registry | | Engine | | Service | | Bus | | Service | |
|
|
| +-----------+ +-----------+ +-----------+ +-----------+ +-----------+ |
|
|
| |
|
|
| +-----------+ +-----------+ +-----------+ +-----------+ +-----------+ |
|
|
| | Template | | Form | | Auth | | Media | | Queue | |
|
|
| | Engine | | Builder | | Service | | Manager | | Service | |
|
|
| +-----------+ +-----------+ +-----------+ +-----------+ +-----------+ |
|
|
+-----------------------------------------------------------------------------------+
|
|
|
|
|
v
|
|
+-----------------------------------------------------------------------------------+
|
|
| DOMAIN LAYER (Pure Business Logic) |
|
|
| +-----------+ +-----------+ +-----------+ +-----------+ +-----------+ |
|
|
| | Models | | Commands | | Queries | | Validators| | Transforms| |
|
|
| +-----------+ +-----------+ +-----------+ +-----------+ +-----------+ |
|
|
+-----------------------------------------------------------------------------------+
|
|
|
|
|
v
|
|
+-----------------------------------------------------------------------------------+
|
|
| DATA ACCESS LAYER |
|
|
| +-----------+ +-----------+ +-----------+ +-----------+ |
|
|
| | Repository| | Query | | Migration | | Schema | |
|
|
| | Interfaces| | Builder | | System | | Registry | |
|
|
| +-----------+ +-----------+ +-----------+ +-----------+ |
|
|
| | |
|
|
| v |
|
|
| +--------------------------------------------------------------------+ |
|
|
| | ZIO Quill (PostgreSQL) | |
|
|
| +--------------------------------------------------------------------+ |
|
|
+-----------------------------------------------------------------------------------+
|
|
```
|
|
|
|
---
|
|
|
|
## Core Components
|
|
|
|
### 1. Request Pipeline
|
|
|
|
**Responsibility:** Routes incoming HTTP requests through theme/component resolution to response generation.
|
|
|
|
```scala
|
|
// Conceptual flow
|
|
Request
|
|
-> RouteResolver // Determines page/route from URL
|
|
-> ThemeResolver // Loads theme and layout
|
|
-> ComponentResolver // Instantiates page components
|
|
-> PageRenderer // Renders template with component data
|
|
-> Response
|
|
```
|
|
|
|
**Key Types:**
|
|
```scala
|
|
trait RouteResolver {
|
|
def resolve(path: Path, method: Method): ZIO[Any, RouteError, ResolvedRoute]
|
|
}
|
|
|
|
sealed trait ResolvedRoute
|
|
case class PageRoute(page: Page, params: Map[String, String]) extends ResolvedRoute
|
|
case class ApiRoute(handler: Handler, params: Map[String, String]) extends ResolvedRoute
|
|
case class StaticRoute(asset: Asset) extends ResolvedRoute
|
|
case class RedirectRoute(target: String, permanent: Boolean) extends ResolvedRoute
|
|
```
|
|
|
|
**Build Order:** 2nd (after Plugin System foundation)
|
|
|
|
---
|
|
|
|
### 2. Plugin System
|
|
|
|
**Responsibility:** Plugin discovery, lifecycle management, dependency resolution, and extension point registry.
|
|
|
|
This is the foundational component. Everything else builds on top of it.
|
|
|
|
```scala
|
|
// Plugin definition (similar to WinterCMS Plugin.php)
|
|
trait Plugin {
|
|
def pluginDetails: PluginDetails
|
|
def require: List[PluginId] = Nil // Dependencies
|
|
|
|
// Lifecycle hooks
|
|
def register: ZIO[PluginEnvironment, Nothing, Unit]
|
|
def boot: ZIO[PluginEnvironment, PluginError, Unit]
|
|
|
|
// Registration methods
|
|
def registerComponents: Map[String, ComponentFactory] = Map.empty
|
|
def registerNavigation: List[NavigationItem] = List.empty
|
|
def registerSettings: List[SettingsPage] = List.empty
|
|
def registerPermissions: List[Permission] = List.empty
|
|
def registerMailTemplates: List[MailTemplate] = List.empty
|
|
def registerSchedule: List[ScheduledTask] = List.empty
|
|
}
|
|
|
|
// Plugin registry service
|
|
trait PluginRegistry {
|
|
def discover: ZIO[Any, PluginError, List[PluginManifest]]
|
|
def load(id: PluginId): ZIO[Any, PluginError, Plugin]
|
|
def enable(id: PluginId): ZIO[Any, PluginError, Unit]
|
|
def disable(id: PluginId): ZIO[Any, PluginError, Unit]
|
|
def getEnabled: ZIO[Any, Nothing, List[Plugin]]
|
|
def resolveDependencies(id: PluginId): ZIO[Any, PluginError, List[PluginId]]
|
|
}
|
|
```
|
|
|
|
**Plugin Extension Mechanism (replicating WinterCMS patterns):**
|
|
|
|
```scala
|
|
// 1. Event-based extension (primary mechanism)
|
|
trait EventBus {
|
|
def emit[E](event: E): ZIO[Any, Nothing, Unit]
|
|
def subscribe[E: Tag](handler: E => ZIO[Any, Nothing, Unit]): ZIO[Any, Nothing, Subscription]
|
|
}
|
|
|
|
// Events plugins can emit/subscribe to
|
|
case class ModelCreated[M](model: M)
|
|
case class ModelUpdated[M](model: M, changes: Map[String, Any])
|
|
case class FormExtending(formWidget: FormWidget, model: Any, context: String)
|
|
case class ListExtending(listWidget: ListWidget, model: Any)
|
|
|
|
// 2. Model extension via type classes
|
|
trait ModelExtensions[M] {
|
|
def relations: Map[String, Relation[_, _]] = Map.empty
|
|
def attributes: Map[String, Attribute[_]] = Map.empty
|
|
def behaviors: List[Behavior[M]] = List.empty
|
|
}
|
|
|
|
// 3. Form field injection
|
|
trait FormFieldExtension {
|
|
def extendFields(form: FormDefinition, model: Any, context: String): FormDefinition
|
|
}
|
|
```
|
|
|
|
**ZIO Layer Structure:**
|
|
```scala
|
|
val pluginRegistryLayer: ZLayer[Config & Database, PluginError, PluginRegistry] =
|
|
ZLayer.fromFunction(PluginRegistryLive.apply _)
|
|
|
|
val eventBusLayer: ZLayer[Any, Nothing, EventBus] =
|
|
ZLayer.succeed(EventBusLive())
|
|
```
|
|
|
|
**Build Order:** 1st (foundation for everything)
|
|
|
|
---
|
|
|
|
### 3. Theme Engine
|
|
|
|
**Responsibility:** Theme loading, template rendering, asset management, layout composition.
|
|
|
|
```scala
|
|
trait ThemeEngine {
|
|
def getActive: ZIO[Any, ThemeError, Theme]
|
|
def setActive(id: ThemeId): ZIO[Any, ThemeError, Unit]
|
|
def render(page: Page, data: PageData): ZIO[Any, RenderError, Html]
|
|
def renderPartial(name: String, data: Map[String, Any]): ZIO[Any, RenderError, Html]
|
|
}
|
|
|
|
trait Theme {
|
|
def id: ThemeId
|
|
def pages: ZIO[Any, ThemeError, List[PageDefinition]]
|
|
def layouts: ZIO[Any, ThemeError, List[LayoutDefinition]]
|
|
def partials: ZIO[Any, ThemeError, List[PartialDefinition]]
|
|
def assets: AssetManager
|
|
}
|
|
|
|
// Template definition (parsed from .htm files with YAML frontmatter)
|
|
case class PageDefinition(
|
|
url: String,
|
|
layout: Option[String],
|
|
title: Option[String],
|
|
description: Option[String],
|
|
components: Map[String, ComponentConfig], // Component alias -> config
|
|
markup: String
|
|
)
|
|
```
|
|
|
|
**Template Rendering Options:**
|
|
|
|
| Option | Recommendation | Rationale |
|
|
|--------|---------------|-----------|
|
|
| ScalaTags | **Recommended** | Type-safe, fast (2x Twirl), Scala-native, HTMX-friendly |
|
|
| Twirl | Alternative | Play Framework standard, familiar syntax |
|
|
| Custom Parser | For WinterCMS .htm compat | Parse YAML frontmatter + Twig-like syntax |
|
|
|
|
For maximum WinterCMS compatibility, implement a custom template parser that:
|
|
1. Parses YAML frontmatter for page config
|
|
2. Translates Twig-like syntax to ScalaTags calls
|
|
3. Supports component embedding via `{% component 'alias' %}`
|
|
|
|
**Build Order:** 4th (needs Plugin System, Config, Components)
|
|
|
|
---
|
|
|
|
### 4. Component System
|
|
|
|
**Responsibility:** Reusable UI components with lifecycle, properties, and AJAX handlers.
|
|
|
|
```scala
|
|
// Component definition (mirrors WinterCMS components)
|
|
trait Component {
|
|
def componentDetails: ComponentDetails
|
|
def defineProperties: List[PropertyDefinition] = Nil
|
|
|
|
// Lifecycle
|
|
def init: ZIO[ComponentEnv, ComponentError, Unit] = ZIO.unit
|
|
def onRun: ZIO[ComponentEnv, ComponentError, Unit] = ZIO.unit
|
|
|
|
// Data for template
|
|
def data: ZIO[ComponentEnv, ComponentError, Map[String, Any]]
|
|
|
|
// AJAX/HTMX handlers (replaces WinterCMS onXxx methods)
|
|
def handlers: Map[String, Handler] = Map.empty
|
|
}
|
|
|
|
// Handler for HTMX requests
|
|
type Handler = Request => ZIO[ComponentEnv, ComponentError, Response]
|
|
|
|
// Component factory for registration
|
|
trait ComponentFactory {
|
|
def create(config: ComponentConfig): ZIO[Any, ComponentError, Component]
|
|
}
|
|
|
|
// Property definition (for admin UI)
|
|
case class PropertyDefinition(
|
|
name: String,
|
|
title: String,
|
|
description: Option[String],
|
|
`type`: PropertyType, // string, dropdown, checkbox, etc.
|
|
default: Option[Any],
|
|
options: Option[Map[String, String]], // For dropdowns
|
|
required: Boolean = false,
|
|
validation: List[Validator] = Nil
|
|
)
|
|
```
|
|
|
|
**HTMX Integration:**
|
|
```scala
|
|
// Component can define HTMX handlers
|
|
trait Component {
|
|
// Traditional handler approach
|
|
def onLoadMore(request: Request): ZIO[ComponentEnv, ComponentError, Response] = ???
|
|
|
|
// These become routes: POST /component/{alias}/onLoadMore
|
|
// HTMX attributes: hx-post="/component/posts/onLoadMore" hx-target="#posts-list"
|
|
}
|
|
```
|
|
|
|
**Build Order:** 3rd (needs Plugin System, Config)
|
|
|
|
---
|
|
|
|
### 5. Admin Backend
|
|
|
|
**Responsibility:** Backend administration interface with YAML-driven forms and lists.
|
|
|
|
```scala
|
|
// Backend controller (mirrors WinterCMS Backend\Classes\Controller)
|
|
trait BackendController {
|
|
def actions: Map[String, Action] = Map.empty
|
|
|
|
// Standard CRUD actions
|
|
def index: Action = listAction
|
|
def create: Action = formAction(FormMode.Create)
|
|
def update(id: Id): Action = formAction(FormMode.Update, Some(id))
|
|
def preview(id: Id): Action = formAction(FormMode.Preview, Some(id))
|
|
}
|
|
|
|
// Form configuration (parsed from fields.yaml)
|
|
case class FormConfig(
|
|
fields: Map[String, FieldDefinition],
|
|
tabs: Option[TabsConfig],
|
|
secondaryTabs: Option[TabsConfig]
|
|
)
|
|
|
|
case class FieldDefinition(
|
|
label: String,
|
|
`type`: FieldType, // text, textarea, dropdown, relation, etc.
|
|
span: Span = Span.Auto, // left, right, full, auto
|
|
required: Boolean = false,
|
|
placeholder: Option[String] = None,
|
|
options: Option[FieldOptions] = None,
|
|
dependsOn: List[String] = Nil,
|
|
trigger: Option[TriggerConfig] = None
|
|
)
|
|
|
|
// List configuration (parsed from columns.yaml)
|
|
case class ListConfig(
|
|
columns: Map[String, ColumnDefinition],
|
|
recordUrl: Option[String],
|
|
perPage: Int = 20,
|
|
showCheckboxes: Boolean = true,
|
|
toolbar: ToolbarConfig
|
|
)
|
|
```
|
|
|
|
**YAML Parsing:**
|
|
```scala
|
|
// Use zio-json-yaml or yaml4s for parsing
|
|
val formConfig: ZIO[Any, ParseError, FormConfig] =
|
|
YamlParser.parse[FormConfig](yamlContent)
|
|
```
|
|
|
|
**Build Order:** 6th (needs most other components)
|
|
|
|
---
|
|
|
|
### 6. Database Layer
|
|
|
|
**Responsibility:** Type-safe database access, migrations, model definitions.
|
|
|
|
**Recommendation: ZIO Quill** because:
|
|
- Compile-time query generation (catches SQL errors at compile time)
|
|
- Native ZIO integration via `quill-jdbc-zio`
|
|
- Excellent PostgreSQL support
|
|
- Minimal runtime overhead
|
|
|
|
```scala
|
|
// Model definition (case class + schema)
|
|
case class Post(
|
|
id: Long,
|
|
title: String,
|
|
slug: String,
|
|
content: Option[String],
|
|
publishedAt: Option[Instant],
|
|
authorId: Long,
|
|
createdAt: Instant,
|
|
updatedAt: Instant
|
|
)
|
|
|
|
object Post {
|
|
implicit val schema: Schema[Post] = DeriveSchema.gen[Post]
|
|
|
|
// Quill query definitions
|
|
inline def posts = quote { query[Post] }
|
|
inline def bySlug(slug: String) = quote { posts.filter(_.slug == lift(slug)) }
|
|
inline def published = quote { posts.filter(_.publishedAt.isDefined) }
|
|
}
|
|
|
|
// Repository pattern with ZIO
|
|
trait PostRepository {
|
|
def findById(id: Long): ZIO[Any, RepositoryError, Option[Post]]
|
|
def findBySlug(slug: String): ZIO[Any, RepositoryError, Option[Post]]
|
|
def findPublished(page: Int, perPage: Int): ZIO[Any, RepositoryError, Page[Post]]
|
|
def create(post: Post): ZIO[Any, RepositoryError, Post]
|
|
def update(post: Post): ZIO[Any, RepositoryError, Post]
|
|
def delete(id: Long): ZIO[Any, RepositoryError, Unit]
|
|
}
|
|
|
|
// Implementation using Quill
|
|
class PostRepositoryLive(quill: Quill.Postgres[SnakeCase]) extends PostRepository {
|
|
import quill._
|
|
|
|
def findById(id: Long): ZIO[Any, RepositoryError, Option[Post]] =
|
|
run(quote { Post.posts.filter(_.id == lift(id)) })
|
|
.map(_.headOption)
|
|
.mapError(RepositoryError.fromThrowable)
|
|
}
|
|
```
|
|
|
|
**Migration System:**
|
|
```scala
|
|
// Migrations as versioned ZIO effects
|
|
trait Migration {
|
|
def version: MigrationVersion
|
|
def description: String
|
|
def up: ZIO[Database, MigrationError, Unit]
|
|
def down: ZIO[Database, MigrationError, Unit]
|
|
}
|
|
|
|
// Plugin migrations
|
|
trait Plugin {
|
|
def migrations: List[Migration] = List.empty
|
|
}
|
|
```
|
|
|
|
**Build Order:** 1st-parallel (can be built alongside Plugin System foundation)
|
|
|
|
---
|
|
|
|
### 7. Authentication & Authorization
|
|
|
|
**Responsibility:** User authentication, JWT tokens, permissions, access control.
|
|
|
|
```scala
|
|
// Auth service
|
|
trait AuthService {
|
|
def authenticate(credentials: Credentials): ZIO[Any, AuthError, User]
|
|
def validateToken(token: JwtToken): ZIO[Any, AuthError, User]
|
|
def generateToken(user: User): ZIO[Any, AuthError, JwtToken]
|
|
def refreshToken(token: JwtToken): ZIO[Any, AuthError, JwtToken]
|
|
def hasPermission(user: User, permission: Permission): ZIO[Any, Nothing, Boolean]
|
|
}
|
|
|
|
// Middleware for protected routes
|
|
val authMiddleware: Middleware[AuthService] =
|
|
Middleware.interceptIncomingHandler { request =>
|
|
for {
|
|
token <- extractBearerToken(request)
|
|
user <- ZIO.serviceWithZIO[AuthService](_.validateToken(token))
|
|
} yield (request, Context(user))
|
|
}
|
|
|
|
// Permission-based access control
|
|
val requirePermission: Permission => Middleware[AuthService] = permission =>
|
|
Middleware.interceptIncomingHandler { (request, ctx: Context) =>
|
|
ZIO.serviceWithZIO[AuthService](_.hasPermission(ctx.user, permission))
|
|
.filterOrFail(identity)(AuthError.Forbidden)
|
|
.as((request, ctx))
|
|
}
|
|
```
|
|
|
|
**Build Order:** 5th (needs Database, Plugin System, Config)
|
|
|
|
---
|
|
|
|
### 8. Configuration Service
|
|
|
|
**Responsibility:** Hierarchical configuration from files, database, environment.
|
|
|
|
```scala
|
|
trait ConfigService {
|
|
def get[A: Schema](key: String): ZIO[Any, ConfigError, A]
|
|
def getOpt[A: Schema](key: String): ZIO[Any, ConfigError, Option[A]]
|
|
def set[A: Schema](key: String, value: A): ZIO[Any, ConfigError, Unit]
|
|
}
|
|
|
|
// Configuration sources (in priority order)
|
|
// 1. Environment variables
|
|
// 2. Database (system_settings table)
|
|
// 3. Plugin config files (config/*.yaml)
|
|
// 4. Theme config files
|
|
// 5. Defaults
|
|
```
|
|
|
|
**Build Order:** 1st-parallel (early foundation)
|
|
|
|
---
|
|
|
|
## Component Boundaries
|
|
|
|
| Component | Owns | Does NOT Own |
|
|
|-----------|------|--------------|
|
|
| **Plugin System** | Plugin lifecycle, dependencies, discovery, extension registry | Specific plugin logic |
|
|
| **Theme Engine** | Template loading, rendering, layouts, assets | Page data (components provide this) |
|
|
| **Component System** | Component lifecycle, properties, handlers | Database access (uses Repository) |
|
|
| **Admin Backend** | Form/list rendering, CRUD scaffolding | Business logic (delegates to services) |
|
|
| **Database Layer** | Connections, queries, migrations, schemas | Business rules (pure domain layer) |
|
|
| **Auth Service** | Authentication, tokens, permissions | User registration flows (User plugin) |
|
|
| **Config Service** | Config loading, caching, hierarchy | Config UI (Admin Backend) |
|
|
| **Event Bus** | Event dispatch, subscriptions | Event definitions (plugins define these) |
|
|
|
|
---
|
|
|
|
## Data Flow
|
|
|
|
### Frontend Page Request
|
|
|
|
```
|
|
1. Browser: GET /blog/my-post
|
|
|
|
|
2. ZIO HTTP: Route matching
|
|
|
|
|
3. RouteResolver: Lookup page by URL pattern
|
|
|-- Database: pages table (or theme file scan)
|
|
|-- Returns: PageDefinition { url: "/blog/:slug", components: ["blogPost"] }
|
|
|
|
|
4. ThemeResolver: Load theme, layout, page template
|
|
|-- Filesystem: themes/mytheme/pages/blog-post.htm
|
|
|-- Returns: LayoutDefinition, PageTemplate
|
|
|
|
|
5. ComponentResolver: Instantiate components
|
|
|-- PluginRegistry: Get component factories
|
|
|-- For each component in page:
|
|
| |-- Factory.create(config)
|
|
| |-- Component.init()
|
|
| |-- Component.onRun()
|
|
| |-- Component.data() -> Map[String, Any]
|
|
|
|
|
6. PageRenderer: Render template with component data
|
|
|-- Merge: layout + page + partial templates
|
|
|-- Inject: component data into template scope
|
|
|-- Execute: ScalaTags/template -> Html
|
|
|
|
|
7. Response: HTML with HTMX attributes
|
|
```
|
|
|
|
### HTMX Handler Request
|
|
|
|
```
|
|
1. Browser: POST /component/blogPosts/onLoadMore (HTMX)
|
|
|-- Headers: HX-Request: true, HX-Trigger: load-more-btn
|
|
|
|
|
2. ZIO HTTP: Component handler route
|
|
|
|
|
3. ComponentResolver:
|
|
|-- Load component by alias from session/page context
|
|
|-- Component.handlers("onLoadMore")
|
|
|
|
|
4. Handler execution:
|
|
|-- Handler(request) -> ZIO[ComponentEnv, Error, Response]
|
|
|-- Returns: HTML fragment
|
|
|
|
|
5. Response: HTML fragment
|
|
|-- HTMX swaps into target element
|
|
```
|
|
|
|
### Admin Form Save
|
|
|
|
```
|
|
1. Admin: POST /backend/blog/posts/update/5
|
|
|-- Body: form data (multipart or JSON)
|
|
|
|
|
2. BackendController.update(5):
|
|
|-- FormWidget.load(fields.yaml)
|
|
|-- Validate input against field definitions
|
|
|-- Event: FormSaving(widget, model, data) // Plugins can modify
|
|
|
|
|
3. Model save:
|
|
|-- Repository.update(model)
|
|
|-- Event: ModelUpdated(model, changes) // Plugins react
|
|
|
|
|
4. Response: Redirect to list or show success flash
|
|
```
|
|
|
|
---
|
|
|
|
## ZIO Patterns
|
|
|
|
### Layer Architecture
|
|
|
|
```scala
|
|
// Application layers (bottom-up)
|
|
val dataLayer: ZLayer[Any, Nothing, Database & Cache] =
|
|
ZLayer.make[Database & Cache](
|
|
PostgresDatabase.layer,
|
|
RedisCache.layer
|
|
)
|
|
|
|
val coreServicesLayer: ZLayer[Database & Cache, Nothing, Services] =
|
|
ZLayer.make[Services](
|
|
ConfigServiceLive.layer,
|
|
EventBusLive.layer,
|
|
AuthServiceLive.layer,
|
|
PluginRegistryLive.layer
|
|
)
|
|
|
|
val domainLayer: ZLayer[Services & Database, Nothing, Repositories] =
|
|
ZLayer.make[Repositories](
|
|
PostRepositoryLive.layer,
|
|
UserRepositoryLive.layer,
|
|
// ... more repositories
|
|
)
|
|
|
|
val httpLayer: ZLayer[Services & Repositories, Nothing, HttpApp] =
|
|
ZLayer.make[HttpApp](
|
|
PublicRoutes.layer,
|
|
AdminRoutes.layer,
|
|
ApiRoutes.layer,
|
|
MiddlewareStack.layer
|
|
)
|
|
|
|
// Full application
|
|
val appLayer: ZLayer[Any, AppError, HttpApp] =
|
|
dataLayer >>> coreServicesLayer >>> domainLayer >>> httpLayer
|
|
```
|
|
|
|
### Effect Organization
|
|
|
|
```scala
|
|
// Pure business logic (no effects)
|
|
object PostLogic {
|
|
def generateSlug(title: String): String =
|
|
title.toLowerCase.replaceAll("[^a-z0-9]+", "-")
|
|
|
|
def validatePost(post: Post): Validated[ValidationError, Post] = ???
|
|
}
|
|
|
|
// Effectful operations wrapped in ZIO
|
|
trait PostService {
|
|
def createPost(input: CreatePostInput): ZIO[Any, PostError, Post]
|
|
}
|
|
|
|
class PostServiceLive(
|
|
repo: PostRepository,
|
|
events: EventBus,
|
|
auth: AuthService
|
|
) extends PostService {
|
|
|
|
def createPost(input: CreatePostInput): ZIO[Any, PostError, Post] =
|
|
for {
|
|
_ <- auth.requirePermission(Permission.CreatePost)
|
|
slug <- ZIO.succeed(PostLogic.generateSlug(input.title))
|
|
valid <- ZIO.fromEither(PostLogic.validatePost(input.toPost(slug)))
|
|
created <- repo.create(valid)
|
|
_ <- events.emit(PostCreated(created))
|
|
} yield created
|
|
}
|
|
```
|
|
|
|
### Resource Safety
|
|
|
|
```scala
|
|
// Database transactions
|
|
def withTransaction[R, E, A](zio: ZIO[R & Transaction, E, A]): ZIO[R & Database, E, A] =
|
|
ZIO.scoped {
|
|
for {
|
|
tx <- Database.beginTransaction
|
|
result <- zio.provideSome[R](ZLayer.succeed(tx))
|
|
_ <- tx.commit
|
|
} yield result
|
|
}
|
|
|
|
// Plugin lifecycle with proper cleanup
|
|
def loadPlugins: ZIO[Scope & PluginRegistry, PluginError, List[Plugin]] =
|
|
for {
|
|
registry <- ZIO.service[PluginRegistry]
|
|
plugins <- registry.discover
|
|
sorted <- registry.topologicalSort(plugins) // Dependency order
|
|
loaded <- ZIO.foreach(sorted) { manifest =>
|
|
ZIO.acquireRelease(
|
|
registry.load(manifest.id).tap(_.boot)
|
|
)(plugin => plugin.shutdown.ignore)
|
|
}
|
|
} yield loaded
|
|
```
|
|
|
|
### Error Handling
|
|
|
|
```scala
|
|
// Domain errors as sealed traits
|
|
sealed trait PostError
|
|
object PostError {
|
|
case class NotFound(id: Long) extends PostError
|
|
case class SlugTaken(slug: String) extends PostError
|
|
case class ValidationFailed(errors: List[ValidationError]) extends PostError
|
|
case class DatabaseError(cause: Throwable) extends PostError
|
|
}
|
|
|
|
// Error recovery and mapping
|
|
def findOrCreate(slug: String, default: => Post): ZIO[PostService, Nothing, Post] =
|
|
ZIO.serviceWithZIO[PostService](_.findBySlug(slug))
|
|
.catchSome { case PostError.NotFound(_) =>
|
|
ZIO.serviceWithZIO[PostService](_.create(default))
|
|
}
|
|
.orDie // Defect if still fails
|
|
```
|
|
|
|
---
|
|
|
|
## Build Order
|
|
|
|
Based on dependencies, build in this order:
|
|
|
|
### Phase 1: Foundation (Parallel)
|
|
| Component | Dependencies | Effort |
|
|
|-----------|--------------|--------|
|
|
| Database Layer | None | Medium |
|
|
| Config Service | None | Low |
|
|
| Event Bus | None | Low |
|
|
|
|
### Phase 2: Core Services
|
|
| Component | Dependencies | Effort |
|
|
|-----------|--------------|--------|
|
|
| Plugin System | Config, Event Bus, Database | High |
|
|
|
|
### Phase 3: Domain Components
|
|
| Component | Dependencies | Effort |
|
|
|-----------|--------------|--------|
|
|
| Component System | Plugin System, Config | Medium |
|
|
| Auth Service | Database, Config | Medium |
|
|
|
|
### Phase 4: Rendering
|
|
| Component | Dependencies | Effort |
|
|
|-----------|--------------|--------|
|
|
| Theme Engine | Plugin System, Component System, Config | High |
|
|
| Template Parser | Theme Engine | Medium |
|
|
|
|
### Phase 5: HTTP Layer
|
|
| Component | Dependencies | Effort |
|
|
|-----------|--------------|--------|
|
|
| Request Pipeline | All above | Medium |
|
|
| Public Routes | Request Pipeline | Low |
|
|
| API Routes | Request Pipeline, Auth | Low |
|
|
|
|
### Phase 6: Admin
|
|
| Component | Dependencies | Effort |
|
|
|-----------|--------------|--------|
|
|
| Admin Backend | All above | High |
|
|
| Form Widget System | Admin Backend | High |
|
|
| List Widget System | Admin Backend | Medium |
|
|
|
|
### Phase 7: Core Plugins
|
|
| Component | Dependencies | Effort |
|
|
|-----------|--------------|--------|
|
|
| User Plugin | All core | High |
|
|
| Blog Plugin | All core | Medium |
|
|
| Pages Plugin | All core | Medium |
|
|
|
|
---
|
|
|
|
## WinterCMS Architecture Comparison
|
|
|
|
| Aspect | WinterCMS | SummerCMS |
|
|
|--------|-----------|-----------|
|
|
| **Language** | PHP (Laravel) | Scala (ZIO) |
|
|
| **Effect System** | None (exceptions) | ZIO effects (typed errors) |
|
|
| **DI** | Laravel Container | ZIO Layers |
|
|
| **HTTP** | Laravel Router | ZIO HTTP |
|
|
| **Database** | Eloquent ORM | ZIO Quill (compile-time) |
|
|
| **Templates** | Twig | ScalaTags + custom parser |
|
|
| **Events** | Laravel Events | ZIO Event Bus |
|
|
| **Config** | PHP arrays/YAML | YAML + ZIO Config |
|
|
| **Plugin Discovery** | Filesystem scan | Filesystem + SPI |
|
|
| **Model Extension** | Dynamic `extend()` | Type classes + events |
|
|
| **Frontend** | AJAX (Snowboard) | HTMX + Vue optional |
|
|
|
|
### Key Architectural Differences
|
|
|
|
1. **Type Safety:** SummerCMS gains compile-time query validation (Quill) and typed errors (ZIO) that WinterCMS lacks.
|
|
|
|
2. **Concurrency:** ZIO fibers enable true async/concurrent plugin loading and request handling vs PHP's request-per-process model.
|
|
|
|
3. **Resource Safety:** ZIO's `Scope` ensures database connections, file handles, and plugin resources are properly released even on errors.
|
|
|
|
4. **Model Extension:** WinterCMS uses runtime `extend()` calls. SummerCMS uses compile-time type classes for static extensions, plus events for runtime extensions.
|
|
|
|
5. **Testing:** ZIO's layer system enables true dependency injection for testing. Mock any layer by providing a test implementation.
|
|
|
|
---
|
|
|
|
## Sources
|
|
|
|
- [ZIO Architectural Patterns](https://zio.dev/reference/architecture/architectural-patterns/)
|
|
- [ZIO Service Pattern](https://zio.dev/reference/service-pattern/)
|
|
- [ZIO Layers](https://zio.dev/reference/contextual/zlayer/)
|
|
- [Structuring ZIO 2 Applications](https://softwaremill.com/structuring-zio-2-applications/)
|
|
- [ZIO HTTP Introduction](https://zio.dev/zio-http/)
|
|
- [ZIO HTTP HTMX Integration](https://index.scala-lang.org/zio/zio-http/artifacts/zio-http-htmx/3.3.0)
|
|
- [ZIO Quill Getting Started](https://zio.dev/zio-quill/getting-started/)
|
|
- [ZIO Schema](https://zio.dev/zio-schema/)
|
|
- [ZIO JSON](https://zio.dev/zio-json/)
|
|
- [ScalaTags Documentation](https://com-lihaoyi.github.io/scalatags/)
|
|
- [WinterCMS Plugin Extension](https://wintercms.com/docs/v1.2/docs/plugin/extending)
|
|
- [WinterCMS Plugin Registration](https://wintercms.com/docs/v1.2/docs/plugin/registration)
|
|
- [Tapir ZIO HTTP4s Integration](https://tapir.softwaremill.com/en/latest/server/zio-http4s.html)
|
|
- [Plugin Architecture Patterns](https://www.dotcms.com/blog/plugin-achitecture)
|