Appendix: Satellite Packages

A reference map of what exists in each optional satellite package, by namespace. For core (kinetis/kinetis itself), see Appendix: System Layout.

packages/bref-adapter (kinetis/bref-adapter)

Separate Composer package, not part of kinetis/kinetis core.

  • Kinetis\BrefAdapter\BrefLambdaAdapter implements Kinetis\Runtime\RuntimeAdapterInterfacerun() 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 <timestamp>_<description>.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\MigrationRunnerpending()/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 <description>. 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\QueueInterfacepush(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\JobInvokerinvoke(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/migrationsbin/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\FilesystemFILESYSTEM_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 MailerInterfaceSymfony\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 <token> 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\TokenGeneratorgenerate(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 <token> 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/RS512JwtIssuer 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\JwtIssuerissue(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.16.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