revolution/atproto-lexicon-contracts
PHP contracts for AT Protocol lexicons: typed interfaces and value objects that keep atproto records, XRPC payloads, and schema handling consistent across Laravel apps. Code against stable abstractions instead of raw arrays.
composer require revolution/atproto-lexicon-contracts
This is a contracts-only package: auto-generated PHP interfaces and enums derived from ATProto lexicon JSON schemas. There is no HTTP client, no serializer, and no runtime behaviour bundled — you bring (or install) an implementation. That distinction shapes everything below.
Where to look first:
vendor/revolution/atproto-lexicon-contracts/composer.json — confirm the PHP version floor (enums require 8.1+), the PSR-4 root, and whether a generation script is shipped.src/ — namespaces typically mirror ATProto NSIDs in PascalCase, e.g. App\Bsky\Feed\Post, Com\Atproto\Repo\CreateRecord. Skim the tree before writing code; the shape tells you what's covered.README.md and any CHANGELOG — auto-generated packages change shape with upstream lexicon revisions, so check the last update date against the ATProto lexicons you care about.First use case: stop writing 'app.bsky.feed.post' as a string literal. Type a record creation flow against the generated interface and the $type enum:
use Revolution\Atproto\Lexicon\App\Bsky\Feed\Post;
final class CreatePost
{
public function __construct(private Post $record) {}
public function payload(): array
{
return [
'repo' => auth()->user()->did,
'collection' => $this->record::NSID, // or the enum case
'record' => $this->record->toArray(),
];
}
}
The win is compile-time safety on field names and enum values, not transport.
Interface-first services with container bindings. These are contracts; Laravel won't autowire a concrete implementation unless you tell it to. Register bindings in a service provider and type-hint interfaces in your services and jobs. This keeps your app decoupled from whichever runtime package (the author's sibling packages, symfony/serializer, cuyz/valinor, or hand-rolled hydrators) you end up using.
Enum-driven validation and casts. The generated enums map lexicon knownValues. Use them in FormRequest rules via Rule::enum(PostType::class) and as Eloquent cast targets for any column storing a known-value string. This gives you validation, IDE completion, and DB consistency from one source of truth.
DTO hydration at the boundary. Keep JSON↔contract mapping in one small mapper class. Controllers, jobs, and listeners should never call json_decode and index into arrays for ATProto payloads. When the lexicon updates, you change the mapper, not forty call sites.
Record builders. Wrap multi-field records in a fluent builder that returns a contract instance. ATProto records have many optional and union-typed fields (facets, embed, langs); builders keep controllers thin and make test fixtures trivial.
Queue boundaries. Interface instances are not serializable by default. Before dispatching a job, convert records to plain arrays (or implement Serializable on your concrete classes). Re-hydrate on the worker side.
Pin the version. Auto-generated code churns. Use an exact constraint ("1.2.3", not "^1.2") and review diffs on upgrade — a lexicon revision can rename or remove an interface without a semver major.
Testing. Mock the generated interfaces in unit tests; assert on enum cases rather than magic strings. For integration tests against a real PDS, treat the contracts as fixtures, not as an HTTP layer.
vendor/. The code is generated; any local patch is lost on the next composer update. If a schema is wrong for your use case, wrap it or open an upstream issue.knownValues (the "open enum" pattern). Prefer ::tryFrom() and retain the raw string when persisting, or you'll crash on records from newer clients.$type discriminators and unions. Lexicon unions map to interfaces with a discriminator accessor. Handle the "unknown union member" branch defensively; treat it as an open extension point, not an error.\Revolution\Atproto\Lexicon\App\Bsky\Feed\Post\Embed\External). Alias imports aggressively; run composer dump-autoload -o — the class count is large and classmap authoring speeds up boot.var_dump($record::class) and array_keys((array) $record) will expose the mismatch faster than stepping through the serializer.Extension points. The only real extension surfaces are your own concrete classes implementing the generated interfaces, your own enum-to-string mapping layer, and your serializer's normalizer configuration. There are no service providers to extend and no events to listen to inside the package itself.
How can I help you explore Laravel packages today?