← Architecture

3.1.1 Application foundation

This guide describes the reusable application layer below KMP’s domain modules. It complements the multi-tenant architecture, which is the authority for datasource and scope rules.

Bootstrap and dependency injection

app/src/Application.php owns plugin loading, middleware ordering, service container bindings, authentication/authorization services, workflow handler registration, and shared registries. app/config/bootstrap.php loads configuration and connects it to CakePHP facilities.

Prefer constructor injection for services registered in the container. A controller may retrieve a CakePHP component or table through established framework APIs, but business services should not depend on controller state or the service locator. Avoid adding process-global mutable state; long-running workers make state leaks visible across tenants and jobs.

Base classes

Controllers

Tenant-facing web controllers extend AppController; API controllers extend ApiController. Their responsibilities are to:

  1. parse and validate the transport-level request;
  2. load the target model/resource;
  3. call authorize(), authorizeModel(), and/or applyScope();
  4. invoke the owning table or service; and
  5. produce HTML, Turbo Frame, redirect, or API responses.

Use TurboResponseTrait for established Turbo response behavior, DataverseGridTrait for grids, and WorkflowDispatchTrait where an existing domain flow uses it. Do not put multi-record workflows, plugin discovery, or cross-tenant loops in controllers.

Tables and entities

Tables extend App\Model\Table\BaseTable; entities extend App\Model\Entity\BaseEntity. These provide common project conventions and audit/public-ID integration used throughout the schema. Tables own associations, validation, build rules, finders, persistence callbacks, and domain invariants that are local to their aggregate.

Entities expose record behavior and virtual fields but should not open service connections or orchestrate external side effects. Use services for workflows that span aggregates, plugins, mail, storage, or queues.

Policies

Policies extend App\Policy\BasePolicy. Resource methods answer whether an identity may perform an action. Table/query policies constrain which records an identity may see. Branch-aware permission reach is expressed through the existing permission policy and scoping helpers, not copied SQL predicates.

A view may hide unavailable controls for usability, but server-side policy checks remain mandatory.

Service layer

app/src/Services is organized by capability. Important boundaries include:

A service should have one clear owner, explicit inputs/outputs, transactional boundaries where needed, and retry-safe side effects. It must not silently choose a tenant; callers enter the tenant scope first.

Registries and plugin integration

Registries let plugins contribute capabilities without core code naming each plugin:

Registry Purpose
NavigationRegistry navigation items and placement
ViewCellRegistry plugin-owned detail/sidebar/tab fragments
ApiDataRegistry approved API data contributions
ActionItemCompletionFormRegistry action-item completion UI/providers
Workflow*Registry classes workflow entities, triggers, conditions, actions, approvers
ApprovalContextRendererRegistry domain rendering for approval context

Registration occurs during application/plugin bootstrap using stable keys and provider contracts. Rendered cells and navigation items still perform normal authorization. Avoid hard-coding plugin controllers, templates, or table names into core UI.

Grid foundation

Grid controllers use DataverseGridTrait, while column definitions extend App\KMP\GridColumns\BaseGridColumns. The grid provider describes fields, labels, formatters, sortability, search, filters, and system views. Controllers apply policy scope before returning rows.

Keep filtering metadata declarative. Date ranges use the current filterable and filterType: date-range column metadata; do not add controller-specific filter parsers when the shared grid can express the behavior. Treat column HTML as user-facing UI and preserve labels, focus, status announcements, and non-color cues.

Events and side effects

CakePHP events are appropriate for decoupled notifications where ordering and transaction semantics are explicit. Use a direct service call when the caller requires a result or failure. Queue slow or retryable work through the tenant queue inside tenant context, or through a platform job for an explicitly central operation.

Do not dispatch an irreversible external effect before the owning transaction commits. Use idempotency keys or durable state when retries may occur.

Caching

Cache only data whose ownership and invalidation are understood. Tenant-derived values go through TenantAwareCache; registry/operations values use an explicit platform namespace. Cache keys should describe the semantic input, not just an integer record ID. Mutation paths must invalidate all affected representations, including navigation, permissions, grids, and plugin contributions where applicable.

CakePHP core/model metadata caches are process/application caches and are not a substitute for tenant-aware result caching.

Dates and timezones

Persist timestamps in UTC. Use immutable CakePHP date/time values for domain logic where practical. Convert user-entered and displayed date-times through the project timezone utilities, and pre-format values before handing them to mailers. Date-only business boundaries may intentionally use the configured application/kingdom timezone; the owning feature guide should state that rule.

Error and audit behavior

Expected validation and authorization failures should produce useful, non-sensitive responses. Unexpected failures flow to CakePHP error handling and structured logs. Attach correlation and safe domain context, but never log secret values, raw credentials, recovery keys, entire request bodies, or member records.

Use existing audit and Footprint attribution. Platform lifecycle, secret, backup, and operator actions have separate central audit expectations and should not be represented only in a tenant log.

How to add a feature

  1. Identify the owning core domain or active plugin.
  2. Define its tenant/platform data ownership before creating a table.
  3. Reuse a base class, registry, behavior, grid, or workflow provider where it fits.
  4. Put transport logic in controllers, persistence rules in tables/entities, authorization in policies, and orchestration in services.
  5. Add a forward migration to the correct platform/core/plugin track.
  6. Test policy denial, branch scope, tenant A/B isolation, retries, and UI accessibility as applicable.
  7. Update the nearest durable guide rather than adding a temporary task note.

Source map