
Symfonicat
One Symfony runtime. Many domains, packages, and native capabilities.
Symfonicat is a Symfony 8 multi-tenant frontend runtime and administration system. It composes host-aware shells from domains, reusable subdomains, endpoint patterns, parcels, modules, PSR-15 middleware, scoped environment values, generated Electron applications, package assets, and compiled C or Go extensions.
# Mental model
Symfonicat separates the public data plane from the administration control plane. Public traffic never depends on live CRUD queries: it builds an in-memory catalog from config/packages/symfonicat.yaml. The protected /core UI edits Doctrine rows, then automatically writes those rows back to the same YAML file.
Bare host, top-level parcel, modules, middleware, env, and child subdomains.
An affix scoped to an optional domain. The same affix can be reused on multiple domains.
A repeatable path-argument pattern, optionally enforced against one domain or subdomain.
A package-scoped frontend entry and its base environment values.
An attachable PHP + JavaScript feature callable only inside an authorized runtime scope.
A selected URL context used to generate a buildable Electron shell.
# Quick start
The repository is self-contained: clone it, build the multi-stage image, synchronize discovered package data, load the YAML catalog, create a two-factor administrator, and unlock the core UI.
git clone https://github.com/symfonicat/core symfonicat
cd symfonicat
cp .env.example .env
docker compose up -d --build
docker exec -it php bin/console symfonicat:schema:update
docker exec php bin/console symfonicat:load
docker exec -it php bin/console symfonicat:admin:create <email>
touch symfonicat.lockAdd 127.0.0.1 example.com and 127.0.0.1 subdomain1.example.com to /etc/hosts. The lock file intentionally makes /core look like a 404 until it is enabled.
# Admin interface
The /core control plane is the operational center of Symfonicat. It is a server-rendered Bootstrap interface backed by Doctrine repositories and forms, giving administrators one place to configure the runtime without manually maintaining every relation in YAML. The dashboard opens on Domains and provides direct navigation to Domains, Subdomains, Endpoints, Parcels, Middleware, Applications, Environment, Files, schema synchronization, YAML dump/load, and logout.
AdminLockSubscriber runs at priority 4096. Until symfonicat.lock exists, every core route returns a private, cache-disabled, noindex 404. Caddy reinforces the same boundary before PHP handles the request.
Symfony form login uses AdminUserProvider, CSRF protection, a five-minute user snapshot cache, and a Redis-backed rate limiter capped at 10 attempts per five minutes.
Each Admin owns a SHA-1, 30-second, six-digit TOTP secret created by symfonicat:admin:create. Verification is bound to both the administrator and an authorization-header fingerprint in session state.
Doctrine flushes originating from /core are inspected. Changes to any non-Admin Symfonicat entity or tracked association automatically dump the ordered symfonicat_* tables back to deterministic YAML.
What administrators configure
Create and edit bare hostnames, select their parcels, attach modules and middleware, assign environment values, and manage the top-level runtime context.
Create reusable affixes, optionally bind them to a Domain, select a parcel, enable catch behavior, and independently attach modules, middleware, and environment values.
Define literal and wildcard path arguments, trailing-path catch behavior, Domain or Subdomain enforcement, a parcel, scoped modules, middleware, and environment overrides.
Review package-discovered frontend entries and choose which parcel implements the shell for each Domain, Subdomain, or Endpoint.
Create desktop application targets, choose their Domain/Subdomain/Endpoint context, set names and environment overrides, then generate Electron skeletons.
Manage grouped keys and their Parcel, Domain, Subdomain, Endpoint, and Application values through dedicated scoped forms.
Review automatically discovered PSR-15 services and attach them to the runtime scopes where they should execute.
Upload validated default, Domain, and Subdomain assets that symfonicat_asset() resolves according to the active request.
Schema and configuration workflow
The schema screen runs the same synchronization path as symfonicat:schema:update: it updates Doctrine metadata, reconciles tagged middleware, discovers package parcels and modules, and reports created, updated, or removed rows. The YAML controls expose explicit dump and load operations. Dumps exclude administrators, order tables by foreign-key dependency, and normalize JSON and booleans; loads validate tables and columns, delete children before parents, insert parents before children, reset sequences, and run inside a transaction.
The admin interface uses the database for safe CRUD and validation. Its automatic YAML output becomes the deployable catalog read by public requests, so the public runtime can resolve tenants and features without depending on live control-plane queries.
# Docker and FrankenPHP stack
The Dockerfile starts from dunglas/frankenphp:builder-php8.4. Its base stage installs the C toolchain, Autoconf, Node 22, Composer, xcaddy, inotify, and the PHP extensions required by the application. It downloads FrankenPHP source because Symfonicat recompiles the server with project and vendor Go modules.
npm ci plus Encore build with parcel and module discovery.The php, npm, and messenger-worker services share named vendor and node_modules volumes. The entrypoint seeds /symfonicat/vendor from the Composer tree baked into the image, writes the Caddy TLS snippet from ECS certificate environment values when present, and otherwise uses internal TLS. Set SYMFONICAT_NATIVE_WATCH=1 to replace the normal server process with the native rebuild watcher.
# Public request lifecycle
- PublicRuntimeSubscriberSkips
/core/*and/application/*, resolves Domain and Subdomain from the host, validates signed/m/*module requests, and selects the most-specific matching Endpoint. - Normal Symfony routingRegular routes remain authoritative. Runtime catch-all routes use priority
-1000and require an explicit request attribute. - RuntimeRendererChooses the active entity, resolves an override or default Twig shell, issues a signed module context, and renders domain/subdomain/endpoint/application variables.
- RuntimeMiddlewareRunnerBuilds a deduplicated PSR-15 chain from domain → subdomain → endpoint and converts between HttpFoundation and PSR-7.
Template precedence
templates/{domain,subdomain,endpoint}/overrides/{id}.html.twig
templates/{domain,subdomain,endpoint}/main.html.twigFor subdomains, the override id is the public affix. For domains and endpoints, it is the clean entity id. The renderer also exposes a signed request object used by frontend modules.
# Domains and reusable subdomains
A Domain id is always a bare hostname. DomainService finds the longest configured suffix, so both example.com and any host beneath it resolve to that domain. Public reads come from YAML; /core reads the database repository.
A Subdomain has an internal integer id, a public affix, and an optional Domain relation. Uniqueness is domain + affix, not affix alone. That means app.example.com and app.example.org can be distinct rows with different parcels, modules, middleware, and environment values while reusing the same public label.
SubdomainService first extracts the leading host affix, then asks RuntimeConfig for the row keyed by domain|affix. A domainless row is a separate fallback lookup, not a collision.
# Endpoints
Endpoints turn paths into runtime targets. arguments is an ordered list of literals or * wildcards. catch allows trailing segments; an empty argument list with catch enabled acts as a wildcard for the current path. Endpoint rows can enforce a Domain or a Subdomain, then contribute their own parcel, modules, middleware, and env. Longer argument patterns are evaluated before shorter patterns so the most specific matching endpoint wins.
# Parcels and assets
A Parcel is the package-scoped frontend shell unit. Composer packages opt in with extra.symfonicat: true and place entries under assets/parcel/<name>/index.js. PackageDiscoveryService produces ids such as symfonicat/core/domainparcel; ParcelService synchronizes those paths and clears entity references when stale package parcels disappear.
Administrators choose which discovered Parcel implements each Domain, Subdomain, or Endpoint. This separates frontend implementation from tenant and route configuration: one Parcel can be reused by many runtime scopes, while any scope can switch to a different Parcel from the admin interface without changing its hostname, affix, or path rules.
Provides the default frontend shell for the bare host and its public paths.
Replaces the Domain shell for a reusable subdomain context.
Implements a specific matched path experience, with its own modules, middleware, and environment overrides.
Each Parcel can contribute base environment values that later scopes override.
Private build entries
symfonicat:data:webpack reads the active catalog, scans root and installed vendor packages, and passes configured parcel/module entries to webpack.symfonicat.js. Encore produces versioned bundles under public/build, with async WebAssembly enabled.
Public files
symfonicat_asset(path) checks the active subdomain, then domain, then public/default. The admin file screen uploads domain/subdomain/default assets through validated form models.
# Modules and the hybrid package system
Modules are package-discovered features attachable to Domains, Subdomains, or Endpoints. The backend route exists only when its module is attached to the active runtime scope. This is enforced twice: signed request context validation in PublicRuntimeSubscriber, then attachment checks inside AbstractModuleController.
Runtime-aware services
A module is not limited to receiving JSON. Its controller inherits the core context services, and attributed action parameters are made callable during container compilation. Modules can therefore inspect the complete active request composition and respond differently for each tenant, path, application, or environment.
Loads the active Domain and normalized host, allowing a module to make tenant-level decisions.
Loads the active Subdomain, including its reusable affix and Domain relationship.
Exposes the current path, individual arguments, all arguments, and endpoint-style argument matching.
Reads one environment key or the fully merged environment across Parcel, Domain, Subdomain, Endpoint, and Application scopes.
Loads the selected Application, resolves one for the current context, and reports whether the request is operating as an Application.
Provides the YAML-backed catalog of Domains, Subdomains, Endpoints, Applications, Parcels, Modules, and Middleware without public database queries.
Identifies the current module from its route and connects it to package discovery and attachment enforcement.
Exposes request attributes such as the resolved Endpoint and decoded module_json payload alongside standard headers and query data.
This service-based context model lets the same module implementation behave differently on separate Domains, on reused Subdomain affixes, at specific Endpoints, under different merged Env values, or inside a generated Application. Because these are normal Symfony services, modules can also request other typed application services in their #[Module] action signatures.
PHP module API
<?php
namespace Acme\Feature\Module;
use Symfonicat\Attribute\Module;
use Symfonicat\Attribute\ModuleRoute;
use Symfonicat\Controller\AbstractModuleController;
use Symfony\Component\HttpFoundation\Response;
#[ModuleRoute(path: '/m/acme/catalog')]
final class CatalogController extends AbstractModuleController
{
#[Module(permission: 'PUBLIC_ACCESS')]
public function product(): Response
{
$payload = $this->requestStack
?->getCurrentRequest()
?->attributes->get('module_json');
return $this->json([
'ok' => true,
'productId' => $payload['productId'] ?? null,
'domain' => $this->domainService->load()?->getTld(),
]);
}
#[Module]
public function card(): Response
{
return $this->html('<article>Rendered by PHP</article>');
}
}#[ModuleRoute] targets a controller class and defaults to /m/<composer-package>. #[Module] targets a public action and defaults to PUBLIC_ACCESS. During container compilation, Symfonicat scans module actions and makes typed action dependencies callable.
JavaScript bridge
const mod = 'acme/catalog/main'
mod.log('module active')
// POST Brotli-compressed JSON to /m/acme/catalog/main/product
const product = await mod.json('product', {
productId: 42,
})
// Request an HTML fragment from the same attached module.
const card = await mod.html('card', {
productId: 42,
})
document.querySelector('#product-card').innerHTML = card.json(), .html(), and .log()Caddy’s brotli_decompress Go handler expands every /m/* body before PHP. SymfonicatModuleSubscriber decodes JSON into the module_json request attribute. Empty JSON responses normalize to {}; HTML stays text.
One package, two back ends, one frontend
Symfonicat and Nymfonicat consume the same Composer-managed hybrid package. The shared package keeps browser code at its root and splits only the server implementation: Symfony/PHP code lives under src/, while Nymfonicat loads its Node.js counterpart from modules/.
symfonicat/analytics/
├── composer.json # extra.symfonicat: true
├── package.json # optional Node package metadata
├── assets/
│ ├── module/main/index.js # one browser module for both runtimes
│ └── parcel/analyticsparcel/index.js
├── modules/main.js # Nymfonicat Node.js backend
└── src/Module/AnalyticsModule.php # Symfonicat PHP backendassets/module/main/index.js is compiled by both build systems and uses one stable module id and browser API.
assets/parcel/analyticsparcel/index.js supplies the same shell source for Domain, Subdomain, or Endpoint composition in either runtime.
src/Module/AnalyticsModule.php declares attributed module actions and uses Symfonicat’s runtime services.
modules/main.js implements the equivalent Nymfonicat behavior and receives Express runtime context.
Hybrid lifecycle
- Composer discovers the package
extra.symfonicat: trueopts the package into Symfonicat and is also accepted by Nymfonicat discovery. - Each runtime registers its backendSymfonicat discovers attributed PHP actions; Nymfonicat resolves the package’s Node module path.
- Both builds find the same assets
assets/modulesupplies browser features andassets/parcelsupplies shell entries from the installed package. - Scopes select featuresAdministrators attach the module and choose the parcel for a Domain, Subdomain, or Endpoint.
- The browser uses one contractThe same frontend sends the same action and payload; the current runtime dispatches locally to PHP or Node.js.
Keep the shared frontend entry, module id, action names, payloads, and JSON/HTML response shapes compatible. Put runtime-specific logic only in src/ and modules/. This yields one browser experience without forcing either server runtime to emulate the other.
# PSR-15 middleware
Every service implementing Psr\Http\Server\MiddlewareInterface is automatically tagged symfonicat.middleware. The YAML catalog stores class-backed middleware rows and join tables for each scope.
- Domain middlewareruns whenever a domain is active
- Subdomain middlewareadds to subdomain and endpoint renders
- Endpoint middlewareadds only for endpoint renders
The runner deduplicates by id/class, preserves scope order, passes Symfonicat request attributes into PSR-7, then converts the final PSR response back to HttpFoundation.
# Environment values
Environment keys are two-level names: an EnvParent such as colors plus an Env such as primary. Scope rows carry the actual string. EnvService recursively merges them and exposes the same grouped structure to Twig and window.env.
- Parcelbase defaults for the selected shell
- Domaintenant-wide overrides
- Subdomainsubdomain parcel + scoped overrides
- Endpointpath-context overrides
- Applicationhighest priority in application context
{{ env('colors.primary') }}
<script>console.log(window.env.colors.primary)</script>Infrastructure environment variables
Symfony loads .env, local overrides, and environment-specific files in increasing precedence; real process variables win. Compose rewrites database, Redis, Messenger, and Mercure addresses to their service names. The exported .env.example documents these runtime and AWS inputs:
# Symfony and runtime
APP_ENV APP_DEBUG APP_SECRET APP_SHARE_DIR SERVER_NAME DEFAULT_URI
DATABASE_URL REDIS_URL LOCK_DSN
MESSENGER_TRANSPORT_DSN MESSENGER_FAILED_TRANSPORT_DSN
MESSENGER_CONSUMER_NAME MESSENGER_WORKERS
MERCURE_URL MERCURE_PUBLIC_URL MERCURE_JWT_SECRET
ASSET_BASE_URL SYMFONY_TRUSTED_PROXIES SYMFONICAT_NATIVE_WATCH
# Local PostgreSQL
POSTGRES_DB POSTGRES_USER POSTGRES_PASSWORD
# Shared AWS configuration
AWS_REGION AWS_PROFILE
# ECR image build and push
AWS_ECR_REGION AWS_ECR_ACCOUNT_ID AWS_ECR_REPOSITORY AWS_ECR_TAG
AWS_ECR_CONTEXT AWS_ECR_DOCKERFILE AWS_ECR_BUILD_TARGET AWS_ECR_LOCAL_IMAGE
# ECS task and service
AWS_ECS_REGION AWS_ECS_CLUSTER_NAME AWS_ECS_SERVICE_NAME AWS_ECS_CONTAINER_NAME
AWS_ECS_IMAGE_URI AWS_ECS_WAIT_FOR_STABLE AWS_ECS_TASK_FAMILY AWS_ECS_LAUNCH_TYPE
AWS_ECS_CPU AWS_ECS_MEMORY AWS_ECS_CONTAINER_PORT AWS_ECS_DESIRED_COUNT
AWS_ECS_ASSIGN_PUBLIC_IP AWS_ECS_SUBNETS_JSON AWS_ECS_SECURITY_GROUPS_JSON
AWS_ECS_TARGET_GROUP_ARN AWS_ECS_EXECUTION_ROLE_ARN AWS_ECS_TASK_ROLE_ARN
# ACM and Route 53
AWS_CERT_REGION AWS_ECS_TLS_FULLCHAIN_B64 AWS_ECS_TLS_PRIVATE_KEY_B64
AWS_ROUTE53_REGION AWS_ROUTE53_RECORDS_JSON# symfonicat.yaml
This file is both the deployable runtime catalog and the serialized admin configuration. AdminYaml introspects every symfonicat_* table except admins, orders tables by foreign-key dependency, normalizes JSON/booleans, and writes deterministic YAML. Loading runs in a transaction, deletes children before parents, inserts parents before children, validates tables/columns, and resets auto-increment sequences.
No database lookup is required to resolve domains, subdomains, endpoints, parcels, modules, middleware, applications, or env.
Forms operate on repositories. A flush subscriber persists the resulting control-plane state back to YAML.
The catalog contains rows and join rows for parcels, domains, subdomains, endpoints, applications, env parents/keys/values, middleware, modules, and every attachment relation. Composer install runs symfonicat:purge; deployments then rebuild the schema and load clean configuration.
# Native C and Go system
Native code is a first-class package capability. Discovery scans three package layers: the application root, built-in core/, and every installed Composer package opted into Symfonicat. Each layer can ship traditional PHP C extensions and/or Go modules compiled into FrankenPHP.
native/
├── ext/
│ └── <php-extension>/
│ ├── config.m4
│ └── <extension>.c
└── go/
└── <go-module>/
├── go.mod
├── *.go
└── *.stub.php
core/native/{ext,go}/...
vendor/<vendor>/<package>/native/{ext,go}/...C extension path
- Discover
NativeDiscoveryServicefinds child directories under everynative/extroot using exact and glob package patterns. - Stage
bin/native-buildcopies each directory into/usr/src/php/ext/<name>. - Configure
config.m4is inspected forPHP_ARG_ENABLEorPHP_ARG_WITH. - CompileAll discovered names are passed to
docker-php-ext-install. Built-ins include the Cremove_stringfunction and the nativemodule_routesscanner.
Go / FrankenPHP path
- DiscoverDirectories under every
native/goroot must include Go source; module-backed entries expose a path ingo.mod. - Generate PHP bridgeFiles containing
export_php:functionrun throughfrankenphp extension-init, producing C headers, arginfo, stubs, and bridge code. - Resolve modules
go mod tidyvalidates each module before build flags are assembled. - Compile serverxcaddy rebuilds FrankenPHP with CGO, Mercure, Vulcain, Caddy Brotli, project/vendor modules, and smaller no-database build tags.
Exports symfonicat_reverse(string): string from Go into PHP using FrankenPHP Zend-string conversion.
Registers Caddy handlers for build-file precompression and transparent module request decompression.
A C extension reads Composer metadata and PSR-4 maps to accelerate discovery of opted-in module controllers.
A conventional PHP C extension used by the generated native service proxy example.
Service proxy compilation
interface TextToolsInterface
{
#[CompileNative]
public function removeString(string $value, string $needle): string;
}
// NativeProxyCompilerPass detects the attribute and aliases the
// interface to Symfonicat\NativeProxy\Service\TextToolsNativeProxy.
// The proxy calls the compiled C extension function directly:
return \remove_string($value, $needle);#[CompileNative] marks interface/service methods intended for native delegation. NativeProxyCompilerPass detects those methods, locates the matching Symfonicat\NativeProxy class, decorates the original service, and aliases supported App/Native/Symfonicat interfaces to the proxy.
Development watcher
With SYMFONICAT_NATIVE_WATCH=1, bin/native-watch snapshots root/core/vendor native files, prefers recursive inotify, falls back to polling, recompiles on change, and restarts FrankenPHP only after a successful native build. It records backend, PID, snapshot, timestamp, and last changed path in a sentinel file.
# Applications and Electron
An Application selects a Domain, Subdomain, or Endpoint context. Requests carrying the application id expose the entity in Twig and window.application; its environment rows override endpoint values.
symfonicat:application:build clears generated output and creates applications/<application.id>/main.js, package.json, and README.md. It resolves templates/application/overrides/<id>.js.twig before the default template, builds an HTTPS URL from domain + subdomain + endpoint arguments, and configures Electron Builder.
# Console command map
symfonicat:schema:updateUpdate the Doctrine schema and synchronize middleware, parcel, module, domain, and subdomain rows.
symfonicat:loadLoad config/packages/symfonicat.yaml into the database-backed control plane.
symfonicat:dumpExport every non-admin symfonicat_* table to deterministic YAML.
symfonicat:admin:createCreate or update an administrator, password hash, TOTP secret, and provisioning QR.
symfonicat:core:deleteDelete an administrator; symfonicat:core:remove is an alias.
symfonicat:application:buildGenerate Electron skeletons for every configured Application; symfonicat:electron:build is an alias.
symfonicat:data:webpackEmit active parcel and module entry metadata for Encore.
symfonicat:data:dnsOutput DNS-related runtime data.
symfonicat:discover:{ext,go}:{paths,names}Inspect native directories across root, core, and opted-in vendor packages.
symfonicat:purgeDrop every symfonicat_* table across PostgreSQL, MySQL, or SQLite.
symfonicat:test:mercurePublish a test update through the configured Mercure hub.
symfonicat:test:redisExercise the configured Redis connection.
# AWS deployment
The repository includes an end-to-end container path rather than generic cloud notes.
Builds the runtime target and pushes a tag to private ECR.
Creates or updates a Fargate cluster, task definition, service, networking, execution role, and optional load-balancer target.
Creates/reuses a hosted zone and publishes apex plus wildcard A records to the running task IP.
Requests an exportable ACM apex + wildcard certificate, publishes DNS validation, exports base64 TLS material, then refreshes ECS and DNS.
Defaults are 512 CPU units, 1024 MiB, one public Fargate task on port 443, with default-VPC discovery and an auto-created security group. The entrypoint converts exported certificate environment variables into Caddy-readable files.
# Complete core/src atlas
All 129 files are indexed below. Each group corresponds to an architectural responsibility; every filename links directly to its implementation.
Commands15 files
Command/AdminCreateCommand.php↗Command/AdminDeleteCommand.php↗Command/AdminYamlDumpCommand.php↗Command/AdminYamlLoadCommand.php↗Command/ApplicationBuildCommand.php↗Command/DiscoverExtNamesCommand.php↗Command/DiscoverExtPathsCommand.php↗Command/DiscoverGoNamesCommand.php↗Command/DiscoverGoPathsCommand.php↗Command/DnsDataCommand.php↗Command/MercureTestCommand.php↗Command/PurgeCommand.php↗Command/RedisDebugCommand.php↗Command/SchemaUpdateCommand.php↗Command/WebpackModulesDataCommand.php↗Contracts & dependency injection5 files
Controllers16 files
Controller/AbstractModuleController.php↗Controller/Core/ApplicationController.php↗Controller/Core/AuthController.php↗Controller/Core/DashboardController.php↗Controller/Core/DomainController.php↗Controller/Core/EndpointController.php↗Controller/Core/EnvController.php↗Controller/Core/FileUploadController.php↗Controller/Core/MiddlewareController.php↗Controller/Core/ParcelController.php↗Controller/Core/SchemaController.php↗Controller/Core/SubdomainController.php↗Controller/Core/YamlController.php↗Controller/HealthController.php↗Controller/RuntimeController.php↗Controller/TestController.php↗Entities16 files
Entity/Admin.php↗Entity/Application.php↗Entity/ApplicationEnv.php↗Entity/Domain.php↗Entity/DomainEnv.php↗Entity/Endpoint.php↗Entity/EndpointEnv.php↗Entity/Env.php↗Entity/EnvParent.php↗Entity/Middleware.php↗Entity/Module.php↗Entity/Parcel.php↗Entity/ParcelEnv.php↗Entity/Subdomain.php↗Entity/SubdomainEnv.php↗Entity/VendorScopedIdTrait.php↗Event subscribers6 files
Forms23 files
Form/AbstractScopedEnvType.php↗Form/ApplicationEnvType.php↗Form/ApplicationType.php↗Form/DomainEnvType.php↗Form/DomainType.php↗Form/EndpointEnvType.php↗Form/EndpointType.php↗Form/EnvParentType.php↗Form/EnvType.php↗Form/EventSubscriber/CoreYamlDumpSubscriber.php↗Form/Extension/CoreYamlDumpTypeExtension.php↗Form/FileUploadItemType.php↗Form/FileUploadType.php↗Form/MiddlewareReferenceType.php↗Form/MiddlewareType.php↗Form/Model/FileUploadData.php↗Form/Model/FileUploadItemData.php↗Form/ParcelChoiceFormTrait.php↗Form/ParcelEnvType.php↗Form/ParcelType.php↗Form/SubdomainEnvType.php↗Form/SubdomainType.php↗Form/VendorScopedIdFormTrait.php↗Middleware4 files
Repositories & security11 files
Repository/AdminRepository.php↗Repository/ApplicationRepository.php↗Repository/DomainRepository.php↗Repository/EndpointRepository.php↗Repository/EnvParentRepository.php↗Repository/EnvRepository.php↗Repository/MiddlewareRepository.php↗Repository/ModuleRepository.php↗Repository/ParcelRepository.php↗Repository/SubdomainRepository.php↗Security/AdminUserProvider.php↗Services20 files
Service/AdminMfaService.php↗Service/AdminYaml.php↗Service/AffixService.php↗Service/ApplicationService.php↗Service/DomainService.php↗Service/EnvService.php↗Service/FileUploadService.php↗Service/MiddlewareClassProvider.php↗Service/ModuleRequestContextStore.php↗Service/ModuleService.php↗Service/NativeDiscoveryService.php↗Service/PackageDiscoveryService.php↗Service/ParcelService.php↗Service/PathService.php↗Service/PublicFileUploadService.php↗Service/RuntimeConfig.php↗Service/RuntimeMiddlewareRunner.php↗Service/RuntimeRenderer.php↗Service/SchemaSynchronizer.php↗Service/SubdomainService.php↗Twig extensions10 files
# Testing and the rest of the README
docker exec -it php ./bin/phpunitThe test environment lowers password-hashing cost, uses dedicated cache/Doctrine/routing configuration, and the repository includes unit and integration coverage for runtime, admin, modules, native discovery, configuration, templates, middleware, and commands.
Picture of @dunglas at the zoo
