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\RuntimeAdapterInterface—run()polls the Lambda Runtime API for the next invocation in awhile (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 shapeFrankenPhpAdapterhas). Talks to the Runtime API with plain stream-context HTTP, notext-curl. Parsesmultipart/form-dataviariverline/multipart-parser’sStreamedPart,application/x-www-form-urlencodedviaparse_str()— a Lambda event body is one in-memory string with no livephp://input, sorequest_parse_body()(what core’s own adapters use) can’t apply here.Depends on
kinetis/kinetis(via apathrepository to this monorepo’s root),nyholm/psr7,psr/http-message,riverline/multipart-parser. Owncomposer.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>.phpfiles under amigrations/project-root directory, sorted by filename;load()is a barerequireof the file.Kinetis\Migrations\MigrationRepositoryInterface/SqlMigrationRepository— tracks applied migrations in akinetis_migrationstable (migrationprimary key,applied_at), typed against the genericAmp\Sql\SqlLinksince 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, throwingException\MigrationFileMissingExceptionif that migration’s file no longer exists.Kinetis\Migrations\MigrationScaffolder— writes a new timestamped migration file with theup()/down()stubs filled in.bin/migrate(→vendor/bin/migrate) — its own binary, separate from core’sbin/kinetis:migrate/rollback/status/make <description>. Connects viaKinetis\Persistence\SqlConnectionFactory, readingDB_CONNECTION(mysql|pgsql, required) plusDB_HOST/DB_NAME/DB_USER/DB_PASSWORD/DB_PORT;MIGRATE_CONNECTION_NAME(default'default') selects a named connection instead.Depends on
kinetis/kinetis(via apathrepository to this monorepo’s root),amphp/mysql,amphp/postgres,amphp/sql(SqlMigrationRepositorytypes against the genericAmp\Sql\SqlLink). Owncomposer.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 sharedAmp\Sql\SqlLinkfamily (auto-detected instanceofAmp\Mysql\MysqlLink/Amp\Postgres\PostgresLink).select()/selectRaw()/where()/orWhere()/whereIn()/whereRaw()/join()/leftJoin()/orderBy()/orderByRaw()/limit()/offset(), terminalget()/first()/count()(optionalHydrator-based DTO mapping) andinsert()/insertGetId()/update()/delete(). Accepts a plain pool or an in-flightSqlTransaction, so it composes insideTransactionGuard::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 everyto*Sql()compile method, built together in one pass so bound parameters always land in the same position as their?in the generated SQL, even oncewhereRaw()/whereIn()mix with structuredwhere()calls.Query::paginate(int $perPage, int $page = 1, ?string $dtoClass = null): Kinetis\Http\Pagination\Paginator— acount()fortotal/lastPageplus alimit()/offset()-basedget()for the page, against the samewhere()/join()filters already on the query. A page past the last one returns emptydatawith the realtotalstill reported, not an error.Query::cursorPaginate(int $perPage, ?string $cursor, string $cursorColumn = 'id', ?string $dtoClass = null): Kinetis\Http\Pagination\CursorPaginator— orders by$cursorColumnand filtersWHERE $cursorColumn > $cursoronce one is given (nullfetches from the start); noCOUNT(*), no page number. Always fetches raw rows first regardless of$dtoClass, sonextCursoris read off the real column name rather than a hydrated DTO’s own property name.One
Queryinstance is one query — nothing resets between fluent calls; construct a fresh instance per query.Depends on
kinetis/kinetis(via apathrepository to this monorepo’s root),amphp/mysql,amphp/postgres. Owncomposer.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.$queuesis checked in the given order — priority by list position, not a numeric score.$maxAttemptsnull (the default) defers to the processingQueueWorker’s own$defaultMaxAttempts, which is never itself unlimited; onceQueuedJob::$attemptsreaches the effective cap,fail()removes the job permanently instead ofrelease()retrying it.Kinetis\Queue\QueuedJob—{class, args, handle, queue, attempts, maxAttempts}.$attemptsis the attempt number the currentpop()represents (1-indexed), not a raw failure count.Kinetis\Queue\JobSerializer— converts aJobinstance to plain{class, args}data (reading each constructor parameter’s value off a same-named property via reflection) and back vianew $class(...$args). ThrowsException\UnserializableJobExceptionfor a constructor parameter with no matching property.Kinetis\Queue\JobInvoker—invoke(Job $job, ContainerInterface $container): void, reflecting and callinghandle()with each parameter resolved through the given container. Shared byQueueWorkerandSyncQueue.Kinetis\Queue\RedisQueue— backed byAmp\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 untilack()/release(); delayed jobs live in a sorted set scored by ready-at time, promoted once perpop()call.Kinetis\Queue\SqlQueue— backed by the genericAmp\Sql\SqlLink(dialect-agnostic SQL, including priority ordering viaCASE queue WHEN ... END). Dequeues viaSELECT ... FOR UPDATE SKIP LOCKEDinside a transaction;pop()’s blocking contract is a poll loop suspended withKinetis\Async\Timer::delay(), since SQL has no native blocking-wait primitive. Requires thekinetis_queue_jobstable (withqueue,attempts,max_attemptscolumns and a composite(queue, available_at, reserved_at)index) — seeresources/migrations/create_kinetis_queue_jobs_table.{mysql,pgsql}.php.stub, not auto-created.fail()deletes the row, the same asack().Kinetis\Queue\SyncQueue— runspush()’s job immediately, inline, viaJobInvoker;pop()always returnsnull,ack()/release()/fail()are no-ops. For local development; not selectable viabin/queue’sQUEUE_CONNECTION. A freshRequestScopeperpush(), same asQueueWorker; unlikeQueueWorker, 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 freshRequestScopeper job viaAppScope::createRequestScope(),handle()’s parameters autowired through it viaJobInvoker. A throwing job is always logged (job class/args + the exception); the effective cap isQueuedJob::$maxAttempts ?? $defaultMaxAttempts— released while$attemptsis below it,fail()ed once reached.$defaultMaxAttemptsis non-nullable: there is no configuration on this class that produces unlimited retries by default.Kinetis\Queue\QueuedListenerInvoker— implements core’sKinetis\Events\ListenerInvokerInterface. Serializes the event (viaJobSerializer, generalized to accept anyobject, notJobspecifically) and pushes anInvokeListenerJobcarrying the listener’s class/method as plain strings.Kinetis\Queue\InvokeListenerJob— the jobQueuedListenerInvokerpushes.handle(RequestScope $scope)resolves the listener through the given scope and reconstructs the event viaJobSerializer::deserialize(), invoking the original method by name.bin/queue(→vendor/bin/queue) — its own binary, separate from core’sbin/kinetisand fromkinetis/migrations’bin/migrate:work [--queue=high,default]. Connects viaQUEUE_CONNECTION(redis|sql|sqs|rabbitmq, required) plus the matchingREDIS_*/DB_*convention, orQUEUE_SQS_*/QUEUE_RABBITMQ_*(packages/queue-sqs/packages/queue-rabbitmq, below);QUEUE_CONNECTION_NAME(default'default') selects a named connection of that backend.QUEUE_POLL_TIMEOUTandQUEUE_MAX_ATTEMPTS(passed through asQueueWorker’s$defaultMaxAttempts, both defaulting to0/no-retries when unset) round out its own env vars.QUEUE_CONNECTION=sqs/rabbitmqare eachclass_exists()-gated against their own package’s client-factory class; throwsException\QueueUnavailableExceptionnaming the missing package when it isn’t installed.Depends on
kinetis/kinetis(via apathrepository to this monorepo’s root),amphp/redis,amphp/sql(SqlQueuetypes against the genericAmp\Sql\SqlLink),psr/log(QueueWorker’s failure logging),psr/container(JobInvoker’s container parameter). Owncomposer.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 byAsyncAws\Sqs\SqsClient.push()/pop()map ontoSendMessage/ReceiveMessage;ack()/fail()ontoDeleteMessage;release()ontoChangeMessageVisibilitywithVisibilityTimeout: 0(immediately available again, rather than waiting out the normal timeout). A queue name resolves to an SQS queue of that name (optionally prefixed) viaGetQueueUrl, cached per instance — never auto-created.delaySecondsuses SQS’s own nativeSendMessagedelay, capped at 900 seconds — a longer value throws before any network call.QueuedJob::$attemptscomes directly from SQS’s ownApproximateReceiveCountmessage attribute;$maxAttempts(no native SQS equivalent) travels as a custommaxAttemptsmessage attribute.pop()’s multi-queue priority cycling uses a short, fixed per-queueWaitTimeSeconds(SQS’s own long-polling primitive, capped at 20 seconds) — noKinetis\Async\Timer::delay()orconcurrently()wrapper, since the injectedAmpHttpClienttransport 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— buildsSqsClientwithKinetis\RevoltHttpClient\AmpHttpClientFactory::create()injected as its transport.QUEUE_SQS_REGIONrequired;QUEUE_SQS_ENDPOINT/QUEUE_SQS_QUEUE_PREFIXoptional, all viaConfig::scopedKey(). Credentials are never read fromKinetis\Config— left to AsyncAws’s own default credential provider chain.kinetis/queue’s ownbin/queuedispatches to this package forQUEUE_CONNECTION=sqs(see above).Depends on
kinetis/kinetis,kinetis/queue, andkinetis/revolt-http-client(all viapathrepositories),async-aws/sqs. Owncomposer.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 byThesis\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 delayedpush()instead publishes to a dedicated{queue}.delayqueue configured withx-dead-letter-exchange/x-dead-letter-routing-keypointing back at the real queue and a per-messageexpirationequal to the delay, so RabbitMQ itself moves the message once it expires — no polling-based promotion.attempts/maxAttemptstravel as plain message headers (AMQP 0-9-1 has no native attempt count, only a booleanredeliveredflag);release()republishes with an incrementedattemptsheader before discarding the original delivery vianack(requeue: false), sincenack’s ownrequeueflag redelivers the message unchanged.QueuedJob::$handleis theThesis\Amqp\DeliveryMessageitself.pop()’s multi-queue priority cycling usesbasic.get(a single, immediate, non-blocking request per queue — AMQP has no native blocking-wait-with-timeout primitive), sleeping viaAmp\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\Channelkeeps a permanent background reader for its whole lifetime, andconcurrently()waits forRevolt\EventLoop::run()to return on its own, which never happens while that reader is still registered, even for aconcurrently()call whose own tasks never touch this queue.Kinetis\QueueRabbitMq\RabbitMqClientFactory::fromConfig(Config $config, string $connection = 'default'): Client— buildsThesis\Amqp\ClientfromThesis\Amqp\Config::fromURI().QUEUE_RABBITMQ_URLrequired, viaConfig::scopedKey().kinetis/queue’s ownbin/queuedispatches to this package forQUEUE_CONNECTION=rabbitmq(see above).Depends on
kinetis/kinetisandkinetis/queue(both viapathrepositories),thesis/amqp. Owncomposer.json/phpunit.xml/phpstan.neon.
packages/storage (kinetis/storage)¶
Separate Composer package, not part of kinetis/kinetis core.
Kinetis\Storage\AmpFileAdapter— aLeague\Flysystem\FilesystemAdapterfor local disk backed byAmp\File\Filesysteminstead 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-memoryphp://tempresource, since a PHPresourcecan’t lazily pull from a userland object without a registered stream wrapper.write()/writeStream()/copy()stream genuinely, viaAmp\ByteStream\pipe()between realAmp\File\Filehandles.Kinetis\Storage\FilesystemFactory::fromConfig(Config $config, string $connection = 'default'): League\Flysystem\Filesystem—FILESYSTEM_DRIVER(default'local') andFILESYSTEM_ROOT(required for the local driver), both viaConfig::scopedKey()for named connections.FILESYSTEM_DRIVER=s3dispatches topackages/storage-s3(below) if installed, else throwsException\StorageUnavailableException.Depends on
kinetis/kinetis(via apathrepository),league/flysystem,league/mime-type-detection(FinfoMimeTypeDetector),amphp/file,amphp/byte-stream(AmpFileAdapter’s streamingwrite()/writeStream()/copy(), viaAmp\ByteStream\pipe()). Owncomposer.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— mirrorsSymfony\Component\HttpClient\AmpHttpClient’s own constructor exactly, no Kinetis-specific defaults layered on top.Depends on
symfony/http-client(^8.0— the first version whoseAmpHttpClienttargets the current, Revolt-basedamphp/http-clientgeneration rather than the old pre-Fiber one),symfony/http-client-contracts, andamphp/http-client(^5.3, an optional peer dependency ofsymfony/http-clientthat isn’t auto-installed, so declared directly). Owncomposer.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— buildsAsyncAws\S3\S3ClientwithKinetis\RevoltHttpClient\AmpHttpClientFactory::create()injected as its transport, wraps it inLeague\Flysystem\AsyncAwsS3\AsyncAwsS3Adapter.FILESYSTEM_S3_BUCKET/FILESYSTEM_S3_REGIONrequired;FILESYSTEM_S3_PREFIX/FILESYSTEM_S3_ENDPOINT/FILESYSTEM_S3_PATH_STYLEoptional, all viaConfig::scopedKey(). Credentials are never read fromKinetis\Config— left toAsyncAws\Core\Configuration’s own default credential provider chain.kinetis/storage’s ownKinetis\Storage\FilesystemFactorydispatches to this package forFILESYSTEM_DRIVER=s3,class_exists()-gated; throwsKinetis\Storage\Exception\StorageUnavailableExceptionnaming this package when it isn’t installed.Depends on
kinetis/kinetisandkinetis/revolt-http-client(both viapathrepositories),async-aws/s3,league/flysystem-async-aws-s3. Owncomposer.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 singleMAILER_DSN(Config::scopedKey()for named connections) and always passesKinetis\RevoltHttpClient\AmpHttpClientFactory::create()intoSymfony\Component\Mailer\Transport::fromDsn()as itsHttpClientInterface. 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\MailerInterfaceis used directly, the same “don’t wrap an already-right abstraction” reasoningkinetis/storagealready applies toLeague\Flysystem\FilesystemOperator.Transport::fromDsn()discovers whichever bridge package (symfony/sendgrid-mailer,symfony/mailgun-mailer, …) is actually installed via its ownclass_exists()-gated factory list —MailerFactoryhas no dispatch logic of its own.Mail is queueable with zero code in this package: a
kinetis/queueJob’s ownhandle()method constructor-injectsMailerInterfaceexactly like any other service, resolved through the same containerQueueWorker/SyncQueuealready autowire against.Depends on
kinetis/kinetisandkinetis/revolt-http-client(both viapathrepositories),symfony/mailer. Owncomposer.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 anAuthorization: Bearer <token>header against an app-suppliedUserProviderInterface, registering the resolved user on the currentRequestScopeasCurrentUserInterfaceon success, or returning401with aWWW-Authenticate: Bearerheader on failure. Resolved fresh per request from the route’s ownRequestScope, so it constructor-injectsRequestScopedirectly.Kinetis\Auth\UserProviderInterface— one method,findByToken(string $token): ?CurrentUserInterface. Storage-agnostic; the app implements it.Kinetis\Auth\TokenGenerator—generate(int $bytes = 32): string, arandom_bytes()wrapper, hex-encoded.Depends on
kinetis/kinetis(via apathrepository to this monorepo’s root),nyholm/psr7(BearerAuthMiddleware’s401response),psr/http-message,psr/http-server-middleware. Owncomposer.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 anAuthorization: Bearer <token>header’s signature viafirebase/php-jwt, registering the decoded claims as aJwtUser(CurrentUserInterface) on success, or returning401withWWW-Authenticate: Beareron failure — a decode exception, a structurally valid token with nosubclaim, and a revoked token (checked against the optionalRevocationStore) are all treated identically.$keyis the shared secret forHS256/HS384/HS512, or the public half of a key pair (PEM string) forRS256/RS384/RS512—JwtIssuertakes the matching private half. Deliberately notfinal:#[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()readssub, 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/algorithmJwtAuthMiddlewareverifies against.sub/iat/exp/jti(a random unique token ID) always win over a same-named entry in$claims.Kinetis\AuthJwt\RevocationStore— aPsr\SimpleCache\CacheInterface-backed denylist keyed byjti.revoke(string $jti, int $ttlSeconds)is the primitive;revokeToken(JwtUser $user)derives the TTL from the token’s ownexpclaim automatically. Per-token revocation only, not per-user.Depends on
kinetis/kinetis(via apathrepository to this monorepo’s root),firebase/php-jwt(^7.1—6.10/6.11are excluded by an open security advisory),psr/simple-cache(RevocationStore),nyholm/psr7,psr/http-message,psr/http-server-middleware. Owncomposer.json/phpunit.xml/phpstan.neon.
See also¶
Appendix: System Layout — the same reference map for core (
kinetis/kinetis).Appendix: Continuous Integration — what actually runs in CI, including the real-backend integration checks for several packages listed above.
Migrations, Query Builder, Queue, Queue (SQS), Queue (RabbitMQ), Storage, Storage (S3), Appendix: Revolt HTTP Client, Mailer, Authentication, JWT Authentication — the task-oriented page for each package above.