componenta / skeleton
Componenta Framework application skeleton
Requires
- php: ^8.4
- ext-ctype: *
- ext-json: *
- ext-mbstring: *
- componenta/app: ^1.0
- componenta/app-console: ^1.0
- componenta/app-http: ^1.0
- componenta/composer-plugin: ^1.0
- componenta/config: ^1.0
- componenta/error-handler-app: ^1.0
- componenta/http: ^1.0
- componenta/http-body-parsing-middleware: ^1.0
- componenta/http-psr: ^1.0
- componenta/http-psr-nyholm: ^1.0
- componenta/interceptor: ^1.0
- componenta/interceptor-app: ^1.0
- componenta/path-resolver: ^1.0
- componenta/router: ^1.0
- componenta/router-app: ^1.0
- componenta/templater-app: ^1.0
- psr/container: ^2.0
- symfony/console: ^7.4 || ^8.0
Requires (Dev)
- pestphp/pest: ^4.0
- phpstan/phpstan: ^2.1
- symfony/var-dumper: ^7.4 || ^8.0
This package is not auto-updated.
Last update: 2026-06-15 15:08:26 UTC
README
Componenta Skeleton is the starter distribution of Componenta Framework for PHP 8.4+ applications. It provides a ready project with entry points, configuration, container wiring, error handling, class discovery, and HTTP, API, CLI, and WebSocket presets.
The skeleton shows how the framework assembles an application from Componenta packages: Composer discovers package providers, configuration merges them with project files, the container builds services, and Runner starts the selected execution scope: HTTP, CLI, or WebSocket.
Installation
composer create-project componenta/skeleton my-app
During composer create-project, Installer::install() runs and a follow-up Composer update installs the packages selected by the preset. In interactive mode the installer asks questions; in non-interactive mode it uses defaults. After the preset is selected, it adds the matching Composer requirements, writes entry points, and removes installer-only files.
Details: componenta/composer-plugin describes Composer provider discovery, and componenta/app describes the application runtime.
Presets
| Preset | Created files and behavior |
|---|---|
| Web | HTTP application with public/index.php, routing, templates, config/routes.php, config/pipeline.php, and composer serve. |
| Full | Web application with default selectable packages plus CQRS, policies, authentication, Cycle ORM, and an optional WebSocket server. |
| API | HTTP application with routing and a JSON welcome response, by default without template files. |
| CLI | Console application without HTTP, WebSocket, routing, or public entry point. |
| WebSocket | WebSocket application with bin/websocket.php and WebSocket configuration, without HTTP public entry point. |
In interactive mode, selection prompts use numbered choices: enter 0 for the first option, 1 for the second option, and so on. HTTP presets ask for a PSR-7 implementation: Nyholm, Diactoros, Guzzle, or Slim. HTTP presets also ask for a template renderer: Plates by default for Web, no renderer by default for API. The test runner is selected during installation: Pest by default, or PHPUnit. The Full preset uses the default selectable packages: Nyholm PSR-7, Plates, and Pest; it still asks whether to add the WebSocket server.
In non-interactive mode, the installer uses the Web preset with Nyholm PSR-7, templates, Pest, CQRS, and policies. Authentication, Cycle ORM, and the WebSocket add-on are disabled by default.
Details: componenta/http-psr describes HTTP factories, componenta/http-psr-nyholm documents one PSR-7 integration, and componenta/templater-app describes template integration.
Installer Options
After the preset is selected, the installer configures project requirements and files:
- HTTP presets create
public/index.php,config/routes.php,config/pipeline.php,src/Welcome.php, and the safe error templatetemplates/error/500.phtml; - HTTP presets create
templates/welcome.phtmland installcomponenta/templater-appwhen templates are selected; - the CLI preset does not create HTTP or WebSocket infrastructure;
- the WebSocket preset creates
bin/websocket.php,config/websocket.php, and the starter applicationsrc/WebSocket/WelcomeApplication.php; - CQRS, policies, authentication, Cycle ORM, and the WebSocket add-on can be enabled or disabled interactively; the Full preset enables CQRS, policies, authentication, and Cycle ORM automatically;
- when authentication is enabled, CQRS and policies are forced on;
- CLI commands are executed through
php bin/console.php.
Details: componenta/cqrs-app describes command and query integration, componenta/policy-app describes policy integration, componenta/auth describes authentication, and componenta/cycle-app describes Cycle ORM.
Application Lifecycle
- The outer entry point (
public/index.php,bin/console.php, orbin/websocket.php) loads Composer autoload, creates aPathResolver, and invokes the rootindex.php. - The root
index.phpreceives aScope:Scope::HTTP,Scope::CLI, orScope::WEBSOCKET. config/container.phpcallsConfigFactory::create()andContainerFactory::create().config/config.phpreturns aConfigDefinition: package config providers and discovery directories.- The container is built from configuration, discovered classes, and services contributed by packages.
Runner::run()selects the adapter for the current scope and starts the application.- Bootloaders prepare the selected scope by registering commands, routes, handlers, listeners, templates, or WebSocket applications.
The order is the same for every preset. Only the execution scope and installed integration packages change.
Details: componenta/app describes Scope, Runner, adapters, and bootloaders; componenta/app-http describes HTTP scope; componenta/app-console describes CLI scope; componenta/websocket-app describes WebSocket scope.
Configuration
The main configuration file is config/config.php. It loads package providers, attribute providers, console command configuration, and project autoload configuration:
return new ConfigDefinition( providers: [ new ComposerPackageConfigProvider($paths->resolve('config/componenta-providers.php')), new AttributeConfigProvider(), new FileProvider($paths->resolve('config/console.php')), new FileProvider($paths->resolve('config/autoload/{{,*.}global,{,*.}local}.{php,yaml,json}')), ], discovery: new DiscoveryDefinition( directories: ['src'], ), );
ComposerPackageConfigProvider loads providers generated from installed packages. AttributeConfigProvider loads configuration from attributes discovered in project code. config/console.php registers project console commands in the same config graph. The autoload FileProvider loads project files from config/autoload.
*.global.* files are shared project configuration. *.local.* files are local environment configuration and are normally not committed. The installer creates config/autoload/app.local.php from the packaged local template and removes app.local.php.dist from the installed project.
Details: componenta/config describes config providers and file loading, and componenta/app describes ConfigFactory, environment handling, and cache layout.
Config Providers And #[AsConfig]
A config provider is a callable class that returns a configuration array. Packages use providers to register factories, aliases, autowiring, bootloaders, middleware, compiler contributors, and their own config keys.
Package providers are wired by componenta/composer-plugin: each package declares provider classes in extra.componenta.config-providers, the plugin collects them into config/componenta-providers.php, and ComposerPackageConfigProvider loads that file.
Project config can live in src/ and be marked with #[AsConfig]. A minimal project provider looks like this:
namespace App; use App\Boot\WarmupBootloader; use Componenta\App\Config\AsConfig; use Componenta\App\ConfigKey; #[AsConfig] final class ConfigProvider extends \Componenta\Config\ConfigProvider { protected function getConfig(): array { return [ ConfigKey::BOOTLOADERS => [ WarmupBootloader::class, ], ]; } }
AttributeConfigProvider finds classes marked with #[AsConfig] in discovery directories, creates the class without constructor arguments, invokes it, and merges the returned array into application config. The provider must return an array or iterable. Use config/autoload/*.local.php for machine-local settings; use #[AsConfig] for package or application-module configuration.
#[AsConfig] can target classes, functions, or methods, but the current AttributeConfigProvider scans discovered classes and invokes providers placed on classes. In the skeleton, the supported primary pattern is a class with __invoke() under src/.
During installation, src/ConfigProvider.php is generated for the selected preset. HTTP presets register InterceptorConfigKey::HTTP_INTERCEPTORS with AttributeInterceptor::class. CQRS presets register CqrsConfigKey::COMMAND_MIDDLEWARES and CqrsConfigKey::QUERY_MIDDLEWARES; when policies are selected, policy middleware is included in those chains. CLI and WebSocket-only presets keep this provider minimal and do not create HTTP or CQRS configuration.
Details: componenta/config describes the base ConfigProvider, and componenta/app describes AttributeConfigProvider and discovery.
Package Discovery
Componenta packages declare config providers in composer.json under extra.componenta.config-providers. componenta/composer-plugin reads this metadata after composer install, composer update, and composer dump-autoload, then writes config/componenta-providers.php.
The generated file returns an array of provider classes and must not be edited manually. The installer does not write it directly: the file appears or changes when the Composer plugin runs. Before the plugin runs for the first time, the file may be missing; ComposerPackageConfigProvider then returns an empty configuration. When a package is removed from Composer, its provider disappears from the generated file on the next Composer event.
Details: componenta/composer-plugin describes metadata format, Composer events, and atomic provider file writes.
Class Discovery
DiscoveryDefinition tells the framework which directories to scan in development mode. The skeleton scans src. Discovered classes are passed to packages that understand attributes:
componenta/app-consolediscovers console commands marked with#[AsCommand];componenta/router-appdiscovers HTTP routes;componenta/cqrs-appdiscovers command and query handlers;componenta/policy-appprepares policy maps;componenta/interceptor-appprepares interceptor maps.
In development mode, discovery results and derived maps are cached in var/cache/dev. In production-like modes, the application should read prepared files from var/cache/build.
Details: componenta/class-finder describes class discovery; componenta/router-app, componenta/cqrs-app, componenta/policy-app, and componenta/interceptor-app describe their discovery maps.
Development And Build Modes
By default .env.dist contains:
APP_ENV=development APP_DEBUG=true
With APP_ENV=development, the application assembles configuration from providers, files, and attributes, scans configured directories, and uses development caches to make repeated starts faster.
When APP_ENV is not development, ConfigFactory assumes the application runs from build cache and reads var/cache/build/config.cache.php. In that mode, the project config definition is not rebuilt on every request. The build cache must exist before the application is started in that environment; otherwise startup fails with a configuration error.
With APP_ENV=production, the container also tries to use prepared var/cache/build/container.cache.php and var/cache/build/container.factory.php files when they exist.
APP_DEBUG controls whether detailed error information is shown to users by runtime HTTP error handling. The generated HTTP entry point also catches failures that happen before the container starts, writes them with error_log(), returns status 500, and renders templates/error/500.phtml. That bootstrap-safe page is used regardless of APP_DEBUG, because the normal error renderer may not be available yet.
Details: componenta/app describes ConfigFactory, CacheLayout, and compile support; componenta/error-handler-app describes HTTP error handling and safe rendering.
Container
config/container.php is the application composition point. It loads configuration and returns a PSR-11 container:
$result = ConfigFactory::create( paths: $paths, definition: static fn () => require $paths->resolve('config/config.php'), ); return ContainerFactory::create($paths, $result->config, $result->discovered);
ContainerFactory adds PathResolverInterface, discovered classes, and services declared by providers. The project can extend the container through config/autoload/*.php files or through App\ConfigProvider marked with #[AsConfig].
Details: componenta/di describes the DI container, factories, attributes, and property resolvers; componenta/config describes config array shape.
Application Config
The final application configuration is represented by Componenta\Config\Config. ConfigFactory::create() creates it, and ContainerFactory stores the same object in the container under Config::class and the 'config' alias.
use Componenta\Config\Config; use Componenta\Config\ConfigPath; /** @var \Psr\Container\ContainerInterface $container */ $config = $container->get(Config::class); $name = $config->string(new ConfigPath('app.name'), 'Componenta App'); $debug = $config->bool(new ConfigPath('app.debug'), false);
Services can receive the full config through constructor injection:
namespace App\Service; use Componenta\Config\Config; use Componenta\Config\ConfigPath; final readonly class FeatureFlags { public function __construct( private Config $config, ) {} public function enabled(string $name): bool { return $this->config->bool(new ConfigPath("features.$name"), false); } }
If a service needs one value, use the DI #[Config] attribute. A string key is read literally, while ConfigPath enables dot-notation traversal of nested arrays:
namespace App\Service; use Componenta\Config\ConfigPath; use Componenta\DI\Attribute\Config; final readonly class MailerOptions { public function __construct( #[Config(new ConfigPath('mail.from'))] public string $from, #[Config(new ConfigPath('mail.retries'), default: 3)] public int $retries, ) {} }
Main Config methods:
| Method | Purpose |
|---|---|
get(string|ConfigPath $key, mixed $default = DefaultValue::None) |
Returns a raw value. Without a default, throws when the key is missing. |
has(string|ConfigPath $key) |
Checks whether a key exists. |
string(), int(), float(), bool(), array() |
Return a value with type conversion. |
only(string|ConfigPath|array $keys) |
Returns a new Config containing only selected keys. |
except(string|ConfigPath|array $keys) |
Returns a new Config without selected keys. |
toArray() |
Returns the full config array. |
get('database.host') looks for the literal $config['database.host'] key. Nested access requires new ConfigPath('database.host'), which reads $config['database']['host'].
Config also exposes the environment property. Use it to read variables loaded from .env or the process environment:
$env = $config->environment; $isProduction = $env?->match('APP_ENV', 'production') ?? false; $timezone = $env?->string('APP_TIMEZONE', 'UTC') ?? 'UTC';
Config files and providers usually return arrays instead of reading Config. Reading the assembled Config belongs in services, factories, and bootloaders after all providers have been merged.
Details: componenta/config describes Config, ConfigPath, Environment, file loading, and merge rules; componenta/di describes the #[Config] attribute.
Application Bootloaders
An application bootloader runs startup work before the current scope starts: HTTP, CLI, or WebSocket. Use bootloaders for work that needs the built container and the prepared application target: wiring the HTTP pipeline, registering console commands, restoring discovery maps, assigning a WebSocket application, or running application warmup.
Runner creates BootContext, BootloaderProvider reads class names from ConfigKey::BOOTLOADERS, filters them by Scope, resolves matching bootloaders from the container, and calls boot():
use Componenta\App\ConfigKey; return [ ConfigKey::BOOTLOADERS => [ App\Boot\WarmupBootloader::class, ], ];
In the skeleton, this registration lives in src/ConfigProvider.php, which is marked with #[AsConfig]. To add a custom bootloader, add it to getConfig() and register the class as an autowired service:
namespace App; use App\Boot\WarmupBootloader; use App\Service\WarmupService; use Componenta\App\Config\AsConfig; use Componenta\App\ConfigKey; #[AsConfig] final class ConfigProvider extends \Componenta\Config\ConfigProvider { protected function getConfig(): array { return [ ConfigKey::BOOTLOADERS => [ WarmupBootloader::class, ], ]; } protected function getAutowires(): array { return [ WarmupBootloader::class, WarmupService::class, ]; } }
Application bootloaders can extend the base Bootloader class. In that mode __invoke() is called through DI, so method parameters can be resolved from the container:
namespace App\Boot; use App\Service\WarmupService; use Componenta\App\Boot\BootContext; use Componenta\App\Boot\Bootloader; use Componenta\App\Scope; use Componenta\Config\ConfigPath; final class WarmupBootloader extends Bootloader { public static function scopes(): array { return [Scope::HTTP]; } public function supports(BootContext $context): bool { return $context->config->bool(new ConfigPath('warmup.enabled'), false); } public function __invoke(WarmupService $warmup): void { $warmup->run(); } }
If you need full control, implement BootloaderInterface directly. Inside boot(), BootContext::$container, BootContext::$config, BootContext::$scope, and BootContext::target() are available:
namespace App\Boot; use Componenta\App\Boot\BootContext; use Componenta\App\Boot\BootloaderInterface; use Componenta\App\Boot\Target\HttpBootTargetInterface; use Componenta\App\Scope; final class ExtraHttpPipelineBootloader implements BootloaderInterface { public static function scopes(): array { return [Scope::HTTP]; } public function boot(BootContext $context): void { $http = $context->target(HttpBootTargetInterface::class); $http->pipe(\App\Http\Middleware\RequestIdMiddleware::class); } public function supports(BootContext $context): bool { return true; } }
For ordinary global HTTP middleware, prefer config/pipeline.php. A custom HTTP bootloader is useful when registration depends on the container, configuration, or an integration package. For CLI use ConsoleBootTargetInterface; for WebSocket use WebSocketBootTargetInterface.
Details: componenta/app describes BootContext, BootloaderInterface, BootloaderProvider, and boot targets; componenta/app-http, componenta/app-console, and componenta/websocket-app show scope-specific bootloaders.
HTTP And Routing
HTTP presets create public/index.php, config/routes.php, and config/pipeline.php. The public entry point starts Scope::HTTP. The config/pipeline.php file defines the global HTTP pipeline:
$app->pipe(Componenta\Error\Http\Middleware\ErrorHandlerMiddleware::class, priority: 100); $app->pipe(Componenta\Http\Middleware\BodyParsingMiddleware::class, priority: 100);
componenta/router-app adds MatchRouteMiddleware and DispatchRouteMiddleware through RoutingBootloader with priority 50, so the starter error handler and body parser run before route matching. Custom middleware can use the same priority mechanism: higher numbers run earlier.
componenta/router-app uses config/routes.php as the default manual route file. The file receives $routes as a Componenta\Http\Router\Routes instance. Use it for routes that are easier to define programmatically: route groups, shared prefixes, shared middleware, shared tokens and defaults, manual RouteRecord instances, and nested groups.
use App\Http\AdminDashboard; use App\Http\AdminUsers; use App\Http\Middleware\RequireAdminMiddleware; use App\Http\Middleware\RequireAuthenticationMiddleware; /** * @var \Componenta\Http\Router\Routes $routes */ $admin = $routes->group( name: 'admin', prefix: '/admin', middleware: [ RequireAuthenticationMiddleware::class, RequireAdminMiddleware::class, ], tokens: ['id' => '\d+'], ); $admin->get('dashboard', '/', AdminDashboard::class); $admin->get('users.show', '/users/{id}', AdminUsers::class);
The group prefixes route names and paths. In the example, final names are admin.dashboard and admin.users.show, and paths are /admin and /admin/users/{id}. Group settings are inherited by nested groups and routes.
Routes can be added declaratively with #[Route] or manually in config/routes.php. The starter route / lives in src/Welcome.php: when templates are selected it renders templates/welcome.phtml, otherwise it returns JSON:
{"status":"ok","message":"Componenta Framework skeleton is running."}
Details: componenta/router describes the router; componenta/router-app describes route attribute discovery; componenta/app-http describes the HTTP adapter; componenta/http describes base HTTP contracts and exceptions.
HTTP Middleware
Global middleware is registered in config/pipeline.php. HttpBootloader includes this file, and $app implements HttpBootTargetInterface. Each $app->pipe(...) call adds middleware to the application-wide HTTP pipeline. The optional priority argument controls ordering; higher priority middleware runs earlier:
use App\Http\Middleware\RequestIdMiddleware; use Componenta\Error\Http\Middleware\ErrorHandlerMiddleware; use Componenta\Http\Middleware\BodyParsingMiddleware; /** * @var \Componenta\App\Boot\Target\HttpBootTargetInterface $app */ $app->pipe(ErrorHandlerMiddleware::class, priority: 100); $app->pipe(RequestIdMiddleware::class, priority: 100); $app->pipe(BodyParsingMiddleware::class, priority: 100);
Order matters: middleware runs by priority, and definitions with the same priority keep their registration order. Error handling is usually first so it can catch exceptions from later layers. BodyParsingMiddleware must run before handlers that use #[MapRequestPayload], because it fills the parsed body on the PSR-7 request.
The base HTTP preset installs ErrorHandlerMiddleware and BodyParsingMiddleware. Additional framework packages provide ready middleware: CorsMiddleware, CsrfMiddleware, ThrottleMiddleware, and TrustedProxyMiddleware. They can be placed in the global pipeline or on specific route groups and routes when the package is installed and its provider is loaded by the Composer plugin. App\Http\Middleware\... classes in the examples below are application PSR-15 middleware classes that you create in the project.
Group middleware is registered on a route group in config/routes.php. It applies to every route in the group and is inherited by nested groups:
use App\Http\AdminDashboard; use App\Http\AdminUsers; use App\Http\Middleware\RequireAdminMiddleware; use App\Http\Middleware\RequireAuthenticationMiddleware; /** * @var \Componenta\Http\Router\Routes $routes */ $admin = $routes->group( name: 'admin', prefix: '/admin', middleware: [ RequireAuthenticationMiddleware::class, RequireAdminMiddleware::class, ], ); $admin->get('dashboard', '/', AdminDashboard::class); $admin->get('users.show', '/users/{id}', AdminUsers::class);
Route-specific middleware is registered with the middlewares argument on #[Route] or manually with RouteRecord. Route middleware is appended after group middleware:
namespace App\Http; use App\Http\Middleware\AuditPostAccessMiddleware; use Componenta\Http\Router\Attribute\Route; final class PostController { #[Route( name: 'posts.show', path: '/posts/{id}', methods: 'GET', middlewares: [AuditPostAccessMiddleware::class], tokens: ['id' => '\d+'], group: 'api', )] public function show(): array { return ['status' => 'ok']; } }
use App\Http\PostController; use App\Http\Middleware\AuditPostAccessMiddleware; use Componenta\Http\Router\RouteRecord; /** * @var \Componenta\Http\Router\Routes $routes */ $routes->addRoute(RouteRecord::get( name: 'posts.show', path: '/posts/{id}', handler: [PostController::class, 'show'], middlewares: [AuditPostAccessMiddleware::class], tokens: ['id' => '\d+'], ));
Middleware definitions are resolved by componenta/middleware-factory. The common definition is a container class name. Ready MiddlewareInterface objects, RequestHandlerInterface objects, MiddlewareGroup, and callable middleware are also supported when resolvers can handle them. Plain strings such as 'auth' are not a built-in named middleware registry. If the application wants aliases like that, add a custom resolver or use class names directly.
Details: componenta/app-http describes config/pipeline.php, componenta/middleware-factory describes resolving definitions to PSR-15 middleware, componenta/router describes group and route middleware ordering. Middleware package READMEs document concrete implementations: componenta/http-body-parsing-middleware, componenta/http-cors-middleware, componenta/http-csrf-middleware, componenta/http-throttle-middleware, and componenta/http-trusted-proxy-middleware.
The #[Route] Attribute
#[Route] can be placed on an invokable class or controller method. It defines the route name, path, HTTP methods, middleware, parameter constraints, defaults, group name, and priority.
namespace App\Http; use Componenta\DI\Attribute\RequestAttribute; use Componenta\Http\Router\Attribute\Route; final class PostController { #[Route( name: 'posts.show', path: '/posts/{id:\\d+}', methods: 'GET', group: 'api', priority: 20, )] public function show(#[RequestAttribute] int $id): array { return ['id' => $id]; } }
methods accepts a string ('GET'), a pipe-separated string ('GET|POST'), or an array (['GET', 'POST']). middlewares accepts a string or array. tokens defines regex constraints for path parameters, and defaults defines default values.
Parameter constraints can also be written inline in the path. For example, /posts/{id:\d+} and /archive/[?year:\d+=2026] define the route token directly in the template. Explicit tokens override inline constraints when both are present.
priority controls attribute-route registration order: higher values are registered first and therefore matched first when route patterns overlap. This matters for conflicts such as /{slug} and /archive.
When #[Route] references a group, that group must be explicitly registered in config/routes.php before attribute routes are finalized:
/** * @var \Componenta\Http\Router\Routes $routes */ use App\Http\Middleware\RequireAuthenticationMiddleware; $routes->group('api', '/api'); $routes->group('admin', '/admin', middleware: [RequireAuthenticationMiddleware::class]);
If the group is not registered, the route does not receive the group's prefix, middleware, tokens, or defaults: it is added as a normal route with the group name preserved in the record. Groups referenced by route attributes should therefore be declared explicitly in config/routes.php.
Details: componenta/router describes RouteRecord, Routes, and RouteGroup, and componenta/router-app describes AttributeRouteLocator.
HTTP Request Mapping
#[Route] only matches the URL to a handler. Path parameters such as {id} are stored as PSR-7 request attributes, but they are not automatically injected into method arguments by name. Handler parameters must use request-mapping attributes from componenta/di.
For single values, use single-value attributes:
namespace App\Http; use Componenta\DI\Attribute\PayloadParam; use Componenta\DI\Attribute\QueryParam; use Componenta\DI\Attribute\RequestAttribute; use Componenta\Http\Router\Attribute\Route; final class PostController { #[Route('posts.show', '/posts/{id}', 'GET', tokens: ['id' => '\d+'])] public function show( #[RequestAttribute] int $id, #[QueryParam(default: false, cast: 'bool')] bool $preview, ): array { return ['id' => $id, 'preview' => $preview]; } #[Route('posts.rename', '/posts/{id}/rename', 'POST', tokens: ['id' => '\d+'])] public function rename( #[RequestAttribute] int $id, #[PayloadParam] string $title, ): array { return ['id' => $id, 'title' => $title]; } }
Main single-value attributes:
| Attribute | Source |
|---|---|
#[RequestAttribute] |
PSR-7 request attributes. Route parameters are stored here. |
#[QueryParam] |
Query string, for example ?page=2. |
#[PayloadParam] |
Parsed request body. JSON and non-native form parsing require BodyParsingMiddleware; the HTTP preset includes it. |
#[Header] |
HTTP header. |
#[Cookie] |
Cookie. |
#[UploadedFile] |
Uploaded file from $request->getUploadedFiles(). |
When no name is passed, RequestAttribute, QueryParam, and PayloadParam use the method parameter name. Therefore #[PayloadParam] string $title reads the title field, and #[RequestAttribute] int $id reads the id request attribute. Pass an explicit name only when the HTTP field differs from the argument name: #[PayloadParam('post_title')] string $title. Query string and payload values often need cast, because raw request data arrives as strings. Route parameters are already converted by the router to int or float when the matched value looks numeric.
For DTOs or arrays, use Map* attributes. They extract a data array, apply map, cast, defaults, sortMap, and exclude, validate the DTO when a validator is available, and build the object through the container:
namespace App\Http; use Componenta\DI\Attribute\MapQueryString; use Componenta\DI\Attribute\MapRequestPayload; use Componenta\Http\Router\Attribute\Route; final readonly class PostListQuery { public function __construct( public int $page = 1, public ?string $tag = null, ) {} } final class MapPostListQuery extends MapQueryString { protected array $cast = ['page' => 'int']; protected array $defaults = ['page' => 1]; } final readonly class CreatePostCommand { public function __construct( public string $title, public string $body, ) {} } final class PostController { #[Route('posts.index', '/posts', 'GET')] public function index(#[MapPostListQuery] PostListQuery $query): array { return ['page' => $query->page, 'tag' => $query->tag]; } #[Route('posts.create', '/posts', 'POST')] public function create(#[MapRequestPayload] CreatePostCommand $command): array { return ['title' => $command->title]; } }
Details: componenta/di describes request mapping, Map* attributes, casters, and DTO validation; componenta/router-app describes how a route handler is executed through the DI interceptor.
Interceptors
Interceptors wrap any PHP callable: a controller, handler, service method, or function. Use them for cross-cutting behavior that should not be duplicated in business code: logging, metrics, transactions, authorization, caching, parameter normalization, result serialization, or exception handling.
The base componenta/interceptor package contains the runtime layer:
| Component | Purpose |
|---|---|
InterceptorInterface |
Contract for one interceptor. Receives CallableContextInterface and ContextHandlerInterface. |
InterceptingExecutor |
Callable executor with an interceptor chain. Implements CallableExecutorInterface and PipelineInterface. |
AttributeInterceptor |
Reads #[Intercept] attributes from a callable and adds declared interceptors to the chain. |
ParameterResolvingInterceptor |
Resolves callable parameters through DI before later interceptors run. |
CallbackInterceptorFactory |
Creates closure-based interceptors: before(), after(), catch(), finally(), around(). |
#[Intercept] |
Function or method attribute that attaches an interceptor class with constructor parameters. |
ScopedInterface and Scope |
Restrict an interceptor to HTTP, CONSOLE, GRPC, QUEUE, or WEBSOCKET execution. |
Example attribute interceptor:
use Componenta\DI\Attribute\RequestAttribute; use Componenta\Interceptor\Attribute\Intercept; final class UserController { #[Intercept(LogCallInterceptor::class, ['channel' => 'http'])] #[Intercept(SerializeResultInterceptor::class)] public function show(#[RequestAttribute] int $id): User { // ... } }
Attributes execute as layers from outside to inside: the top #[Intercept] is the outer layer, and the bottom one is closest to the callable body. An interceptor can call $handler->handle($context), modify the context or result, catch an exception, or short-circuit the chain by returning without invoking the original callable.
componenta/interceptor registers PipelineInterface for HTTP handler execution. Its factory always starts with ParameterResolvingInterceptor, then appends interceptors listed in InterceptorConfigKey::HTTP_INTERCEPTORS. HTTP presets generate src/ConfigProvider.php with AttributeInterceptor::class in that list, so controller parameters are resolved through DI first, then #[Intercept] attributes are applied.
componenta/interceptor-app does not execute interceptors. It compiles interceptor attributes into a map for build cache so production-like runs do not need to read attributes through reflection on every request.
Details: componenta/interceptor describes interceptor execution, componenta/interceptor-app describes build-cache integration, and componenta/router-app describes HTTP handler integration with interceptors.
Console Commands
The CLI preset and other presets with componenta/app-console use bin/console.php. Commands are collected in the shared Componenta\App\Console\ConfigKey::COMMANDS config key. Packages add their commands from config providers; the application adds project-local commands in config/console.php:
use App\Console\ImportPostsCommand; use Componenta\App\Console\ConfigKey as ConsoleConfigKey; return [ ConsoleConfigKey::COMMANDS => [ ImportPostsCommand::class, ], ];
In development mode, commands inside discovery directories can also be marked with Symfony #[AsCommand]. Attribute discovery is a development convenience; production builds use the assembled console.commands config as the command source.
Standard Symfony Console commands are also available, for example:
php bin/console.php list php bin/console.php app:build php bin/console.php app:preload php bin/console.php app:cache:clear php bin/console.php app:cache:clear --build php bin/console.php app:cache:clear --dev php bin/console.php app:cache:clear --runtime
Run app:build before starting an environment where APP_ENV is not development; those environments read var/cache/build/config.cache.php instead of rebuilding the project config definition on every request. app:preload generates the preload file from build-cache artifacts. app:cache:clear clears build, development, and runtime caches by default; --build, --dev, and --runtime limit the command to one cache area.
When Cycle ORM is installed, componenta/cycle-app adds db:create, db:generate, db:schema, db:migrate, db:rollback, db:status, and db:sync. When routing is installed, componenta/router-app adds router:list.
Details: componenta/app-console describes the command registry, CLI bootloader, command discovery, and maintenance commands. componenta/cycle-app documents database commands. componenta/router-app documents router:list.
Application Commands And Queries
When componenta/cqrs-app is installed, application behavior can be modeled as commands and queries. A command changes state; a query reads data. Their handlers are registered by CQRS packages and can be discovered automatically.
HTTP controllers, console commands, and other entry points should not know the execution details of a business action. They create a command or query and pass it to the matching bus.
The selected application middleware chains live in src/ConfigProvider.php. With CQRS enabled, the skeleton registers command middleware in this order:
use Componenta\CQRS\Command\Middleware\EventMiddleware; use Componenta\CQRS\Command\Middleware\PolicyMiddleware as CommandPolicyMiddleware; use Componenta\CQRS\Command\Middleware\TransactionMiddleware; use Componenta\CQRS\Command\Middleware\TransportMiddleware; use Componenta\CQRS\ConfigKey as CqrsConfigKey; use Componenta\CQRS\Query\Middleware\PolicyMiddleware as QueryPolicyMiddleware; return [ CqrsConfigKey::COMMAND_MIDDLEWARES => [ CommandPolicyMiddleware::class, TransportMiddleware::class, TransactionMiddleware::class, EventMiddleware::class, ], CqrsConfigKey::QUERY_MIDDLEWARES => [ QueryPolicyMiddleware::class, ], ];
If policies are not selected, the policy middleware entries are omitted and query middleware defaults to an empty list. The base componenta/cqrs provider intentionally starts with empty command and query chains; the application decides which runtime guarantees it needs.
Details: componenta/cqrs describes the command bus, query bus, operations, middleware, and async execution; componenta/cqrs-app describes CQRS discovery and compile-cache integration.
Policies
When componenta/policy-app is installed, access checks are described as policies and policy attributes on application actions. This keeps authorization outside command and query handlers.
A typical flow is: an entry point creates a command or query, CQRS middleware resolves the current actor, componenta/policy checks the policy for that action, and execution continues to the handler only when access is allowed.
Details: componenta/policy describes policies, providers, and attributes; componenta/policy-app describes compile integration; componenta/cqrs describes policy middleware placement.
Templates
When a template renderer is selected during installation, the HTTP preset installs componenta/templater-app. The project gets a templates/ directory, the view() helper, and templates/welcome.phtml.
HTTP error templates live in templates/error/. The safe 500 page must be available even when detailed error output is disabled.
Details: componenta/templater describes renderer contracts, and componenta/templater-app describes the view() helper and application integration.
WebSocket
The WebSocket preset creates the separate entry point bin/websocket.php and starts Scope::WEBSOCKET. HTTP infrastructure is not created for this preset. When WebSocket support is added as an extra feature to another preset, the installer adds the serve:websocket Composer script.
Details: componenta/websocket-server describes the base socket server, and componenta/websocket-app describes WebSocket scope integration.
Available Commands And Composer Scripts
Scripts depend on the selected preset:
| Command | Available when | Purpose |
|---|---|---|
composer serve |
HTTP presets | Starts PHP built-in web server on localhost:8000 with public/ as document root. |
composer serve:websocket |
WebSocket preset or WebSocket add-on | Starts bin/websocket.php. |
composer test |
Always | Runs the selected test runner: Pest or PHPUnit. |
composer analyse |
Always | Runs PHPStan over directories created by the selected preset. |
php bin/console.php list |
componenta/app-console is installed |
Lists available console commands. |
php bin/console.php app:build |
componenta/app-console is installed |
Builds config and container cache files for non-development environments. |
php bin/console.php app:preload |
componenta/app-console is installed |
Generates a preload file from build-cache artifacts. |
php bin/console.php app:cache:clear [--build|--dev|--runtime] |
componenta/app-console is installed |
Clears all application cache directories, or only the selected cache area. |
php bin/console.php router:list |
HTTP routing is installed | Lists registered routes. |
The CLI preset does not create public/, config/routes.php, config/pipeline.php, or WebSocket files. HTTP presets create config/routes.php and enable routing through installed packages.
Details: componenta/app-console describes CLI support, and componenta/router-app describes HTTP route discovery.
Project Layout
| Path | Purpose |
|---|---|
.env |
Local environment file created from .env.dist. |
config/config.php |
Main declaration of providers and discovery. |
config/container.php |
Application container assembly. |
config/componenta-providers.php |
Generated provider list for installed packages. Created by componenta/composer-plugin. |
config/autoload/ |
Project configuration from *.global.* and *.local.* files. |
src/ |
Application code under the App\ namespace. |
bin/ |
CLI entry point. |
public/ |
HTTP entry point. Created only for HTTP presets. |
templates/ |
Application and error templates. Created when templates or an HTTP safe error page are needed. |
var/cache/dev/ |
Development caches. |
var/cache/build/ |
Build cache for environments outside development. |
var/cache/runtime/ |
Runtime caches. |
log/ |
Application logs. |
storage/ |
Application files. |
Details: componenta/path-resolver describes project-root path resolution, and componenta/app describes cache layout.
Related Packages
componenta/app- lifecycle, scopes, configuration, container, caches, and bootloaders.componenta/app-http- HTTP application adapter.componenta/app-console- console runtime and commands.componenta/composer-plugin- generation ofconfig/componenta-providers.php.componenta/config- config providers and file loaders.componenta/di- container, factories, and DI attributes.componenta/routerandcomponenta/router-app- routing and route discovery.componenta/cqrsandcomponenta/cqrs-app- commands, queries, and their discovery.componenta/policyandcomponenta/policy-app- access policies and compile integration.componenta/templaterandcomponenta/templater-app- templates and theview()helper.componenta/error-handlerandcomponenta/error-handler-app- error handling and HTTP-safe rendering.componenta/websocket-serverandcomponenta/websocket-app- WebSocket server and application integration.