Maintain local ⭤ remote in sync with automatic AT Protocol parity for Laravel (alpha & unstable)
at main 355 lines 10 kB view raw view rendered
1# atp-schema Integration 2 3Parity 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. 4 5## How It Works 6 7The `SocialDept\AtpParity\Data\Record` class extends `SocialDept\AtpSchema\Data\Data`: 8 9```php 10namespace SocialDept\AtpParity\Data; 11 12use SocialDept\AtpClient\Contracts\Recordable; 13use SocialDept\AtpSchema\Data\Data; 14 15abstract class Record extends Data implements Recordable 16{ 17 public function getType(): string 18 { 19 return static::getLexicon(); 20 } 21} 22``` 23 24This means all Parity records inherit: 25 26- `getLexicon()` - Returns the lexicon NSID 27- `fromArray()` - Creates instance from array data 28- `toArray()` - Converts to array 29- `toRecord()` - Converts to record format for API calls 30- Type validation and casting 31 32## Using Generated Schema Classes 33 34atp-schema generates PHP classes for all AT Protocol lexicons. Use them directly with Parity: 35 36```php 37use SocialDept\AtpParity\Support\SchemaMapper; 38use SocialDept\AtpSchema\Generated\App\Bsky\Feed\Post; 39use SocialDept\AtpSchema\Generated\App\Bsky\Feed\Like; 40use SocialDept\AtpSchema\Generated\App\Bsky\Graph\Follow; 41 42// Post mapper 43$postMapper = new SchemaMapper( 44 schemaClass: Post::class, 45 modelClass: \App\Models\Post::class, 46 toAttributes: fn(Post $post) => [ 47 'content' => $post->text, 48 'published_at' => $post->createdAt, 49 'langs' => $post->langs, 50 'reply_parent' => $post->reply?->parent->uri, 51 'reply_root' => $post->reply?->root->uri, 52 ], 53 toRecordData: fn($model) => [ 54 'text' => $model->content, 55 'createdAt' => $model->published_at->toIso8601String(), 56 'langs' => $model->langs ?? ['en'], 57 ], 58); 59 60// Like mapper 61$likeMapper = new SchemaMapper( 62 schemaClass: Like::class, 63 modelClass: \App\Models\Like::class, 64 toAttributes: fn(Like $like) => [ 65 'subject_uri' => $like->subject->uri, 66 'subject_cid' => $like->subject->cid, 67 'liked_at' => $like->createdAt, 68 ], 69 toRecordData: fn($model) => [ 70 'subject' => [ 71 'uri' => $model->subject_uri, 72 'cid' => $model->subject_cid, 73 ], 74 'createdAt' => $model->liked_at->toIso8601String(), 75 ], 76); 77 78// Follow mapper 79$followMapper = new SchemaMapper( 80 schemaClass: Follow::class, 81 modelClass: \App\Models\Follow::class, 82 toAttributes: fn(Follow $follow) => [ 83 'subject_did' => $follow->subject, 84 'followed_at' => $follow->createdAt, 85 ], 86 toRecordData: fn($model) => [ 87 'subject' => $model->subject_did, 88 'createdAt' => $model->followed_at->toIso8601String(), 89 ], 90); 91``` 92 93## Creating Custom Records 94 95For custom lexicons or when you need more control, extend the `Record` class: 96 97```php 98<?php 99 100namespace App\AtpRecords; 101 102use Carbon\Carbon; 103use SocialDept\AtpParity\Data\Record; 104 105class CustomPost extends Record 106{ 107 public function __construct( 108 public readonly string $text, 109 public readonly Carbon $createdAt, 110 public readonly ?array $facets = null, 111 public readonly ?array $embed = null, 112 public readonly ?array $langs = null, 113 ) {} 114 115 public static function getLexicon(): string 116 { 117 return 'app.bsky.feed.post'; 118 } 119 120 public static function fromArray(array $data): static 121 { 122 return new static( 123 text: $data['text'], 124 createdAt: Carbon::parse($data['createdAt']), 125 facets: $data['facets'] ?? null, 126 embed: $data['embed'] ?? null, 127 langs: $data['langs'] ?? null, 128 ); 129 } 130 131 public function toArray(): array 132 { 133 return array_filter([ 134 '$type' => static::getLexicon(), 135 'text' => $this->text, 136 'createdAt' => $this->createdAt->toIso8601String(), 137 'facets' => $this->facets, 138 'embed' => $this->embed, 139 'langs' => $this->langs, 140 ], fn($v) => $v !== null); 141 } 142} 143``` 144 145## Custom Lexicons (AppView) 146 147Building a custom AT Protocol application? Define your own lexicons: 148 149```php 150<?php 151 152namespace App\AtpRecords; 153 154use Carbon\Carbon; 155use SocialDept\AtpParity\Data\Record; 156 157class Article extends Record 158{ 159 public function __construct( 160 public readonly string $title, 161 public readonly string $body, 162 public readonly Carbon $publishedAt, 163 public readonly ?array $tags = null, 164 public readonly ?string $coverImage = null, 165 ) {} 166 167 public static function getLexicon(): string 168 { 169 return 'com.myapp.blog.article'; // Your custom NSID 170 } 171 172 public static function fromArray(array $data): static 173 { 174 return new static( 175 title: $data['title'], 176 body: $data['body'], 177 publishedAt: Carbon::parse($data['publishedAt']), 178 tags: $data['tags'] ?? null, 179 coverImage: $data['coverImage'] ?? null, 180 ); 181 } 182 183 public function toArray(): array 184 { 185 return array_filter([ 186 '$type' => static::getLexicon(), 187 'title' => $this->title, 188 'body' => $this->body, 189 'publishedAt' => $this->publishedAt->toIso8601String(), 190 'tags' => $this->tags, 191 'coverImage' => $this->coverImage, 192 ], fn($v) => $v !== null); 193 } 194} 195``` 196 197## Working with Embedded Types 198 199atp-schema generates classes for embedded types. Use them in your mappings: 200 201```php 202use SocialDept\AtpSchema\Generated\App\Bsky\Feed\Post; 203use SocialDept\AtpSchema\Generated\App\Bsky\Embed\Images; 204use SocialDept\AtpSchema\Generated\App\Bsky\Embed\External; 205use SocialDept\AtpSchema\Generated\Com\Atproto\Repo\StrongRef; 206 207$mapper = new SchemaMapper( 208 schemaClass: Post::class, 209 modelClass: \App\Models\Post::class, 210 toAttributes: fn(Post $post) => [ 211 'content' => $post->text, 212 'published_at' => $post->createdAt, 213 'has_images' => $post->embed instanceof Images, 214 'has_link' => $post->embed instanceof External, 215 'embed_data' => $post->embed?->toArray(), 216 ], 217 toRecordData: fn($model) => [ 218 'text' => $model->content, 219 'createdAt' => $model->published_at->toIso8601String(), 220 ], 221); 222``` 223 224## Handling Union Types 225 226AT Protocol uses union types for fields like `embed`. atp-schema handles these via discriminated unions: 227 228```php 229use SocialDept\AtpSchema\Generated\App\Bsky\Feed\Post; 230use SocialDept\AtpSchema\Generated\App\Bsky\Embed\Images; 231use SocialDept\AtpSchema\Generated\App\Bsky\Embed\External; 232use SocialDept\AtpSchema\Generated\App\Bsky\Embed\Record; 233use SocialDept\AtpSchema\Generated\App\Bsky\Embed\RecordWithMedia; 234 235$toAttributes = function(Post $post): array { 236 $attributes = [ 237 'content' => $post->text, 238 'published_at' => $post->createdAt, 239 ]; 240 241 // Handle embed union type 242 if ($post->embed) { 243 match (true) { 244 $post->embed instanceof Images => $attributes['embed_type'] = 'images', 245 $post->embed instanceof External => $attributes['embed_type'] = 'external', 246 $post->embed instanceof Record => $attributes['embed_type'] = 'quote', 247 $post->embed instanceof RecordWithMedia => $attributes['embed_type'] = 'quote_media', 248 default => $attributes['embed_type'] = 'unknown', 249 }; 250 $attributes['embed_data'] = $post->embed->toArray(); 251 } 252 253 return $attributes; 254}; 255``` 256 257## Reply Threading 258 259Posts can be replies to other posts: 260 261```php 262use SocialDept\AtpSchema\Generated\App\Bsky\Feed\Post; 263 264$toAttributes = function(Post $post): array { 265 $attributes = [ 266 'content' => $post->text, 267 'published_at' => $post->createdAt, 268 'is_reply' => $post->reply !== null, 269 ]; 270 271 if ($post->reply) { 272 // Parent is the immediate post being replied to 273 $attributes['reply_parent_uri'] = $post->reply->parent->uri; 274 $attributes['reply_parent_cid'] = $post->reply->parent->cid; 275 276 // Root is the top of the thread 277 $attributes['reply_root_uri'] = $post->reply->root->uri; 278 $attributes['reply_root_cid'] = $post->reply->root->cid; 279 } 280 281 return $attributes; 282}; 283``` 284 285## Facets (Rich Text) 286 287Posts with mentions, links, and hashtags use facets: 288 289```php 290use SocialDept\AtpSchema\Generated\App\Bsky\Feed\Post; 291use SocialDept\AtpSchema\Generated\App\Bsky\Richtext\Facet; 292 293$toAttributes = function(Post $post): array { 294 $attributes = [ 295 'content' => $post->text, 296 'published_at' => $post->createdAt, 297 ]; 298 299 // Extract mentions, links, and tags from facets 300 $mentions = []; 301 $links = []; 302 $tags = []; 303 304 foreach ($post->facets ?? [] as $facet) { 305 foreach ($facet->features as $feature) { 306 $type = $feature->getType(); 307 match ($type) { 308 'app.bsky.richtext.facet#mention' => $mentions[] = $feature->did, 309 'app.bsky.richtext.facet#link' => $links[] = $feature->uri, 310 'app.bsky.richtext.facet#tag' => $tags[] = $feature->tag, 311 default => null, 312 }; 313 } 314 } 315 316 $attributes['mentions'] = $mentions; 317 $attributes['links'] = $links; 318 $attributes['tags'] = $tags; 319 $attributes['facets'] = $post->facets; // Store raw for reconstruction 320 321 return $attributes; 322}; 323``` 324 325## Type Safety Benefits 326 327Using atp-schema classes provides: 328 3291. **IDE Autocompletion** - Full property and method suggestions 3302. **Type Checking** - Static analysis catches errors 3313. **Validation** - Data is validated on construction 3324. **Documentation** - Generated classes include docblocks 333 334```php 335// IDE knows $post->text is string, $post->createdAt is string, etc. 336$toAttributes = function(Post $post): array { 337 return [ 338 'content' => $post->text, // string 339 'published_at' => $post->createdAt, // string (ISO 8601) 340 'langs' => $post->langs, // ?array 341 'facets' => $post->facets, // ?array 342 ]; 343}; 344``` 345 346## Regenerating Schema Classes 347 348When the AT Protocol schema updates, regenerate the classes: 349 350```bash 351# In the atp-schema package 352php artisan atp:generate 353``` 354 355Your mappers will automatically work with the updated types.