Maintain local ⭤ remote in sync with automatic AT Protocol parity for Laravel (alpha & unstable)
at dev 426 lines 11 kB view raw view rendered
1[![Parity Header](./header.png)](https://github.com/socialdept/atp-parity) 2 3<h3 align="center"> 4 Bidirectional mapping between AT Protocol records and Laravel Eloquent models. 5</h3> 6 7<p align="center"> 8 <br> 9 <a href="https://packagist.org/packages/socialdept/atp-parity" title="Latest Version on Packagist"><img src="https://img.shields.io/packagist/v/socialdept/atp-parity.svg?style=flat-square"></a> 10 <a href="https://packagist.org/packages/socialdept/atp-parity" title="Total Downloads"><img src="https://img.shields.io/packagist/dt/socialdept/atp-parity.svg?style=flat-square"></a> 11 <a href="https://github.com/socialdept/atp-parity/actions/workflows/tests.yml" title="GitHub Tests Action Status"><img src="https://img.shields.io/github/actions/workflow/status/socialdept/atp-parity/tests.yml?branch=main&label=tests&style=flat-square"></a> 12 <a href="LICENSE" title="Software License"><img src="https://img.shields.io/github/license/socialdept/atp-parity?style=flat-square"></a> 13</p> 14 15--- 16 17## What is Parity? 18 19**Parity** is a Laravel package that bridges your Eloquent models with AT Protocol records. It provides bidirectional mapping, automatic firehose synchronization, and type-safe transformations between your database and the decentralized social web. 20 21Think of it as Laravel's model casts, but for AT Protocol records. 22 23## Why use Parity? 24 25- **Laravel-style code** - Familiar patterns you already know 26- **Bidirectional mapping** - Transform records to models and back 27- **Firehose sync** - Automatically sync network events to your database 28- **Type-safe DTOs** - Full integration with atp-schema generated types 29- **Model traits** - Add AT Protocol awareness to any Eloquent model 30- **Flexible mappers** - Define custom transformations for your domain 31 32## Quick Example 33 34```php 35use SocialDept\AtpParity\RecordMapper; 36use SocialDept\AtpSchema\Data\Data; 37use Illuminate\Database\Eloquent\Model; 38 39class PostMapper extends RecordMapper 40{ 41 public function recordClass(): string 42 { 43 return \SocialDept\AtpSchema\Generated\App\Bsky\Feed\Post::class; 44 } 45 46 public function modelClass(): string 47 { 48 return \App\Models\Post::class; 49 } 50 51 protected function recordToAttributes(Data $record): array 52 { 53 return [ 54 'content' => $record->text, 55 'published_at' => $record->createdAt, 56 ]; 57 } 58 59 protected function modelToRecordData(Model $model): array 60 { 61 return [ 62 'text' => $model->content, 63 'createdAt' => $model->published_at->toIso8601String(), 64 ]; 65 } 66} 67``` 68 69## Installation 70 71```bash 72composer require socialdept/atp-parity 73``` 74 75Optionally publish the configuration: 76 77```bash 78php artisan vendor:publish --tag=parity-config 79``` 80 81## Getting Started 82 83Once installed, you're three steps away from syncing AT Protocol records: 84 85### 1. Create a Mapper 86 87Define how your record maps to your model: 88 89```php 90class PostMapper extends RecordMapper 91{ 92 public function recordClass(): string 93 { 94 return Post::class; // Your atp-schema DTO or custom Record 95 } 96 97 public function modelClass(): string 98 { 99 return \App\Models\Post::class; 100 } 101 102 protected function recordToAttributes(Data $record): array 103 { 104 return ['content' => $record->text]; 105 } 106 107 protected function modelToRecordData(Model $model): array 108 { 109 return ['text' => $model->content]; 110 } 111} 112``` 113 114### 2. Register Your Mapper 115 116```php 117// config/parity.php 118return [ 119 'mappers' => [ 120 App\AtpMappers\PostMapper::class, 121 ], 122]; 123``` 124 125### 3. Add the Trait to Your Model 126 127```php 128use SocialDept\AtpParity\Concerns\HasAtpRecord; 129 130class Post extends Model 131{ 132 use HasAtpRecord; 133} 134``` 135 136Your model can now convert to/from AT Protocol records and query by URI. 137 138## What can you build? 139 140- **Data mirrors** - Keep local copies of AT Protocol data 141- **AppViews** - Build custom applications with synced data 142- **Analytics platforms** - Store and analyze network activity 143- **Content aggregators** - Collect and organize posts locally 144- **Moderation tools** - Track and manage content in your database 145- **Hybrid applications** - Combine local and federated data 146 147## Ecosystem Integration 148 149Parity is designed to work seamlessly with the other atp-* packages: 150 151| Package | Integration | 152|---------|-------------| 153| **atp-schema** | Records extend `Data`, use generated DTOs directly | 154| **atp-client** | `RecordHelper` for fetching and hydrating records | 155| **atp-signals** | `ParitySignal` for automatic firehose sync | 156 157### Using with atp-schema 158 159Use generated schema classes directly with `SchemaMapper`: 160 161```php 162use SocialDept\AtpSchema\Generated\App\Bsky\Feed\Post; 163use SocialDept\AtpParity\Support\SchemaMapper; 164 165$mapper = new SchemaMapper( 166 schemaClass: Post::class, 167 modelClass: \App\Models\Post::class, 168 toAttributes: fn(Post $p) => [ 169 'content' => $p->text, 170 'published_at' => $p->createdAt, 171 ], 172 toRecordData: fn($m) => [ 173 'text' => $m->content, 174 'createdAt' => $m->published_at->toIso8601String(), 175 ], 176); 177 178$registry->register($mapper); 179``` 180 181### Using with atp-client 182 183Fetch records by URI and convert directly to models: 184 185```php 186use SocialDept\AtpParity\Support\RecordHelper; 187 188$helper = app(RecordHelper::class); 189 190// Fetch as typed DTO 191$record = $helper->fetch('at://did:plc:xxx/app.bsky.feed.post/abc123'); 192 193// Fetch and convert to model (unsaved) 194$post = $helper->fetchAsModel('at://did:plc:xxx/app.bsky.feed.post/abc123'); 195 196// Fetch and sync to database (upsert) 197$post = $helper->sync('at://did:plc:xxx/app.bsky.feed.post/abc123'); 198``` 199 200The helper automatically resolves the DID to find the correct PDS endpoint, so it works with any AT Protocol server - not just Bluesky. 201 202### Using with atp-signals 203 204Enable automatic firehose synchronization by registering the `ParitySignal`: 205 206```php 207// config/signal.php 208return [ 209 'signals' => [ 210 \SocialDept\AtpParity\Signals\ParitySignal::class, 211 ], 212]; 213``` 214 215Run `php artisan signal:consume` and your models will automatically sync with matching firehose events. 216 217### Importing Historical Data 218 219For existing records created before you started consuming the firehose: 220 221```bash 222# Import a user's records 223php artisan parity:import did:plc:z72i7hdynmk6r22z27h6tvur 224 225# Check import status 226php artisan parity:import-status 227``` 228 229Or programmatically: 230 231```php 232use SocialDept\AtpParity\Import\ImportService; 233 234$service = app(ImportService::class); 235$result = $service->importUser('did:plc:z72i7hdynmk6r22z27h6tvur'); 236 237echo "Synced {$result->recordsSynced} records"; 238``` 239 240## Documentation 241 242For detailed documentation on specific topics: 243 244- [Record Mappers](docs/mappers.md) - Creating and using mappers 245- [Model Traits](docs/traits.md) - HasAtpRecord and SyncsWithAtp 246- [atp-schema Integration](docs/atp-schema-integration.md) - Using generated DTOs 247- [atp-client Integration](docs/atp-client-integration.md) - RecordHelper and fetching 248- [atp-signals Integration](docs/atp-signals-integration.md) - ParitySignal and firehose sync 249- [Importing](docs/importing.md) - Syncing historical data 250 251## Model Traits 252 253### HasAtpRecord 254 255Add AT Protocol awareness to your models: 256 257```php 258use SocialDept\AtpParity\Concerns\HasAtpRecord; 259 260class Post extends Model 261{ 262 use HasAtpRecord; 263 264 protected $fillable = ['content', 'atp_uri', 'atp_cid']; 265} 266``` 267 268Available methods: 269 270```php 271// Get AT Protocol metadata 272$post->getAtpUri(); // at://did:plc:xxx/app.bsky.feed.post/rkey 273$post->getAtpCid(); // bafyre... 274$post->getAtpDid(); // did:plc:xxx (extracted from URI) 275$post->getAtpCollection(); // app.bsky.feed.post (extracted from URI) 276$post->getAtpRkey(); // rkey (extracted from URI) 277 278// Check sync status 279$post->hasAtpRecord(); // true if synced 280 281// Convert to record DTO 282$record = $post->toAtpRecord(); 283 284// Query scopes 285Post::withAtpRecord()->get(); // Only synced posts 286Post::withoutAtpRecord()->get(); // Only unsynced posts 287Post::whereAtpUri($uri)->first(); // Find by URI 288``` 289 290### SyncsWithAtp 291 292Extended trait for bidirectional sync tracking: 293 294```php 295use SocialDept\AtpParity\Concerns\SyncsWithAtp; 296 297class Post extends Model 298{ 299 use SyncsWithAtp; 300} 301``` 302 303Additional methods: 304 305```php 306// Track sync status 307$post->getAtpSyncedAt(); // Last sync timestamp 308$post->hasLocalChanges(); // True if updated since last sync 309 310// Mark as synced 311$post->markAsSynced($uri, $cid); 312 313// Update from remote 314$post->updateFromRecord($record, $uri, $cid); 315``` 316 317## Database Migration 318 319Add AT Protocol columns to your models: 320 321```php 322Schema::table('posts', function (Blueprint $table) { 323 $table->string('atp_uri')->nullable()->unique(); 324 $table->string('atp_cid')->nullable(); 325 $table->timestamp('atp_synced_at')->nullable(); // For SyncsWithAtp 326}); 327``` 328 329## Configuration 330 331```php 332// config/parity.php 333return [ 334 // Registered mappers 335 'mappers' => [ 336 App\AtpMappers\PostMapper::class, 337 App\AtpMappers\ProfileMapper::class, 338 ], 339 340 // Column names for AT Protocol metadata 341 'columns' => [ 342 'uri' => 'atp_uri', 343 'cid' => 'atp_cid', 344 ], 345]; 346``` 347 348## Creating Custom Records 349 350Extend the `Record` base class for custom AT Protocol records: 351 352```php 353use SocialDept\AtpParity\Data\Record; 354use Carbon\Carbon; 355 356class PostRecord extends Record 357{ 358 public function __construct( 359 public readonly string $text, 360 public readonly Carbon $createdAt, 361 public readonly ?array $facets = null, 362 ) {} 363 364 public static function getLexicon(): string 365 { 366 return 'app.bsky.feed.post'; 367 } 368 369 public static function fromArray(array $data): static 370 { 371 return new static( 372 text: $data['text'], 373 createdAt: Carbon::parse($data['createdAt']), 374 facets: $data['facets'] ?? null, 375 ); 376 } 377} 378``` 379 380The `Record` class extends `atp-schema`'s `Data` and implements `atp-client`'s `Recordable` interface, ensuring full compatibility with the ecosystem. 381 382## Requirements 383 384- PHP 8.2+ 385- Laravel 10, 11, or 12 386- [socialdept/atp-schema](https://github.com/socialdept/atp-schema) ^0.3 387- [socialdept/atp-client](https://github.com/socialdept/atp-client) ^0.0 388- [socialdept/atp-resolver](https://github.com/socialdept/atp-resolver) ^1.1 389- [socialdept/atp-signals](https://github.com/socialdept/atp-signals) ^1.1 390 391## Testing 392 393```bash 394composer test 395``` 396 397## Resources 398 399- [AT Protocol Documentation](https://atproto.com/) 400- [Bluesky API Docs](https://docs.bsky.app/) 401- [atp-schema](https://github.com/socialdept/atp-schema) - Generated AT Protocol DTOs 402- [atp-client](https://github.com/socialdept/atp-client) - AT Protocol HTTP client 403- [atp-signals](https://github.com/socialdept/atp-signals) - Firehose event consumer 404 405## Support & Contributing 406 407Found a bug or have a feature request? [Open an issue](https://github.com/socialdept/atp-parity/issues). 408 409Want to contribute? Check out the [contribution guidelines](contributing.md). 410 411## Changelog 412 413Please see [changelog](changelog.md) for recent changes. 414 415## Credits 416 417- [Miguel Batres](https://batres.co) - founder & lead maintainer 418- [All contributors](https://github.com/socialdept/atp-parity/graphs/contributors) 419 420## License 421 422Parity is open-source software licensed under the [MIT license](license.md). 423 424--- 425 426**Built for the Federation** - By Social Dept.