# Getting Started This page gets a minimal Kinetis application running under FrankenPHP, end to end. If you just want to see what Kinetis code looks like, skip to [Your first controller](#your-first-controller). ## Requirements - PHP 8.4 or later - [Composer](https://getcomposer.org) - [FrankenPHP](https://frankenphp.dev) for the primary, persistent-worker deployment target — though everything in this guide also runs correctly under classic PHP-FPM, which Kinetis detects and falls back to automatically. See {doc}`runtime-adapters` for the full detection story. ## Installation ```{code-block} bash composer require kinetis/kinetis ``` Kinetis ships as a single package (`kinetis/kinetis`) — there's no `kinetis/http`, `kinetis/di`, `kinetis/routing` to assemble separately. ## Your first controller Kinetis controllers are plain PHP classes. There's no base class to extend and no interface to implement — routes are declared with attributes directly on public methods: ```{code-block} php :caption: src/Http/UserController.php name, email: $data->email); } #[Get('/users')] public function index(#[Query] int $page = 1, #[Query] int $limit = 20): array { return ['page' => $page, 'limit' => $limit]; } #[Get('/users/{id}')] public function show(int $id): array { return ['id' => $id]; } } ``` A few things worth noticing already, since they come up throughout this documentation: - `#[Body]` marks a parameter as bound to the decoded JSON request body — its type (`CreateUserRequest`) is a DTO class that gets **validated before the controller ever runs**. See {doc}`routing-validation`. - `#[Query]` binds a query-string parameter, cast to the parameter's declared scalar type. - `show()`'s `$id` parameter needs no attribute at all — Kinetis matches it against the `{id}` placeholder in the route path by name. - The controller class itself is `final readonly` with no constructor here, but if it needed dependencies, they'd be constructor-injected from the container — see {doc}`container`. The DTO referenced above is just as plain: ```{code-block} php :caption: src/Requests/CreateUserRequest.php boot(); $router = RouteDiscovery::discover(ProjectRoot::detect(__DIR__)); $adapter = RuntimeDetector::detect(); $kernel = new Kernel($app, $router, isPersistent: $adapter->isPersistent()); $adapter->run($kernel->handle(...)); ``` `RouteDiscovery::discover()` finds `UserController` on its own — any class anywhere under one of your own PSR-4 roots is picked up automatically, with no required directory or namespace convention and nothing to register by hand (see {doc}`cli` for restricting the scan on a large application). `RuntimeDetector::detect()` figures out which runtime it's running under — FrankenPHP, AWS Lambda, or plain PHP-FPM — and returns the matching adapter, with zero configuration on your part. The exact same `public/index.php` runs unmodified in all three. See {doc}`runtime-adapters` for how that detection actually works and what each adapter does differently. ```{tip} `bin/kinetis build` pre-compiles routing (and everything else discovered by namespace) ahead of a production deploy — see {doc}`caching`. ``` ## Running it under FrankenPHP A minimal `Caddyfile`: ```{code-block} :caption: Caddyfile { admin off } :8080 { root * public php_server { worker public/index.php } } ``` ```{warning} The `worker` directive **must** point at the same file Caddy's `php_server` directive would classically execute for an unmatched request — in this setup, `public/index.php`. Caddy falls back to classically re-executing `index.php` for any request path that doesn't match a real static file *before* it ever routes to a worker pointed somewhere else. Point `worker` at a different script and every request will silently keep re-executing `index.php` from scratch instead of ever reaching your worker, with no error to indicate why. ``` ```{code-block} bash docker run --rm -p 8080:8080 -v "$PWD":/app -w /app dunglas/frankenphp:latest \ frankenphp run --config Caddyfile ``` ```{note} FrankenPHP's worker mode keeps `public/index.php` — including route discovery — loaded in memory across every request it serves. If you edit a controller while this container is still running, restart it to see the change; PHP has no way to redeclare an already-loaded class with new content. For active local development where you're editing code constantly, PHP-FPM's classic boot-and-die model (which Kinetis detects and runs under automatically — no code changes needed) rebuilds this on every single request instead, at the cost of paying discovery's cost every time rather than once. See {doc}`runtime-adapters`. ``` ```{code-block} bash :caption: Try it curl -X POST http://localhost:8080/users \ -H "Content-Type: application/json" \ -d '{"name": "John Doe", "email": "john@example.com"}' # {"name":"John Doe","email":"john@example.com"} ``` Every registered route also gets a free, zero-config OpenAPI document and Swagger UI — visit `http://localhost:8080/docs` right now, no annotations beyond the attributes already on `UserController` needed. More on that in {doc}`routing-validation`. ## Next steps - {doc}`core-concepts` — why persistent workers change the rules, and how Kinetis's request lifecycle is built around that. - {doc}`container` — the two-tier container, and the one PHPStan rule that keeps it an enforced guarantee instead of a convention. - {doc}`routing-validation` — the full attribute vocabulary, validation constraints, and the OpenAPI generator. - {doc}`auth` — opaque Bearer-token authentication middleware, for when you want your own token storage (and revocation). - {doc}`auth-jwt` — stateless JWT authentication instead, verifying a signed token with no storage lookup at all.