Maintain local ⭤ remote in sync with automatic AT Protocol parity for Laravel (alpha & unstable)
1# Model Traits
2
3Parity provides two traits to add AT Protocol awareness to your Eloquent models.
4
5## HasAtpRecord
6
7The base trait for models that store AT Protocol record references.
8
9### Setup
10
11```php
12<?php
13
14namespace App\Models;
15
16use Illuminate\Database\Eloquent\Model;
17use SocialDept\AtpParity\Concerns\HasAtpRecord;
18
19class Post extends Model
20{
21 use HasAtpRecord;
22
23 protected $fillable = [
24 'content',
25 'published_at',
26 'atp_uri',
27 'atp_cid',
28 ];
29}
30```
31
32### Database Migration
33
34```php
35Schema::create('posts', function (Blueprint $table) {
36 $table->id();
37 $table->text('content');
38 $table->timestamp('published_at');
39 $table->string('atp_uri')->nullable()->unique();
40 $table->string('atp_cid')->nullable();
41 $table->timestamps();
42});
43```
44
45### Available Methods
46
47#### `getAtpUri(): ?string`
48
49Returns the stored AT Protocol URI.
50
51```php
52$post->getAtpUri();
53// "at://did:plc:abc123/app.bsky.feed.post/xyz789"
54```
55
56#### `getAtpCid(): ?string`
57
58Returns the stored content identifier.
59
60```php
61$post->getAtpCid();
62// "bafyreib2rxk3rjnlvzj..."
63```
64
65#### `getAtpDid(): ?string`
66
67Extracts the DID from the URI.
68
69```php
70$post->getAtpDid();
71// "did:plc:abc123"
72```
73
74#### `getAtpCollection(): ?string`
75
76Extracts the collection (lexicon NSID) from the URI.
77
78```php
79$post->getAtpCollection();
80// "app.bsky.feed.post"
81```
82
83#### `getAtpRkey(): ?string`
84
85Extracts the record key from the URI.
86
87```php
88$post->getAtpRkey();
89// "xyz789"
90```
91
92#### `hasAtpRecord(): bool`
93
94Checks if the model has been synced to AT Protocol.
95
96```php
97if ($post->hasAtpRecord()) {
98 // Model exists on AT Protocol
99}
100```
101
102#### `getAtpMapper(): ?RecordMapper`
103
104Gets the registered mapper for this model class.
105
106```php
107$mapper = $post->getAtpMapper();
108```
109
110#### `toAtpRecord(): ?Data`
111
112Converts the model to an AT Protocol record DTO.
113
114```php
115$record = $post->toAtpRecord();
116$data = $record->toArray(); // Ready for API calls
117```
118
119### Query Scopes
120
121#### `scopeWithAtpRecord($query)`
122
123Query only models that have been synced.
124
125```php
126$syncedPosts = Post::withAtpRecord()->get();
127```
128
129#### `scopeWithoutAtpRecord($query)`
130
131Query only models that have NOT been synced.
132
133```php
134$localOnlyPosts = Post::withoutAtpRecord()->get();
135```
136
137#### `scopeWhereAtpUri($query, string $uri)`
138
139Find a model by its AT Protocol URI.
140
141```php
142$post = Post::whereAtpUri('at://did:plc:xxx/app.bsky.feed.post/abc')->first();
143```
144
145## SyncsWithAtp
146
147Extended trait for bidirectional synchronization tracking. Includes all `HasAtpRecord` functionality plus sync timestamps and conflict detection.
148
149### Setup
150
151```php
152<?php
153
154namespace App\Models;
155
156use Illuminate\Database\Eloquent\Model;
157use SocialDept\AtpParity\Concerns\SyncsWithAtp;
158
159class Post extends Model
160{
161 use SyncsWithAtp;
162
163 protected $fillable = [
164 'content',
165 'published_at',
166 'atp_uri',
167 'atp_cid',
168 'atp_synced_at',
169 ];
170
171 protected $casts = [
172 'published_at' => 'datetime',
173 'atp_synced_at' => 'datetime',
174 ];
175}
176```
177
178### Database Migration
179
180```php
181Schema::create('posts', function (Blueprint $table) {
182 $table->id();
183 $table->text('content');
184 $table->timestamp('published_at');
185 $table->string('atp_uri')->nullable()->unique();
186 $table->string('atp_cid')->nullable();
187 $table->timestamp('atp_synced_at')->nullable();
188 $table->timestamps();
189});
190```
191
192### Additional Methods
193
194#### `getAtpSyncedAtColumn(): string`
195
196Returns the column name for the sync timestamp. Override to customize.
197
198```php
199public function getAtpSyncedAtColumn(): string
200{
201 return 'last_synced_at'; // Default: 'atp_synced_at'
202}
203```
204
205#### `getAtpSyncedAt(): ?DateTimeInterface`
206
207Returns when the model was last synced.
208
209```php
210$syncedAt = $post->getAtpSyncedAt();
211// Carbon instance or null
212```
213
214#### `markAsSynced(string $uri, string $cid): void`
215
216Marks the model as synced with the given metadata. Does not save.
217
218```php
219$post->markAsSynced($uri, $cid);
220$post->save();
221```
222
223#### `hasLocalChanges(): bool`
224
225Checks if the model has been modified since the last sync.
226
227```php
228if ($post->hasLocalChanges()) {
229 // Local changes exist that haven't been pushed
230}
231```
232
233This compares `updated_at` with `atp_synced_at`.
234
235#### `updateFromRecord(Data $record, string $uri, string $cid): void`
236
237Updates the model from a remote record. Does not save.
238
239```php
240$post->updateFromRecord($record, $uri, $cid);
241$post->save();
242```
243
244## Practical Examples
245
246### Checking Sync Status
247
248```php
249$post = Post::find(1);
250
251if (!$post->hasAtpRecord()) {
252 echo "Not yet published to AT Protocol";
253} elseif ($post->hasLocalChanges()) {
254 echo "Has unpushed local changes";
255} else {
256 echo "In sync with AT Protocol";
257}
258```
259
260### Finding Related Records
261
262```php
263// Get all posts from the same author
264$authorDid = $post->getAtpDid();
265$authorPosts = Post::withAtpRecord()
266 ->get()
267 ->filter(fn($p) => $p->getAtpDid() === $authorDid);
268```
269
270### Building an AT Protocol URL
271
272```php
273$post = Post::find(1);
274
275if ($post->hasAtpRecord()) {
276 $bskyUrl = sprintf(
277 'https://bsky.app/profile/%s/post/%s',
278 $post->getAtpDid(),
279 $post->getAtpRkey()
280 );
281}
282```
283
284### Sync Status Dashboard
285
286```php
287// Get sync statistics
288$stats = [
289 'total' => Post::count(),
290 'synced' => Post::withAtpRecord()->count(),
291 'pending' => Post::withoutAtpRecord()->count(),
292 'with_changes' => Post::withAtpRecord()
293 ->get()
294 ->filter(fn($p) => $p->hasLocalChanges())
295 ->count(),
296];
297```
298
299## Custom Column Names
300
301Both traits respect the global column configuration:
302
303```php
304// config/parity.php
305return [
306 'columns' => [
307 'uri' => 'at_protocol_uri',
308 'cid' => 'at_protocol_cid',
309 ],
310];
311```
312
313For the sync timestamp column, override the method in your model:
314
315```php
316class Post extends Model
317{
318 use SyncsWithAtp;
319
320 public function getAtpSyncedAtColumn(): string
321 {
322 return 'last_synced_at';
323 }
324}
325```
326
327## Event Hooks
328
329The `SyncsWithAtp` trait includes a boot method you can extend:
330
331```php
332class Post extends Model
333{
334 use SyncsWithAtp;
335
336 protected static function bootSyncsWithAtp(): void
337 {
338 parent::bootSyncsWithAtp();
339
340 static::updating(function ($model) {
341 // Custom logic before updates
342 });
343 }
344}
345```
346
347## Combining with Other Traits
348
349The traits work alongside other Eloquent features:
350
351```php
352use Illuminate\Database\Eloquent\Model;
353use Illuminate\Database\Eloquent\SoftDeletes;
354use SocialDept\AtpParity\Concerns\SyncsWithAtp;
355
356class Post extends Model
357{
358 use SoftDeletes;
359 use SyncsWithAtp;
360
361 // Both traits work together
362}
363```