# Appendix: Satellite Packages A reference map of what exists in each optional satellite package, by namespace. For core (`kinetis/kinetis` itself), see {doc}`appendix`. ## `packages/bref-adapter` (`kinetis/bref-adapter`) Separate Composer package, not part of `kinetis/kinetis` core. - `Kinetis\BrefAdapter\BrefLambdaAdapter implements Kinetis\Runtime\RuntimeAdapterInterface` — `run()` polls the Lambda Runtime API for the next invocation in a `while (true)` loop, converts the API Gateway v2 event into a PSR-7 request, and posts the response back as the invocation's result; `isPersistent(): true` (a warm container keeps reusing the same process across invocations, the same shape `FrankenPhpAdapter` has). Talks to the Runtime API with plain stream-context HTTP, not `ext-curl`. Parses `multipart/form-data` via `riverline/multipart-parser`'s `StreamedPart`, `application/x-www-form-urlencoded` via `parse_str()` — a Lambda event body is one in-memory string with no live `php://input`, so `request_parse_body()` (what core's own adapters use) can't apply here. - Depends on `kinetis/kinetis` (via a `path` repository to this monorepo's root), `nyholm/psr7`, `psr/http-message`, `riverline/multipart-parser`. Own `composer.json`/`phpunit.xml`/`phpstan.neon`. ## `packages/migrations` (`kinetis/migrations`) Separate Composer package, not part of `kinetis/kinetis` core. - `Kinetis\Migrations\Migration` — the interface a migration file's anonymous class implements: `up(MysqlLink|PostgresLink $db): void`/`down(...): void`, raw SQL only, issued via `$db->execute()`. - `Kinetis\Migrations\MigrationFile` — discovers `_.php` files under a `migrations/` project-root directory, sorted by filename; `load()` is a bare `require` of the file. - `Kinetis\Migrations\MigrationRepositoryInterface` / `SqlMigrationRepository` — tracks applied migrations in a `kinetis_migrations` table (`migration` primary key, `applied_at`), typed against the generic `Amp\Sql\SqlLink` since its own bookkeeping SQL is dialect-agnostic. - `Kinetis\Migrations\MigrationRunner` — `pending()`/`migrate()`/`rollback()`/`status()`. Never wraps a migration in a transaction; `rollback()` targets the single most recently applied migration only, throwing `Exception\MigrationFileMissingException` if that migration's file no longer exists. - `Kinetis\Migrations\MigrationScaffolder` — writes a new timestamped migration file with the `up()`/`down()` stubs filled in. - `bin/migrate` (→ `vendor/bin/migrate`) — its own binary, separate from core's `bin/kinetis`: `migrate`/`rollback`/`status`/`make `. Connects via `Kinetis\Persistence\SqlConnectionFactory`, reading `DB_CONNECTION` (`mysql`|`pgsql`, required) plus `DB_HOST`/`DB_NAME`/`DB_USER`/`DB_PASSWORD`/`DB_PORT`; `MIGRATE_CONNECTION_NAME` (default `'default'`) selects a named connection instead. - Depends on `kinetis/kinetis` (via a `path` repository to this monorepo's root), `amphp/mysql`, `amphp/postgres`, `amphp/sql` (`SqlMigrationRepository` types against the generic `Amp\Sql\SqlLink`). Own `composer.json`/`phpunit.xml`/`phpstan.neon`. ## `packages/query-builder` (`kinetis/query-builder`) Separate Composer package, not part of `kinetis/kinetis` core. - `Kinetis\QueryBuilder\Query` — a thin, parameterized SQL query builder, not an ORM (no relationships/migrations/change-tracking). One class works with either MySQL or Postgres via the shared `Amp\Sql\SqlLink` family (auto-detected instanceof `Amp\Mysql\MysqlLink`/`Amp\Postgres\PostgresLink`). `select()`/`selectRaw()`/`where()`/`orWhere()`/`whereIn()`/`whereRaw()`/`join()`/`leftJoin()`/`orderBy()`/`orderByRaw()`/`limit()`/`offset()`, terminal `get()`/`first()`/`count()` (optional `Hydrator`-based DTO mapping) and `insert()`/`insertGetId()`/`update()`/`delete()`. Accepts a plain pool or an in-flight `SqlTransaction`, so it composes inside `TransactionGuard::transaction()`. - `Kinetis\QueryBuilder\Dialect` (+ `Dialect\MySqlDialect`/`Dialect\PostgresDialect`) — isolates the only two genuine differences: identifier quoting, and retrieving a generated primary key after an insert (MySQL: `getLastInsertId()`; Postgres: `INSERT ... RETURNING`). - `Kinetis\QueryBuilder\CompiledQuery` — the `{sql, params}` output of every `to*Sql()` compile method, built together in one pass so bound parameters always land in the same position as their `?` in the generated SQL, even once `whereRaw()`/`whereIn()` mix with structured `where()` calls. - `Query::paginate(int $perPage, int $page = 1, ?string $dtoClass = null): Kinetis\Http\Pagination\Paginator` — a `count()` for `total`/`lastPage` plus a `limit()`/`offset()`-based `get()` for the page, against the same `where()`/`join()` filters already on the query. A page past the last one returns empty `data` with the real `total` still reported, not an error. - `Query::cursorPaginate(int $perPage, ?string $cursor, string $cursorColumn = 'id', ?string $dtoClass = null): Kinetis\Http\Pagination\CursorPaginator` — orders by `$cursorColumn` and filters `WHERE $cursorColumn > $cursor` once one is given (`null` fetches from the start); no `COUNT(*)`, no page number. Always fetches raw rows first regardless of `$dtoClass`, so `nextCursor` is read off the real column name rather than a hydrated DTO's own property name. - One `Query` instance is one query — nothing resets between fluent calls; construct a fresh instance per query. - Depends on `kinetis/kinetis` (via a `path` repository to this monorepo's root), `amphp/mysql`, `amphp/postgres`. Own `composer.json`/`phpunit.xml`/`phpstan.neon`. ## `packages/queue` (`kinetis/queue`) Separate Composer package, not part of `kinetis/kinetis` core. - `Kinetis\Queue\Job` — a marker interface (no declared methods) a job class implements. `handle()` is discovered and invoked by reflection, not a fixed interface method, since its parameter list varies per job. - `Kinetis\Queue\QueueInterface` — `push(Job $job, int $delaySeconds = 0, string $queue = 'default', ?int $maxAttempts = null): void`, `pop(int $timeoutSeconds = 0, array $queues = ['default']): ?QueuedJob`, `ack(QueuedJob $job): void`, `release(QueuedJob $job): void`, `fail(QueuedJob $job): void`. `$queues` is checked in the given order — priority by list position, not a numeric score. `$maxAttempts` null (the default) defers to the processing `QueueWorker`'s own `$defaultMaxAttempts`, which is never itself unlimited; once `QueuedJob::$attempts` reaches the effective cap, `fail()` removes the job permanently instead of `release()` retrying it. - `Kinetis\Queue\QueuedJob` — `{class, args, handle, queue, attempts, maxAttempts}`. `$attempts` is the attempt number the current `pop()` represents (1-indexed), not a raw failure count. - `Kinetis\Queue\JobSerializer` — converts a `Job` instance to plain `{class, args}` data (reading each constructor parameter's value off a same-named property via reflection) and back via `new $class(...$args)`. Throws `Exception\UnserializableJobException` for a constructor parameter with no matching property. - `Kinetis\Queue\JobInvoker` — `invoke(Job $job, ContainerInterface $container): void`, reflecting and calling `handle()` with each parameter resolved through the given container. Shared by `QueueWorker` and `SyncQueue`. - `Kinetis\Queue\RedisQueue` — backed by `Amp\Redis\RedisClient`. Uses the "reliable queue" pattern (`popTailPushHeadBlocking()`, i.e. `BRPOPLPUSH`) rather than a plain destructive pop, moving a job to a separate processing list until `ack()`/`release()`; delayed jobs live in a sorted set scored by ready-at time, promoted once per `pop()` call. - `Kinetis\Queue\SqlQueue` — backed by the generic `Amp\Sql\SqlLink` (dialect-agnostic SQL, including priority ordering via `CASE queue WHEN ... END`). Dequeues via `SELECT ... FOR UPDATE SKIP LOCKED` inside a transaction; `pop()`'s blocking contract is a poll loop suspended with `Kinetis\Async\Timer::delay()`, since SQL has no native blocking-wait primitive. Requires the `kinetis_queue_jobs` table (with `queue`, `attempts`, `max_attempts` columns and a composite `(queue, available_at, reserved_at)` index) — see `resources/migrations/create_kinetis_queue_jobs_table.{mysql,pgsql}.php.stub`, not auto-created. `fail()` deletes the row, the same as `ack()`. - `Kinetis\Queue\SyncQueue` — runs `push()`'s job immediately, inline, via `JobInvoker`; `pop()` always returns `null`, `ack()`/`release()`/`fail()` are no-ops. For local development; not selectable via `bin/queue`'s `QUEUE_CONNECTION`. A fresh `RequestScope` per `push()`, same as `QueueWorker`; unlike `QueueWorker`, a failing job's exception propagates rather than being caught and logged. - `Kinetis\Queue\QueueWorker` — `__construct(AppScope $app, QueueInterface $queue, int $defaultMaxAttempts = 0)`, `run()`/`processNext()`. One fresh `RequestScope` per job via `AppScope::createRequestScope()`, `handle()`'s parameters autowired through it via `JobInvoker`. A throwing job is always logged (job class/args + the exception); the effective cap is `QueuedJob::$maxAttempts ?? $defaultMaxAttempts` — released while `$attempts` is below it, `fail()`ed once reached. `$defaultMaxAttempts` is non-nullable: there is no configuration on this class that produces unlimited retries by default. - `Kinetis\Queue\QueuedListenerInvoker` — implements core's `Kinetis\Events\ListenerInvokerInterface`. Serializes the event (via `JobSerializer`, generalized to accept any `object`, not `Job` specifically) and pushes an `InvokeListenerJob` carrying the listener's class/method as plain strings. - `Kinetis\Queue\InvokeListenerJob` — the job `QueuedListenerInvoker` pushes. `handle(RequestScope $scope)` resolves the listener through the given scope and reconstructs the event via `JobSerializer::deserialize()`, invoking the original method by name. - `bin/queue` (→ `vendor/bin/queue`) — its own binary, separate from core's `bin/kinetis` and from `kinetis/migrations`' `bin/migrate`: `work [--queue=high,default]`. Connects via `QUEUE_CONNECTION` (`redis`|`sql`|`sqs`|`rabbitmq`, required) plus the matching `REDIS_*`/`DB_*` convention, or `QUEUE_SQS_*`/`QUEUE_RABBITMQ_*` (`packages/queue-sqs`/`packages/queue-rabbitmq`, below); `QUEUE_CONNECTION_NAME` (default `'default'`) selects a named connection of that backend. `QUEUE_POLL_TIMEOUT` and `QUEUE_MAX_ATTEMPTS` (passed through as `QueueWorker`'s `$defaultMaxAttempts`, both defaulting to `0`/no-retries when unset) round out its own env vars. `QUEUE_CONNECTION=sqs`/`rabbitmq` are each `class_exists()`-gated against their own package's client-factory class; throws `Exception\QueueUnavailableException` naming the missing package when it isn't installed. - Depends on `kinetis/kinetis` (via a `path` repository to this monorepo's root), `amphp/redis`, `amphp/sql` (`SqlQueue` types against the generic `Amp\Sql\SqlLink`), `psr/log` (`QueueWorker`'s failure logging), `psr/container` (`JobInvoker`'s container parameter). Own `composer.json`/`phpunit.xml`/`phpstan.neon`. ## `packages/queue-sqs` (`kinetis/queue-sqs`) Separate Composer package, not part of `kinetis/kinetis` core. - `Kinetis\QueueSqs\SqsQueue implements Kinetis\Queue\QueueInterface` — backed by `AsyncAws\Sqs\SqsClient`. `push()`/`pop()` map onto `SendMessage`/`ReceiveMessage`; `ack()`/`fail()` onto `DeleteMessage`; `release()` onto `ChangeMessageVisibility` with `VisibilityTimeout: 0` (immediately available again, rather than waiting out the normal timeout). A queue name resolves to an SQS queue of that name (optionally prefixed) via `GetQueueUrl`, cached per instance — never auto-created. `delaySeconds` uses SQS's own native `SendMessage` delay, capped at 900 seconds — a longer value throws before any network call. `QueuedJob::$attempts` comes directly from SQS's own `ApproximateReceiveCount` message attribute; `$maxAttempts` (no native SQS equivalent) travels as a custom `maxAttempts` message attribute. `pop()`'s multi-queue priority cycling uses a short, fixed per-queue `WaitTimeSeconds` (SQS's own long-polling primitive, capped at 20 seconds) — no `Kinetis\Async\Timer::delay()` or `concurrently()` wrapper, since the injected `AmpHttpClient` transport tolerates being called from plain top-level code. Standard SQS queues only; FIFO is not supported. - `Kinetis\QueueSqs\SqsClientFactory::fromConfig(Config $config, string $connection = 'default'): SqsClient` — builds `SqsClient` with `Kinetis\RevoltHttpClient\AmpHttpClientFactory::create()` injected as its transport. `QUEUE_SQS_REGION` required; `QUEUE_SQS_ENDPOINT`/`QUEUE_SQS_QUEUE_PREFIX` optional, all via `Config::scopedKey()`. Credentials are never read from `Kinetis\Config` — left to AsyncAws's own default credential provider chain. - `kinetis/queue`'s own `bin/queue` dispatches to this package for `QUEUE_CONNECTION=sqs` (see above). - Depends on `kinetis/kinetis`, `kinetis/queue`, and `kinetis/revolt-http-client` (all via `path` repositories), `async-aws/sqs`. Own `composer.json`/`phpunit.xml`/`phpstan.neon`. ## `packages/queue-rabbitmq` (`kinetis/queue-rabbitmq`) Separate Composer package, not part of `kinetis/kinetis` core. - `Kinetis\QueueRabbitMq\RabbitMqQueue implements Kinetis\Queue\QueueInterface` — backed by `Thesis\Amqp\Client`/`Channel`. A queue is declared durable on first touch by any method, never auto-created ahead of that. `push()` publishes to the queue directly; a delayed `push()` instead publishes to a dedicated `{queue}.delay` queue configured with `x-dead-letter-exchange`/`x-dead-letter-routing-key` pointing back at the real queue and a per-message `expiration` equal to the delay, so RabbitMQ itself moves the message once it expires — no polling-based promotion. `attempts`/`maxAttempts` travel as plain message headers (AMQP 0-9-1 has no native attempt count, only a boolean `redelivered` flag); `release()` republishes with an incremented `attempts` header before discarding the original delivery via `nack(requeue: false)`, since `nack`'s own `requeue` flag redelivers the message unchanged. `QueuedJob::$handle` is the `Thesis\Amqp\DeliveryMessage` itself. `pop()`'s multi-queue priority cycling uses `basic.get` (a single, immediate, non-blocking request per queue — AMQP has no native blocking-wait-with-timeout primitive), sleeping via `Amp\delay()` between full sweeps when nothing is found. One channel per instance, opened lazily and reused. Once opened, `Kinetis\Async\concurrently()` can't be called again anywhere in the same process — `Thesis\Amqp\Channel` keeps a permanent background reader for its whole lifetime, and `concurrently()` waits for `Revolt\EventLoop::run()` to return on its own, which never happens while that reader is still registered, even for a `concurrently()` call whose own tasks never touch this queue. - `Kinetis\QueueRabbitMq\RabbitMqClientFactory::fromConfig(Config $config, string $connection = 'default'): Client` — builds `Thesis\Amqp\Client` from `Thesis\Amqp\Config::fromURI()`. `QUEUE_RABBITMQ_URL` required, via `Config::scopedKey()`. - `kinetis/queue`'s own `bin/queue` dispatches to this package for `QUEUE_CONNECTION=rabbitmq` (see above). - Depends on `kinetis/kinetis` and `kinetis/queue` (both via `path` repositories), `thesis/amqp`. Own `composer.json`/`phpunit.xml`/`phpstan.neon`. ## `packages/storage` (`kinetis/storage`) Separate Composer package, not part of `kinetis/kinetis` core. - `Kinetis\Storage\AmpFileAdapter` — a `League\Flysystem\FilesystemAdapter` for local disk backed by `Amp\File\Filesystem` instead of Flysystem's own local adapter, so every operation suspends the calling Fiber via Revolt rather than blocking the worker. `readStream()` is the one exception: it reads the whole file via the same non-blocking primitive, then buffers it into an in-memory `php://temp` resource, since a PHP `resource` can't lazily pull from a userland object without a registered stream wrapper. `write()`/`writeStream()`/`copy()` stream genuinely, via `Amp\ByteStream\pipe()` between real `Amp\File\File` handles. - `Kinetis\Storage\FilesystemFactory::fromConfig(Config $config, string $connection = 'default'): League\Flysystem\Filesystem` — `FILESYSTEM_DRIVER` (default `'local'`) and `FILESYSTEM_ROOT` (required for the local driver), both via `Config::scopedKey()` for named connections. `FILESYSTEM_DRIVER=s3` dispatches to `packages/storage-s3` (below) if installed, else throws `Exception\StorageUnavailableException`. - Depends on `kinetis/kinetis` (via a `path` repository), `league/flysystem`, `league/mime-type-detection` (`FinfoMimeTypeDetector`), `amphp/file`, `amphp/byte-stream` (`AmpFileAdapter`'s streaming `write()`/`writeStream()`/`copy()`, via `Amp\ByteStream\pipe()`). Own `composer.json`/`phpunit.xml`/`phpstan.neon`. ## `packages/revolt-http-client` (`kinetis/revolt-http-client`) Separate Composer package, not part of `kinetis/kinetis` core — and, unlike every other satellite package, not dependent on it either: `kinetis/kinetis` appears only in `require-dev` (for tests and the `NoStaticPropertiesRule` dogfooding), never in `require`. Genuinely installable and usable with no Kinetis framework present at all. - `Kinetis\RevoltHttpClient\AmpHttpClientFactory::create(array $defaultOptions = [], ?callable $clientConfigurator = null, int $maxHostConnections = 6, int $maxPendingPushes = 50): Symfony\Contracts\HttpClient\HttpClientInterface` — mirrors `Symfony\Component\HttpClient\AmpHttpClient`'s own constructor exactly, no Kinetis-specific defaults layered on top. - Depends on `symfony/http-client` (`^8.0` — the first version whose `AmpHttpClient` targets the current, Revolt-based `amphp/http-client` generation rather than the old pre-Fiber one), `symfony/http-client-contracts`, and `amphp/http-client` (`^5.3`, an optional peer dependency of `symfony/http-client` that isn't auto-installed, so declared directly). Own `composer.json`/`phpunit.xml`/`phpstan.neon`. ## `packages/storage-s3` (`kinetis/storage-s3`) Separate Composer package, not part of `kinetis/kinetis` core. - `Kinetis\StorageS3\S3FilesystemFactory::fromConfig(Config $config, string $connection = 'default'): League\Flysystem\Filesystem` — builds `AsyncAws\S3\S3Client` with `Kinetis\RevoltHttpClient\AmpHttpClientFactory::create()` injected as its transport, wraps it in `League\Flysystem\AsyncAwsS3\AsyncAwsS3Adapter`. `FILESYSTEM_S3_BUCKET`/`FILESYSTEM_S3_REGION` required; `FILESYSTEM_S3_PREFIX`/`FILESYSTEM_S3_ENDPOINT`/`FILESYSTEM_S3_PATH_STYLE` optional, all via `Config::scopedKey()`. Credentials are never read from `Kinetis\Config` — left to `AsyncAws\Core\Configuration`'s own default credential provider chain. - `kinetis/storage`'s own `Kinetis\Storage\FilesystemFactory` dispatches to this package for `FILESYSTEM_DRIVER=s3`, `class_exists()`-gated; throws `Kinetis\Storage\Exception\StorageUnavailableException` naming this package when it isn't installed. - Depends on `kinetis/kinetis` and `kinetis/revolt-http-client` (both via `path` repositories), `async-aws/s3`, `league/flysystem-async-aws-s3`. Own `composer.json`/`phpunit.xml`/`phpstan.neon`. ## `packages/mailer` (`kinetis/mailer`) Separate Composer package, not part of `kinetis/kinetis` core. - `Kinetis\Mailer\MailerFactory::fromConfig(Config $config, string $connection = 'default'): Symfony\Component\Mailer\MailerInterface` — the only class in the package. Reads a single `MAILER_DSN` (`Config::scopedKey()` for named connections) and always passes `Kinetis\RevoltHttpClient\AmpHttpClientFactory::create()` into `Symfony\Component\Mailer\Transport::fromDsn()` as its `HttpClientInterface`. Genuinely non-blocking for any API-based transport (Sendgrid, Mailgun, Postmark, SES, ...) it resolves to; `EsmtpTransport` (SMTP) ignores the injected client and opens a raw, genuinely blocking socket regardless — a disclosed exception, not a bug. - No Kinetis-owned `MailerInterface` — `Symfony\Component\Mailer\MailerInterface` is used directly, the same "don't wrap an already-right abstraction" reasoning `kinetis/storage` already applies to `League\Flysystem\FilesystemOperator`. - `Transport::fromDsn()` discovers whichever bridge package (`symfony/sendgrid-mailer`, `symfony/mailgun-mailer`, ...) is actually installed via its own `class_exists()`-gated factory list — `MailerFactory` has no dispatch logic of its own. - Mail is queueable with zero code in this package: a `kinetis/queue` `Job`'s own `handle()` method constructor-injects `MailerInterface` exactly like any other service, resolved through the same container `QueueWorker`/`SyncQueue` already autowire against. - Depends on `kinetis/kinetis` and `kinetis/revolt-http-client` (both via `path` repositories), `symfony/mailer`. Own `composer.json`/`phpunit.xml`/`phpstan.neon`. ## `packages/auth` (`kinetis/auth`) Separate Composer package, not part of `kinetis/kinetis` core. - `Kinetis\Auth\BearerAuthMiddleware` — PSR-15 route middleware (never global) validating an `Authorization: Bearer ` header against an app-supplied `UserProviderInterface`, registering the resolved user on the current `RequestScope` as `CurrentUserInterface` on success, or returning `401` with a `WWW-Authenticate: Bearer` header on failure. Resolved fresh per request from the route's own `RequestScope`, so it constructor-injects `RequestScope` directly. - `Kinetis\Auth\UserProviderInterface` — one method, `findByToken(string $token): ?CurrentUserInterface`. Storage-agnostic; the app implements it. - `Kinetis\Auth\TokenGenerator` — `generate(int $bytes = 32): string`, a `random_bytes()` wrapper, hex-encoded. - Depends on `kinetis/kinetis` (via a `path` repository to this monorepo's root), `nyholm/psr7` (`BearerAuthMiddleware`'s `401` response), `psr/http-message`, `psr/http-server-middleware`. Own `composer.json`/`phpunit.xml`/`phpstan.neon`. ## `packages/auth-jwt` (`kinetis/auth-jwt`) Separate Composer package, not part of `kinetis/kinetis` core. - `Kinetis\AuthJwt\JwtAuthMiddleware` — PSR-15 route middleware (never global) verifying an `Authorization: Bearer ` header's signature via `firebase/php-jwt`, registering the decoded claims as a `JwtUser` (`CurrentUserInterface`) on success, or returning `401` with `WWW-Authenticate: Bearer` on failure — a decode exception, a structurally valid token with no `sub` claim, and a revoked token (checked against the optional `RevocationStore`) are all treated identically. `$key` is the shared secret for `HS256`/`HS384`/`HS512`, or the *public* half of a key pair (PEM string) for `RS256`/`RS384`/`RS512` — `JwtIssuer` takes the matching *private* half. Deliberately not `final`: `#[Middleware(...)]` carries only a class-string, with nowhere to pass a key, so a subclass supplying it via a constructor of only class-typed parameters is the documented pattern. - `Kinetis\AuthJwt\JwtUser` — wraps the decoded claims (`stdClass`). `id()` reads `sub`, throwing if it's missing or non-scalar; `claim(string)`/`claims()` expose the rest. - `Kinetis\AuthJwt\JwtIssuer` — `issue(string|int $subject, array $claims = [], ?int $ttlSeconds = 3600): string`, signing with the same key/algorithm `JwtAuthMiddleware` verifies against. `sub`/`iat`/`exp`/`jti` (a random unique token ID) always win over a same-named entry in `$claims`. - `Kinetis\AuthJwt\RevocationStore` — a `Psr\SimpleCache\CacheInterface`-backed denylist keyed by `jti`. `revoke(string $jti, int $ttlSeconds)` is the primitive; `revokeToken(JwtUser $user)` derives the TTL from the token's own `exp` claim automatically. Per-token revocation only, not per-user. - Depends on `kinetis/kinetis` (via a `path` repository to this monorepo's root), `firebase/php-jwt` (`^7.1` — `6.10`/`6.11` are excluded by an open security advisory), `psr/simple-cache` (`RevocationStore`), `nyholm/psr7`, `psr/http-message`, `psr/http-server-middleware`. Own `composer.json`/`phpunit.xml`/`phpstan.neon`. ## See also - {doc}`appendix` — the same reference map for core (`kinetis/kinetis`). - {doc}`appendix-ci` — what actually runs in CI, including the real-backend integration checks for several packages listed above. - {doc}`migrations`, {doc}`query-builder`, {doc}`queue`, {doc}`queue-sqs`, {doc}`queue-rabbitmq`, {doc}`storage`, {doc}`storage-s3`, {doc}`revolt-http-client`, {doc}`mailer`, {doc}`auth`, {doc}`auth-jwt` — the task-oriented page for each package above.