Initial research ahahahah
This commit is contained in:
794
.planning/research/ARCHITECTURE.md
Normal file
794
.planning/research/ARCHITECTURE.md
Normal file
@@ -0,0 +1,794 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user