atp-schema Integration#
Parity is built on top of atp-schema, using its Data base class for all record DTOs. This provides type safety, validation, and compatibility with the AT Protocol ecosystem.
How It Works#
The SocialDept\AtpParity\Data\Record class extends SocialDept\AtpSchema\Data\Data:
namespace SocialDept\AtpParity\Data;
use SocialDept\AtpClient\Contracts\Recordable;
use SocialDept\AtpSchema\Data\Data;
abstract class Record extends Data implements Recordable
{
public function getType(): string
{
return static::getLexicon();
}
}
This means all Parity records inherit:
getLexicon()- Returns the lexicon NSIDfromArray()- Creates instance from array datatoArray()- Converts to arraytoRecord()- Converts to record format for API calls- Type validation and casting
Using Generated Schema Classes#
atp-schema generates PHP classes for all AT Protocol lexicons. Use them directly with Parity:
use SocialDept\AtpParity\Support\SchemaMapper;
use SocialDept\AtpSchema\Generated\App\Bsky\Feed\Post;
use SocialDept\AtpSchema\Generated\App\Bsky\Feed\Like;
use SocialDept\AtpSchema\Generated\App\Bsky\Graph\Follow;
// Post mapper
$postMapper = new SchemaMapper(
schemaClass: Post::class,
modelClass: \App\Models\Post::class,
toAttributes: fn(Post $post) => [
'content' => $post->text,
'published_at' => $post->createdAt,
'langs' => $post->langs,
'reply_parent' => $post->reply?->parent->uri,
'reply_root' => $post->reply?->root->uri,
],
toRecordData: fn($model) => [
'text' => $model->content,
'createdAt' => $model->published_at->toIso8601String(),
'langs' => $model->langs ?? ['en'],
],
);
// Like mapper
$likeMapper = new SchemaMapper(
schemaClass: Like::class,
modelClass: \App\Models\Like::class,
toAttributes: fn(Like $like) => [
'subject_uri' => $like->subject->uri,
'subject_cid' => $like->subject->cid,
'liked_at' => $like->createdAt,
],
toRecordData: fn($model) => [
'subject' => [
'uri' => $model->subject_uri,
'cid' => $model->subject_cid,
],
'createdAt' => $model->liked_at->toIso8601String(),
],
);
// Follow mapper
$followMapper = new SchemaMapper(
schemaClass: Follow::class,
modelClass: \App\Models\Follow::class,
toAttributes: fn(Follow $follow) => [
'subject_did' => $follow->subject,
'followed_at' => $follow->createdAt,
],
toRecordData: fn($model) => [
'subject' => $model->subject_did,
'createdAt' => $model->followed_at->toIso8601String(),
],
);
Creating Custom Records#
For custom lexicons or when you need more control, extend the Record class:
<?php
namespace App\AtpRecords;
use Carbon\Carbon;
use SocialDept\AtpParity\Data\Record;
class CustomPost extends Record
{
public function __construct(
public readonly string $text,
public readonly Carbon $createdAt,
public readonly ?array $facets = null,
public readonly ?array $embed = null,
public readonly ?array $langs = null,
) {}
public static function getLexicon(): string
{
return 'app.bsky.feed.post';
}
public static function fromArray(array $data): static
{
return new static(
text: $data['text'],
createdAt: Carbon::parse($data['createdAt']),
facets: $data['facets'] ?? null,
embed: $data['embed'] ?? null,
langs: $data['langs'] ?? null,
);
}
public function toArray(): array
{
return array_filter([
'$type' => static::getLexicon(),
'text' => $this->text,
'createdAt' => $this->createdAt->toIso8601String(),
'facets' => $this->facets,
'embed' => $this->embed,
'langs' => $this->langs,
], fn($v) => $v !== null);
}
}
Custom Lexicons (AppView)#
Building a custom AT Protocol application? Define your own lexicons:
<?php
namespace App\AtpRecords;
use Carbon\Carbon;
use SocialDept\AtpParity\Data\Record;
class Article extends Record
{
public function __construct(
public readonly string $title,
public readonly string $body,
public readonly Carbon $publishedAt,
public readonly ?array $tags = null,
public readonly ?string $coverImage = null,
) {}
public static function getLexicon(): string
{
return 'com.myapp.blog.article'; // Your custom NSID
}
public static function fromArray(array $data): static
{
return new static(
title: $data['title'],
body: $data['body'],
publishedAt: Carbon::parse($data['publishedAt']),
tags: $data['tags'] ?? null,
coverImage: $data['coverImage'] ?? null,
);
}
public function toArray(): array
{
return array_filter([
'$type' => static::getLexicon(),
'title' => $this->title,
'body' => $this->body,
'publishedAt' => $this->publishedAt->toIso8601String(),
'tags' => $this->tags,
'coverImage' => $this->coverImage,
], fn($v) => $v !== null);
}
}
Working with Embedded Types#
atp-schema generates classes for embedded types. Use them in your mappings:
use SocialDept\AtpSchema\Generated\App\Bsky\Feed\Post;
use SocialDept\AtpSchema\Generated\App\Bsky\Embed\Images;
use SocialDept\AtpSchema\Generated\App\Bsky\Embed\External;
use SocialDept\AtpSchema\Generated\Com\Atproto\Repo\StrongRef;
$mapper = new SchemaMapper(
schemaClass: Post::class,
modelClass: \App\Models\Post::class,
toAttributes: fn(Post $post) => [
'content' => $post->text,
'published_at' => $post->createdAt,
'has_images' => $post->embed instanceof Images,
'has_link' => $post->embed instanceof External,
'embed_data' => $post->embed?->toArray(),
],
toRecordData: fn($model) => [
'text' => $model->content,
'createdAt' => $model->published_at->toIso8601String(),
],
);
Handling Union Types#
AT Protocol uses union types for fields like embed. atp-schema handles these via discriminated unions:
use SocialDept\AtpSchema\Generated\App\Bsky\Feed\Post;
use SocialDept\AtpSchema\Generated\App\Bsky\Embed\Images;
use SocialDept\AtpSchema\Generated\App\Bsky\Embed\External;
use SocialDept\AtpSchema\Generated\App\Bsky\Embed\Record;
use SocialDept\AtpSchema\Generated\App\Bsky\Embed\RecordWithMedia;
$toAttributes = function(Post $post): array {
$attributes = [
'content' => $post->text,
'published_at' => $post->createdAt,
];
// Handle embed union type
if ($post->embed) {
match (true) {
$post->embed instanceof Images => $attributes['embed_type'] = 'images',
$post->embed instanceof External => $attributes['embed_type'] = 'external',
$post->embed instanceof Record => $attributes['embed_type'] = 'quote',
$post->embed instanceof RecordWithMedia => $attributes['embed_type'] = 'quote_media',
default => $attributes['embed_type'] = 'unknown',
};
$attributes['embed_data'] = $post->embed->toArray();
}
return $attributes;
};
Reply Threading#
Posts can be replies to other posts:
use SocialDept\AtpSchema\Generated\App\Bsky\Feed\Post;
$toAttributes = function(Post $post): array {
$attributes = [
'content' => $post->text,
'published_at' => $post->createdAt,
'is_reply' => $post->reply !== null,
];
if ($post->reply) {
// Parent is the immediate post being replied to
$attributes['reply_parent_uri'] = $post->reply->parent->uri;
$attributes['reply_parent_cid'] = $post->reply->parent->cid;
// Root is the top of the thread
$attributes['reply_root_uri'] = $post->reply->root->uri;
$attributes['reply_root_cid'] = $post->reply->root->cid;
}
return $attributes;
};
Facets (Rich Text)#
Posts with mentions, links, and hashtags use facets:
use SocialDept\AtpSchema\Generated\App\Bsky\Feed\Post;
use SocialDept\AtpSchema\Generated\App\Bsky\Richtext\Facet;
$toAttributes = function(Post $post): array {
$attributes = [
'content' => $post->text,
'published_at' => $post->createdAt,
];
// Extract mentions, links, and tags from facets
$mentions = [];
$links = [];
$tags = [];
foreach ($post->facets ?? [] as $facet) {
foreach ($facet->features as $feature) {
$type = $feature->getType();
match ($type) {
'app.bsky.richtext.facet#mention' => $mentions[] = $feature->did,
'app.bsky.richtext.facet#link' => $links[] = $feature->uri,
'app.bsky.richtext.facet#tag' => $tags[] = $feature->tag,
default => null,
};
}
}
$attributes['mentions'] = $mentions;
$attributes['links'] = $links;
$attributes['tags'] = $tags;
$attributes['facets'] = $post->facets; // Store raw for reconstruction
return $attributes;
};
Type Safety Benefits#
Using atp-schema classes provides:
- IDE Autocompletion - Full property and method suggestions
- Type Checking - Static analysis catches errors
- Validation - Data is validated on construction
- Documentation - Generated classes include docblocks
// IDE knows $post->text is string, $post->createdAt is string, etc.
$toAttributes = function(Post $post): array {
return [
'content' => $post->text, // string
'published_at' => $post->createdAt, // string (ISO 8601)
'langs' => $post->langs, // ?array
'facets' => $post->facets, // ?array
];
};
Regenerating Schema Classes#
When the AT Protocol schema updates, regenerate the classes:
# In the atp-schema package
php artisan atp:generate
Your mappers will automatically work with the updated types.