+55
src/Publish/PublishResult.php
+55
src/Publish/PublishResult.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\AtpParity\Publish;
4
+
5
+
/**
6
+
* Immutable value object representing the result of a publish operation.
7
+
*/
8
+
readonly class PublishResult
9
+
{
10
+
public function __construct(
11
+
public bool $success,
12
+
public ?string $uri = null,
13
+
public ?string $cid = null,
14
+
public ?string $error = null,
15
+
) {}
16
+
17
+
/**
18
+
* Check if the publish operation succeeded.
19
+
*/
20
+
public function isSuccess(): bool
21
+
{
22
+
return $this->success;
23
+
}
24
+
25
+
/**
26
+
* Check if the publish operation failed.
27
+
*/
28
+
public function isFailed(): bool
29
+
{
30
+
return ! $this->success;
31
+
}
32
+
33
+
/**
34
+
* Create a successful result.
35
+
*/
36
+
public static function success(string $uri, string $cid): self
37
+
{
38
+
return new self(
39
+
success: true,
40
+
uri: $uri,
41
+
cid: $cid,
42
+
);
43
+
}
44
+
45
+
/**
46
+
* Create a failed result.
47
+
*/
48
+
public static function failed(string $error): self
49
+
{
50
+
return new self(
51
+
success: false,
52
+
error: $error,
53
+
);
54
+
}
55
+
}
+243
src/Publish/PublishService.php
+243
src/Publish/PublishService.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\AtpParity\Publish;
4
+
5
+
use Illuminate\Database\Eloquent\Model;
6
+
use SocialDept\AtpClient\Facades\Atp;
7
+
use SocialDept\AtpParity\Events\RecordPublished;
8
+
use SocialDept\AtpParity\Events\RecordUnpublished;
9
+
use SocialDept\AtpParity\MapperRegistry;
10
+
use Throwable;
11
+
12
+
/**
13
+
* Service for publishing Eloquent models to AT Protocol.
14
+
*/
15
+
class PublishService
16
+
{
17
+
public function __construct(
18
+
protected MapperRegistry $registry
19
+
) {}
20
+
21
+
/**
22
+
* Publish a model as a new record to AT Protocol.
23
+
*
24
+
* Requires the model to have a DID association (via did column or relationship).
25
+
*/
26
+
public function publish(Model $model): PublishResult
27
+
{
28
+
$did = $this->getDidFromModel($model);
29
+
30
+
if (! $did) {
31
+
return PublishResult::failed('No DID associated with model. Use publishAs() to specify a DID.');
32
+
}
33
+
34
+
return $this->publishAs($did, $model);
35
+
}
36
+
37
+
/**
38
+
* Publish a model as a specific user.
39
+
*/
40
+
public function publishAs(string $did, Model $model): PublishResult
41
+
{
42
+
$mapper = $this->registry->forModel(get_class($model));
43
+
44
+
if (! $mapper) {
45
+
return PublishResult::failed('No mapper registered for model: '.get_class($model));
46
+
}
47
+
48
+
// Check if already published
49
+
$existingUri = $this->getModelUri($model);
50
+
if ($existingUri) {
51
+
return $this->update($model);
52
+
}
53
+
54
+
try {
55
+
$record = $mapper->toRecord($model);
56
+
$collection = $mapper->lexicon();
57
+
58
+
$client = Atp::as($did);
59
+
$response = $client->atproto->repo->createRecord(
60
+
repo: $did,
61
+
collection: $collection,
62
+
record: $record->toArray(),
63
+
);
64
+
65
+
// Update model with ATP metadata
66
+
$this->updateModelMeta($model, $response->uri, $response->cid);
67
+
68
+
event(new RecordPublished($model, $response->uri, $response->cid));
69
+
70
+
return PublishResult::success($response->uri, $response->cid);
71
+
} catch (Throwable $e) {
72
+
return PublishResult::failed($e->getMessage());
73
+
}
74
+
}
75
+
76
+
/**
77
+
* Update an existing published record.
78
+
*/
79
+
public function update(Model $model): PublishResult
80
+
{
81
+
$uri = $this->getModelUri($model);
82
+
83
+
if (! $uri) {
84
+
return PublishResult::failed('Model has not been published yet. Use publish() first.');
85
+
}
86
+
87
+
$mapper = $this->registry->forModel(get_class($model));
88
+
89
+
if (! $mapper) {
90
+
return PublishResult::failed('No mapper registered for model: '.get_class($model));
91
+
}
92
+
93
+
$parts = $this->parseUri($uri);
94
+
95
+
if (! $parts) {
96
+
return PublishResult::failed('Invalid AT Protocol URI: '.$uri);
97
+
}
98
+
99
+
try {
100
+
$record = $mapper->toRecord($model);
101
+
102
+
$client = Atp::as($parts['did']);
103
+
$response = $client->atproto->repo->putRecord(
104
+
repo: $parts['did'],
105
+
collection: $parts['collection'],
106
+
rkey: $parts['rkey'],
107
+
record: $record->toArray(),
108
+
);
109
+
110
+
// Update model with new CID
111
+
$this->updateModelMeta($model, $response->uri, $response->cid);
112
+
113
+
event(new RecordPublished($model, $response->uri, $response->cid));
114
+
115
+
return PublishResult::success($response->uri, $response->cid);
116
+
} catch (Throwable $e) {
117
+
return PublishResult::failed($e->getMessage());
118
+
}
119
+
}
120
+
121
+
/**
122
+
* Delete a published record from AT Protocol.
123
+
*/
124
+
public function delete(Model $model): bool
125
+
{
126
+
$uri = $this->getModelUri($model);
127
+
128
+
if (! $uri) {
129
+
return false;
130
+
}
131
+
132
+
$parts = $this->parseUri($uri);
133
+
134
+
if (! $parts) {
135
+
return false;
136
+
}
137
+
138
+
try {
139
+
$client = Atp::as($parts['did']);
140
+
$client->atproto->repo->deleteRecord(
141
+
repo: $parts['did'],
142
+
collection: $parts['collection'],
143
+
rkey: $parts['rkey'],
144
+
);
145
+
146
+
// Clear ATP metadata from model
147
+
$this->clearModelMeta($model);
148
+
149
+
event(new RecordUnpublished($model, $uri));
150
+
151
+
return true;
152
+
} catch (Throwable $e) {
153
+
return false;
154
+
}
155
+
}
156
+
157
+
/**
158
+
* Get the DID from a model.
159
+
*
160
+
* Override this method or set a did column/relationship on your model.
161
+
*/
162
+
protected function getDidFromModel(Model $model): ?string
163
+
{
164
+
// Check for did column
165
+
if (isset($model->did)) {
166
+
return $model->did;
167
+
}
168
+
169
+
// Check for user relationship with did
170
+
if (method_exists($model, 'user') && $model->user?->did) {
171
+
return $model->user->did;
172
+
}
173
+
174
+
// Check for author relationship with did
175
+
if (method_exists($model, 'author') && $model->author?->did) {
176
+
return $model->author->did;
177
+
}
178
+
179
+
// Try extracting from existing URI
180
+
$uri = $this->getModelUri($model);
181
+
if ($uri) {
182
+
$parts = $this->parseUri($uri);
183
+
184
+
return $parts['did'] ?? null;
185
+
}
186
+
187
+
return null;
188
+
}
189
+
190
+
/**
191
+
* Get the AT Protocol URI from a model.
192
+
*/
193
+
protected function getModelUri(Model $model): ?string
194
+
{
195
+
$column = config('parity.columns.uri', 'atp_uri');
196
+
197
+
return $model->{$column};
198
+
}
199
+
200
+
/**
201
+
* Update model with AT Protocol metadata.
202
+
*/
203
+
protected function updateModelMeta(Model $model, string $uri, string $cid): void
204
+
{
205
+
$uriColumn = config('parity.columns.uri', 'atp_uri');
206
+
$cidColumn = config('parity.columns.cid', 'atp_cid');
207
+
208
+
$model->{$uriColumn} = $uri;
209
+
$model->{$cidColumn} = $cid;
210
+
$model->save();
211
+
}
212
+
213
+
/**
214
+
* Clear AT Protocol metadata from model.
215
+
*/
216
+
protected function clearModelMeta(Model $model): void
217
+
{
218
+
$uriColumn = config('parity.columns.uri', 'atp_uri');
219
+
$cidColumn = config('parity.columns.cid', 'atp_cid');
220
+
221
+
$model->{$uriColumn} = null;
222
+
$model->{$cidColumn} = null;
223
+
$model->save();
224
+
}
225
+
226
+
/**
227
+
* Parse an AT Protocol URI into its components.
228
+
*
229
+
* @return array{did: string, collection: string, rkey: string}|null
230
+
*/
231
+
protected function parseUri(string $uri): ?array
232
+
{
233
+
if (! preg_match('#^at://([^/]+)/([^/]+)/([^/]+)$#', $uri, $matches)) {
234
+
return null;
235
+
}
236
+
237
+
return [
238
+
'did' => $matches[1],
239
+
'collection' => $matches[2],
240
+
'rkey' => $matches[3],
241
+
];
242
+
}
243
+
}