Skip to content

Lifecycle ​

Application separates bootstrap, done once in the constructor, from handling, done for every request:

new Application($config, $container = null)
      │
      ├── 1. Create / accept container
      ├── 2. Register core services (Config, container, RouteDispatcher,
      │                              RoutingMiddleware, RoutingErrorMiddleware)
      ├── 3. Run providers          ← your services
      └── 4. Build the router       ← routes.php, or load it from routesCachePath,
                                      and register it as Solo\Router\Router

$app->run($request?)  =  emit($app->handle($request))
      │
      ▼
ServerRequest (provided or resolved from the container)
      │
      ▼
MiddlewarePipeline ── [mw 1] → [mw 2] → … → [RoutingMiddleware] ── no route: RoutingException
                                                     │
                                                     ▼
                     request attributes: handler, params, route_name, route_attributes
                                                     │
                                                     ▼
                                     route middleware (groups, then route)
                                                     │
                                                     ▼
                              global middleware listed after RoutingMiddleware
                                                     │
                                                     ▼
RouteDispatcher  →  Controller / invokable / callable
      │
      ▼
ResponseInterface  →  returned by handle(), emitted by run()

Bootstrap ​

The constructor runs four steps in order:

  1. Container. The container you pass, or a new Solo\Container\Container. Without either, the constructor throws RuntimeException.
  2. Core services. Application registers factories for Config, Psr\Container\ContainerInterface, Solo\Contracts\Container\WritableContainerInterface, RouteDispatcher, RoutingMiddleware and RoutingErrorMiddleware. Nothing is created yet.
  3. Providers. Each class in Config::$providers is instantiated without arguments, and its register() method is called with the container, in list order.
  4. Router. Application creates a Solo\Router\RouteCollector, calls the callable returned by routes.php with the collector and the container, and builds the router. With routesCachePath set, it loads the router from the cache file instead and runs routes.php only when the file has to be written. The router is kept as $app->router and registered in the container as Solo\Router\Router.

Providers run before the routes, so routes.php can take services from the container. The router is registered after the providers, so a provider must not resolve it inside register(): take it in a factory that runs later, see service providers.

Every configuration error surfaces here, in the constructor, before any request is handled:

ProblemException
No container passed and solophp/container not installedRuntimeException
A provider without a public register() methodInvalidArgumentException
routesPath does not existRuntimeException
routes.php does not return a callableUnexpectedValueException
Invalid routes, such as a duplicate name or pathInvalidArgumentException from build()
A closure or an object in a route when the route cache is writtenLogicException

Handling a request ​

handle() takes a ServerRequestInterface and returns a ResponseInterface:

  1. It creates a MiddlewarePipeline whose final handler is the RouteDispatcher from the container, and adds Config::$middleware to it.
  2. It passes the request to the pipeline through the RoutingErrorMiddleware from the container, so a routing error that no middleware caught still becomes a response. See routing errors.
  3. Global middleware run in list order. A class name or another container ID is resolved when the request reaches it.
  4. RoutingMiddleware resolves the route, writes the route to the request attributes and runs the route middleware: group middleware from the outermost group in, then the route's own.
  5. Global middleware listed after RoutingMiddleware run next and can read the route.
  6. RouteDispatcher resolves the handler and calls it with the request, a new response and the route parameters.
  7. The response travels back out through every middleware in reverse order and is returned.

run() wraps handle() for a classic PHP-FPM front controller:

php
public function run(?ServerRequestInterface $request = null): void

Called without an argument, it resolves ServerRequestInterface from the container. It then calls handle(), resolves Solo\Contracts\Http\EmitterInterface and emits the response. The emitter is resolved after handling, so a request can still replace it, for example to stream the response. To get the response without emitting it, call handle(): see workers and emitting.

Request attributes ​

RoutingMiddleware writes these request attributes; RouteDispatcher reads handler and params. The names are available as constants on RequestAttribute:

AttributeConstantType
handlerRequestAttribute::HANDLERarray{class-string,string} | string | callable
paramsRequestAttribute::PARAMSarray<string,string>, percent-decoded
route_nameRequestAttribute::ROUTE_NAMEstring | null
route_attributesRequestAttribute::ROUTE_ATTRIBUTESarray<string,mixed>

If handler is missing, for example because no routing middleware is listed, the dispatcher throws RouteNotFoundException.

Container services ​

Application registers these services itself:

Service IDValue
Solo\Application\ConfigThe Config passed to the constructor
Psr\Container\ContainerInterfaceThe container itself
Solo\Contracts\Container\WritableContainerInterfaceThe container itself
Solo\Application\RouteDispatcherDispatcher using your ResponseFactoryInterface
Solo\Application\RoutingMiddlewareRouting middleware holding $app->router
Solo\Application\RoutingErrorMiddlewareError middleware using your ResponseFactoryInterface
Solo\Router\Router$app->router, registered after the providers

Your providers register the rest:

Service IDUsed by
Psr\Http\Message\ResponseFactoryInterfaceRouteDispatcher and RoutingErrorMiddleware, on every handle()
Psr\Http\Message\ServerRequestInterfacerun() called without a request
Solo\Contracts\Http\EmitterInterfacerun()

Middleware, controllers and handlers referenced by class name or ID must resolve from the container as well.

Components ​

ClassDescription
ApplicationBootstraps the application; PSR-15 handler (handle()) and run() to emit
ConfigImmutable configuration DTO
MiddlewarePipelineReusable PSR-15 middleware pipeline, a request handler itself
RoutingMiddlewareMatches the route, runs route middleware, throws a RoutingException when it cannot
RoutingErrorMiddlewareAnswers a RoutingException with an empty response of its status and headers
RouteDispatcherResolves and calls the matched route handler
RequestAttributeNames of the request attributes used for routing
Exception\RoutingExceptionBase of RouteNotFoundException, MethodNotAllowedException and InvalidRouteParameterException

Released under the MIT License.