symfonicat / core
Symfonicat Symfony application with admin, routing, module runtime, Electron tooling, and FrankenPHP starter infrastructure.
Package info
Type:subdomain
pkg:composer/symfonicat/core
Requires
- php: >=8.4
- ext-apcu: *
- ext-bcmath: *
- ext-ctype: *
- ext-curl: *
- ext-dom: *
- ext-exif: *
- ext-fileinfo: *
- ext-gd: *
- ext-iconv: *
- ext-igbinary: *
- ext-imagick: *
- ext-intl: *
- ext-json: *
- ext-mbstring: *
- ext-msgpack: *
- ext-pcntl: *
- ext-pdo: *
- ext-pdo_pgsql: *
- ext-posix: *
- ext-redis: *
- ext-simplexml: *
- ext-sockets: *
- ext-tidy: *
- ext-xml: *
- ext-xmlreader: *
- ext-xmlwriter: *
- ext-zend-opcache: *
- ext-zip: *
- ext-zlib: *
- bacon/bacon-qr-code: ^3.1.1
- doctrine/doctrine-bundle: ^3.2.3
- doctrine/orm: ^3.6.7
- intervention/image: ^4.1.2
- nikic/php-parser: ^5.7
- nyholm/psr7: ^1.8.2
- psr/http-server-handler: ^1.0.2
- psr/http-server-middleware: ^1.0.2
- scheb/2fa-bundle: ^8.5
- scheb/2fa-totp: ^8.5
- symfonicat/analytics: ^3.6.0
- symfony/asset: 8.0.*
- symfony/console: 8.0.*
- symfony/dotenv: 8.0.*
- symfony/expression-language: 8.0.*
- symfony/flex: ^2.11
- symfony/form: 8.0.*
- symfony/framework-bundle: 8.0.*
- symfony/http-client: 8.0.*
- symfony/lock: 8.0.*
- symfony/mercure-bundle: ^0.4.2
- symfony/messenger: 8.0.*
- symfony/polyfill-intl-idn: ^1.38.1
- symfony/process: 8.0.*
- symfony/property-access: 8.0.*
- symfony/property-info: 8.0.*
- symfony/psr-http-message-bridge: 8.0.*
- symfony/rate-limiter: 8.0.*
- symfony/redis-messenger: 8.0.*
- symfony/runtime: 8.0.*
- symfony/security-bundle: 8.0.*
- symfony/serializer: 8.0.*
- symfony/string: 8.0.*
- symfony/twig-bundle: 8.0.*
- symfony/ux-turbo: ^2.36
- symfony/validator: 8.0.*
- symfony/webpack-encore-bundle: ^2.4
- symfony/yaml: 8.0.*
- twig/extra-bundle: ^3.24.0
- twig/twig: ^3.27.1
Requires (Dev)
- phpunit/phpunit: ^12.5.29
- symfony/browser-kit: 8.0.*
- symfony/css-selector: 8.0.*
Conflicts
This package is auto-updated.
Last update: 2026-08-25 01:10:46 UTC
README
Symfonicat is a Symfony 8 multi-tenant frontend runtime. It resolves public requests to domains, subdomains, and endpoints, renders the matching parcel-backed template, and exposes modules, middleware, env data, and build-application context where present.
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.lock # enables /core
docker compose up -d --build builds both php and messenger-worker from Dockerfile, so the worker uses the local symfonicat-core-php:local image instead of trying to pull it.
postgres and redis stay on the Compose network only; the stack no longer publishes host ports 5432 or 6379 by default.
php, npm, and messenger-worker mount a named vendor volume at /symfonicat/vendor; the entrypoint seeds it from /opt/symfonicat-vendor so the Composer install baked into the image stays visible after the source bind mount.
Edit /etc/hosts for local public routing:
127.0.0.1 example.com
127.0.0.1 subdomain1.example.com
The core area is disabled until symfonicat.lock exists in the repo root.
Runtime
The runtime subscriber resolves the active Domain, Subdomain, and matching Endpoint before Symfony routing. Runtime catch-all routes have low priority, so normal Symfony routes still win when they match.
- a matched domain renders the domain shell on any public path for that host
- a matched subdomain renders the subdomain shell on any public path for that host
- endpoints match their repeatable
arguments;*matches one path segment - endpoint
catchallows extra path after the matched arguments; with no arguments configured it acts as a wildcard for the current path /core/*and/m/*are reserved from the public catch-all- public runtime reads from
config/packages/symfonicat.yamlonly; database-backed lookups are reserved for/core/*
Templates resolve in this order:
templates/{domain,subdomain,endpoint}/overrides/{id}.html.twigtemplates/{domain,subdomain,endpoint}/main.html.twig
Ids
Domainids are bare hostnames, for exampleexample.comSubdomainids are internal auto-increment integers; the public label issubdomain.affix, for examplesubdomain1Application,Module,Middleware, andParcelids remain package-scoped where applicable, for examplecore/testEndpointids are string ids and may be package-scoped, for examplecore/test
{{ domain.tld }} {# example.com #}
{{ subdomain.affix }} {# subdomain1 #}
{{ endpoint.id }} {# core/test #}
{{ application.id }} {# example-test #}
Applications
Application is the application-scaffold target in this branch. It replaces the old separate Electron row concept: an application selects a URL context, and that selected target is what the generated Electron skeleton will launch once it is built later.
Build-application requests expose application through Twig and window.application when the request context provides it.
Application build templates:
templates/application/main.js.twigtemplates/application/overrides/{application-id}.js.twig
The build command generates a buildable Electron skeleton in applications/{application.id}/ with main.js, package.json, README.md.
Middleware
Middleware is selected from the active runtime scope:
- domain middleware always runs when a domain is active
- subdomain middleware always runs when a subdomain is active
- endpoint middleware runs for endpoint renders
Middleware services implement PSR-15 Psr\Http\Server\MiddlewareInterface and are tagged automatically as symfonicat.middleware.
Modules
Modules can be attached to domains, subdomains, or endpoints.
Backend module controllers should extend Symfonicat\Controller\AbstractModuleController. They only execute when the module is attached to the active domain, subdomain, or endpoint context. AbstractModuleController exposes json() for JsonResponse and html(). Module routes are declared with #[ModuleRoute] on the controller class and #[Module] on the action method.
Front end module code example:
const mod = 'symfonicat/analytics/main' mod.log('module active!') // posts { test: true } to /m/symfonicat/analytics/main const result = await mod.json({ test: true }) mod.log('/m/symfonicat/analytics/main result:', result)
Env
resolution order:
- parcel
- domain
- subdomain
- endpoint where present
- application
Application values override endpoint values when the request is in application context.
The same grouped structure is exposed through window.env and Twig:
{{ env('colors.primary') }}
Assets
Private webpack data comes from symfonicat:data:webpack. It scans the root package and installed Composer packages from configured vendors under:
assets/module/assets/parcel/
The public symfonicat_asset(path) helper resolves shell-specific assets by checking:
- subdomain
- domain
- default
It can also target an entity directly:
<link rel="icon" href="{{ symfonicat_asset('favicon.svg', domain) }}" /> <link rel="icon" href="{{ symfonicat_asset('favicon.svg', subdomain) }}" />
Configuration
symfonicat_subdomain rows store an internal integer id, a public affix, and an optional domain_id; the UI and runtime use affix, not the internal id. Multiple rows may share the same affix when they belong to different domains.
Packages opt into Symfonicat discovery by setting extra.symfonicat: true in their composer.json:
extra: symfonicat: true
composer install runs symfonicat:purge so deployments start with a clean symfonicat_* schema; public runtime still reads config/packages/symfonicat.yaml, and only /core/* routes use the database-backed CRUD/sync flow.
Sync
symfonicat:schema:update synchronizes the Doctrine schema and Symfonicat package rows:
- parcels
- domains
- subdomains
- endpoints
- modules
- middleware
- applications
It removes stale package-backed parcels, clears affected parcel references, mirrors tagged middleware services into rows, and stores domain/subdomain/endpoint middleware in dedicated join tables.
Native
Symfonicat supports the ability for Composer packages to ship with PHP extensions and Go modules. Relative to the project root, and composer packages, modules/extensions are located here:
PHP extensions:
native/ext/**core/native/ext/**vendor/**/**/native/ext/**
Go modules:
native/go/**core/native/go/**vendor/**/**/native/go/**
Composer packages and shared front ends
Symfonicat and Nymfonicat share Composer-managed hybrid packages. Composer
packages should include Nymfonicat endpoints alongside their Symfonicat
endpoints whenever a feature supports both runtimes. The two endpoints use
the same front-end code, so the package should keep browser modules in
assets/module and parcel assets in assets/parcel at the package root.
The package root's src/ directory contains the Symfony/PHP back end. Its
modules/ directory contains the Node.js back end consumed by Nymfonicat.
This layout lets the Symfony application discover the package through
Composer while the Node application loads the corresponding Nymfonicat
module from the same installed package.
For example, the symfonicat/analytics Composer package is laid out like
this (the exact checkout may contain additional Symfony, native, or config
files):
symfonicat/analytics/
├── composer.json
├── package.json
├── assets/
│ ├── module/
│ │ └── main/
│ │ └── index.js
│ └── parcel/
│ └── analyticsparcel/
│ └── index.js
├── modules/
│ └── main.js
└── src/
└── Module/
└── AnalyticsModule.php
In this example, main is the module of the symfonicat/analytics package:
./modules/main.jsis the Node.js back end for Nymfonicat../assets/module/main/index.jsis the hybrid front end used by both the Nymfonicat and Symfonicat endpoints../src/is the Symfony back end.
Package authors should preserve this shared front-end entry point while implementing the server-specific behavior in the Node.js and Symfony sides.
AWS
Read AWS.md for AWS deployment bin/* Bash scripts that make it easy to set up a full HTTPS-enabled container deployment.
PHPUnit
docker exec -it php ./bin/phpunit
Picture of @dunglas at the zoo
included.