Skip to content

Latest commit

 

History

History
176 lines (124 loc) · 12.6 KB

File metadata and controls

176 lines (124 loc) · 12.6 KB

The Kernel

The Kernel is the heart of every AppKit application. It acts as the HTTP request handler, the service container, and the boot coordinator. Your application's App class extends it.

The class itself stays small: it owns the state (every property lives on the Kernel), the boot/reset lifecycle, and the core service accessors. Behavior is composed from one trait per concern — AppContainer (service resolution, repositories, parameters), AppControllers (controller wiring and instantiation), AppModules (module lifecycle), AppRouting (router and URL generation), and AppSecurity (authentication flow and firewall configuration). Traits hold no state of their own, and every method kept its name, visibility and signature when it moved — from your App subclass, nothing changed. Read the trait for the concern you care about; the Kernel file tells you the order things happen in.

The profiling seam

The kernel does not profile; it makes profiling possible through one interface and one timer, the way it makes authentication extensible through AuthenticatorInterface:

  • stopwatch() — a Symfony\Component\Stopwatch\Stopwatch on which the kernel records the phases it owns: security.session, security.authenticate, security.access_control, routing, controller. Application code can start()/stop() its own events around anything it wants on the timeline. Reset by resetModules().
  • profiler() — a Debug\ProfilerInterface whose collect(request, response) is called by PrepareResponse once the response is final, the last thing every handle() does; it returns the response to send, so a profiler can add a header naming the stored profile. The default is NullProfiler. Install one with setProfiler() — from a module's boot(), or in the application factory after boot() — or override the accessor.
  • Query origins. In dev the DebugStack records the application file and line that issued each query (Query::$file / $line), so a repeated-query report can name the loop. A backtrace per query, so never outside dev; DebugStack::collectOrigin() toggles it.
  • Query shapes. Debug\QueryFingerprinter reduces each recorded statement to its shape — bindings and literals masked, IN lists of any length made one — and analyzeStack($debugStack) reports every shape repeated three or more times in the request, most repeated first, with the time it cost. That is the N+1 finder; a profiler calls it from collect(), a test asserts suspects is empty.

What a profiler sees beyond that comes from decorators, not hooks: wrap an authenticator, the validator or the exception handler in config/services.php with a recording decorator in dev, exactly as Symfony's Traceable* classes do. Symfony reaches the same three points through event listeners; here they are explicit calls because the request flow is.

Extending the Kernel

namespace App;

use Modufolio\Appkit\Core\Kernel;

class App extends Kernel
{
    public function handle(ServerRequestInterface $request): ResponseInterface { ... }
    public function reset(): void { ... }
    public function serializer(): SerializerInterface { ... }
    public function parameterResolver(): ParameterResolverInterface { ... }
    public function validator(): ValidatorInterface { ... }
    public function userProvider(): UserProviderInterface { ... }
}

These six abstract methods are your integration points. The framework's test application (tests/App/App.php) provides a complete implementation you can use as a reference.

The request lifecycle

Application code. AppFactory is not part of the framework. The skeleton (modufolio/appkit-skeleton) ships a starting version in src/AppFactory.php — create(string $baseDir): AppInterface, which builds the route loader, loads the config files and boots App — and the framework's test application keeps its own in tests/App/AppFactory.php; it is yours to change.

  1. public/index.php calls your application factory — AppFactory::create($baseDir) in the test app — which instantiates App, loads config files, and calls boot().
  2. boot() applies error-output hardening for the environment (see Exception handling), wires the kernel core services (or loads a legacy config/interfaces.php when mapped), sets up the router cache directory, builds the Symfony container behind the kernel if configureContainer() asked for one (before any module's boot()), and freezes the token unserializer whitelist.
  3. handle(ServerRequestInterface $request) is called. It creates a fresh ApplicationState for the request via createState() — which first rejects a Host header that is not on the trusted-hosts allowlist — then calls handleAuthentication().
  4. handleAuthentication() determines the active firewall, attempts session token restoration, runs authenticators if needed, and either calls controllerResolver() or returns an authentication response.
  5. controllerResolver() enforces global access control, matches the route, enforces attribute-level access control (#[IsGranted]), instantiates the controller, resolves method parameters, and calls the controller method.
  6. The controller returns a ResponseInterface. prepareResponse() finalises headers and cookies.
  7. The response is emitted to the client. reset() clears request-scoped state.

Environment

Modufolio\Appkit\Core\Environment is an enum with three cases.

use Modufolio\Appkit\Core\Environment;

Environment::DEV   // 'dev'
Environment::TEST  // 'test'
Environment::PROD  // 'prod'

Helper methods:

$env->isDev();   // bool
$env->isTest();  // bool
$env->isProd();  // bool

The current environment is read from APP_ENV. It affects router cache validation (compiled routes are cached in var/cache/<env>/router in every environment; every environment except prod checks them for staleness — see Deployment), debug mode, error-output handling (see Exception handling), and exception detail visibility. Persistent caches live under Kernel::cacheDir() — var/cache/<env> — so environments never share a cache.

Access the environment from anywhere you have the kernel:

$this->environment()->isProd(); // inside App or a class with access to the kernel

Application state

ApplicationState is created once per request and holds request-scoped data: the current ServerRequestInterface, the session, the token storage, firewall cache, and controller instances. The concept is inspired by Axum's State extractor from Rust.

After the response is sent, reset() clears this state. This makes AppKit compatible with RoadRunner, where the same process handles many requests.

Note that AbstractApplicationState::reset() covers only session, session storage, token storage, request instances and the firewall cache. Kernel::reset() is abstract — your App is responsible for the rest, including the router (which holds a static compiled-route cache) and the entity manager. See Deployment.

Session cookies are set with HttpOnly and SameSite=Lax by default. Set COOKIE_SECURE=true in your environment to add the Secure flag, or declare a SessionConfiguration in config/services.php for the name, flags and lifetime. Where session data is stored is a \SessionHandlerInterface declared the same way — PHP's file handler under var/sessions unless you say otherwise. See Sessions.

The service container

The Kernel implements ContainerInterface. Call get(string $id) to resolve a service.

Resolution order:

  1. Service definitions (config/services.php)
  2. Kernel core services (or the legacy config/interfaces.php map)
  3. Singleton instances (already-created services)
  4. Repositories (Doctrine entity repositories)
  5. Authenticators (config/authenticators.php)
  6. Legacy factories (config/factories.php)
  7. The Symfony container behind the kernel, when the application configured one with configureContainer() and it knows the id
  8. NotFoundException if nothing matched
use Doctrine\ORM\EntityManagerInterface;

$em = $this->get(EntityManagerInterface::class);

You rarely call get() directly. Dependencies are wired through config files and injected into controllers. See Dependency injection.

Core service accessors

The Kernel exposes lazy-loaded services as methods. Use these inside App or config closures.

Method Returns
entityManager() EntityManagerInterface
environment() Environment
router() RouterInterface
session() FlashBagAwareSessionInterface
tokenStorage() TokenStorageInterface
userProvider() UserProviderInterface
validator() ValidatorInterface
serializer() SerializerInterface
parameterResolver() ParameterResolverInterface
exceptionHandler() ExceptionHandlerInterface
eventDispatcher() EventDispatcherInterface (PSR-14) — the notification seam, see Events
rateLimiter(string $name) RateLimiterFactory for a limiter declared in config/security.php, see Rate limiting
publicDir() The web root, public under the base directory unless setPublicDir() changes it
assetVersioning() AssetVersioningInterface — how templates version asset paths, see Templates
assetIntegrity() AssetIntegrity — the SRI map from config/sri.php
csrfTokenManager() CsrfTokenManagerInterface
request() ServerRequestInterface (the current request)
urlGenerator() UrlGeneratorInterface
logger() LoggerInterface
emitter() EmitterInterface

Your App continues this pattern for its own services — a typed method per service, lazily constructed with ??=, cleared in reset() when request-scoped. This is the primary way services are defined in AppKit; see Dependency injection.

URL helpers

$this->url('/login');     // https://example.com/login  (base derived from the current request)
$this->baseUrl();         // https://example.com  (scheme://host[:port] of the request)
$this->generateUrl('home');                                    // /
$this->generateUrl('post.show', ['slug' => 'hello']);          // /posts/hello

generateUrl() wraps Symfony's UrlGenerator. The third argument accepts UrlGeneratorInterface::ABSOLUTE_URL to force a full URL.

The scheme and host in all of these come from the request. Configure trusted hosts so a spoofed Host header cannot become the base of every absolute URL the response generates.

Parameter bag

Store and retrieve scalar configuration values.

$this->setParameter('app.name', 'My App');
$this->getParameter('app.name'); // 'My App'
$this->hasParameter('app.name'); // true

Parameters are available inside controller config as %app.name% strings.

Booting the application

Application code. AppFactory is not part of the framework. The skeleton (modufolio/appkit-skeleton) ships a starting version in src/AppFactory.php — create(string $baseDir): AppInterface, which builds the route loader, loads the config files and boots App — and the framework's test application keeps its own in tests/App/AppFactory.php; it is yours to change.

AppFactory::create(string $baseDir) — the test app's factory — is the standard entry point. The factory pattern is inspired by Slim PHP. It:

  1. Registers User::class with TokenUnserializer (whitelist-based session deserialization).
  2. Creates a route loader that scans src/Controller/ for #[Route] attributes and also loads PHP route files.
  3. Reads config/security.php into a SecurityConfigurator and config/services.php into a ServiceConfigurator.
  4. Passes all config arrays to App, calls configureServices() and configureSecurity(), and calls boot(). An application that has outgrown hand-wiring adds configureContainer(new ContainerFactory()) between the two — after modules and services, so the Symfony container sees the finished declarations; before boot(), which builds it.

You can create your own factory if you need a different setup.