+82
src/Contracts/RecordMapper.php
+82
src/Contracts/RecordMapper.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\AtpParity\Contracts;
4
+
5
+
use Illuminate\Database\Eloquent\Model;
6
+
use SocialDept\AtpSchema\Data\Data;
7
+
8
+
/**
9
+
* Contract for bidirectional mapping between Record DTOs and Eloquent models.
10
+
*
11
+
* @template TRecord of Data
12
+
* @template TModel of Model
13
+
*/
14
+
interface RecordMapper
15
+
{
16
+
/**
17
+
* Get the Record class this mapper handles.
18
+
*
19
+
* @return class-string<TRecord>
20
+
*/
21
+
public function recordClass(): string;
22
+
23
+
/**
24
+
* Get the Model class this mapper handles.
25
+
*
26
+
* @return class-string<TModel>
27
+
*/
28
+
public function modelClass(): string;
29
+
30
+
/**
31
+
* Get the lexicon NSID this mapper handles.
32
+
*/
33
+
public function lexicon(): string;
34
+
35
+
/**
36
+
* Convert a Record DTO to an Eloquent Model.
37
+
*
38
+
* @param TRecord $record
39
+
* @param array{uri?: string, cid?: string, did?: string, rkey?: string} $meta AT Protocol metadata
40
+
* @return TModel
41
+
*/
42
+
public function toModel(Data $record, array $meta = []): Model;
43
+
44
+
/**
45
+
* Convert an Eloquent Model to a Record DTO.
46
+
*
47
+
* @param TModel $model
48
+
* @return TRecord
49
+
*/
50
+
public function toRecord(Model $model): Data;
51
+
52
+
/**
53
+
* Update an existing model with data from a record.
54
+
*
55
+
* @param TModel $model
56
+
* @param TRecord $record
57
+
* @param array{uri?: string, cid?: string, did?: string, rkey?: string} $meta
58
+
* @return TModel
59
+
*/
60
+
public function updateModel(Model $model, Data $record, array $meta = []): Model;
61
+
62
+
/**
63
+
* Find or create model from record.
64
+
*
65
+
* @param TRecord $record
66
+
* @param array{uri?: string, cid?: string, did?: string, rkey?: string} $meta
67
+
* @return TModel
68
+
*/
69
+
public function upsert(Data $record, array $meta = []): Model;
70
+
71
+
/**
72
+
* Find model by AT Protocol URI.
73
+
*
74
+
* @return TModel|null
75
+
*/
76
+
public function findByUri(string $uri): ?Model;
77
+
78
+
/**
79
+
* Delete model by AT Protocol URI.
80
+
*/
81
+
public function deleteByUri(string $uri): bool;
82
+
}
+25
src/Data/Record.php
+25
src/Data/Record.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\AtpParity\Data;
4
+
5
+
use SocialDept\AtpClient\Contracts\Recordable;
6
+
use SocialDept\AtpSchema\Data\Data;
7
+
8
+
/**
9
+
* Base class for custom AT Protocol records.
10
+
*
11
+
* Extends atp-schema's Data for full compatibility with the ecosystem,
12
+
* including union type support, validation, equality, and hashing.
13
+
*
14
+
* Implements Recordable for seamless atp-client integration.
15
+
*/
16
+
abstract class Record extends Data implements Recordable
17
+
{
18
+
/**
19
+
* Get the record type (alias for getLexicon for Recordable interface).
20
+
*/
21
+
public function getType(): string
22
+
{
23
+
return static::getLexicon();
24
+
}
25
+
}
+93
src/MapperRegistry.php
+93
src/MapperRegistry.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\AtpParity;
4
+
5
+
use Illuminate\Database\Eloquent\Model;
6
+
use SocialDept\AtpParity\Contracts\RecordMapper;
7
+
use SocialDept\AtpSchema\Data\Data;
8
+
9
+
/**
10
+
* Registry for RecordMapper instances.
11
+
*
12
+
* Allows looking up mappers by Record class, Model class, or lexicon NSID.
13
+
*/
14
+
class MapperRegistry
15
+
{
16
+
/** @var array<class-string<Data>, RecordMapper> */
17
+
protected array $byRecord = [];
18
+
19
+
/** @var array<class-string<Model>, RecordMapper> */
20
+
protected array $byModel = [];
21
+
22
+
/** @var array<string, RecordMapper> Keyed by NSID */
23
+
protected array $byLexicon = [];
24
+
25
+
/**
26
+
* Register a mapper.
27
+
*/
28
+
public function register(RecordMapper $mapper): void
29
+
{
30
+
$recordClass = $mapper->recordClass();
31
+
$modelClass = $mapper->modelClass();
32
+
33
+
$this->byRecord[$recordClass] = $mapper;
34
+
$this->byModel[$modelClass] = $mapper;
35
+
$this->byLexicon[$mapper->lexicon()] = $mapper;
36
+
}
37
+
38
+
/**
39
+
* Get a mapper by Record class.
40
+
*
41
+
* @param class-string<Data> $recordClass
42
+
*/
43
+
public function forRecord(string $recordClass): ?RecordMapper
44
+
{
45
+
return $this->byRecord[$recordClass] ?? null;
46
+
}
47
+
48
+
/**
49
+
* Get a mapper by Model class.
50
+
*
51
+
* @param class-string<Model> $modelClass
52
+
*/
53
+
public function forModel(string $modelClass): ?RecordMapper
54
+
{
55
+
return $this->byModel[$modelClass] ?? null;
56
+
}
57
+
58
+
/**
59
+
* Get a mapper by lexicon NSID.
60
+
*/
61
+
public function forLexicon(string $nsid): ?RecordMapper
62
+
{
63
+
return $this->byLexicon[$nsid] ?? null;
64
+
}
65
+
66
+
/**
67
+
* Check if a mapper exists for the given lexicon.
68
+
*/
69
+
public function hasLexicon(string $nsid): bool
70
+
{
71
+
return isset($this->byLexicon[$nsid]);
72
+
}
73
+
74
+
/**
75
+
* Get all registered lexicon NSIDs.
76
+
*
77
+
* @return array<string>
78
+
*/
79
+
public function lexicons(): array
80
+
{
81
+
return array_keys($this->byLexicon);
82
+
}
83
+
84
+
/**
85
+
* Get all registered mappers.
86
+
*
87
+
* @return array<RecordMapper>
88
+
*/
89
+
public function all(): array
90
+
{
91
+
return array_values($this->byLexicon);
92
+
}
93
+
}
+154
src/RecordMapper.php
+154
src/RecordMapper.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\AtpParity;
4
+
5
+
use Illuminate\Database\Eloquent\Model;
6
+
use SocialDept\AtpParity\Contracts\RecordMapper as RecordMapperContract;
7
+
use SocialDept\AtpSchema\Data\Data;
8
+
9
+
/**
10
+
* Abstract base class for bidirectional Record <-> Model mapping.
11
+
*
12
+
* @template TRecord of Data
13
+
* @template TModel of Model
14
+
*
15
+
* @implements RecordMapperContract<TRecord, TModel>
16
+
*/
17
+
abstract class RecordMapper implements RecordMapperContract
18
+
{
19
+
/**
20
+
* Get the Record class this mapper handles.
21
+
*
22
+
* @return class-string<TRecord>
23
+
*/
24
+
abstract public function recordClass(): string;
25
+
26
+
/**
27
+
* Get the Model class this mapper handles.
28
+
*
29
+
* @return class-string<TModel>
30
+
*/
31
+
abstract public function modelClass(): string;
32
+
33
+
/**
34
+
* Map record properties to model attributes.
35
+
*
36
+
* @param TRecord $record
37
+
* @return array<string, mixed>
38
+
*/
39
+
abstract protected function recordToAttributes(Data $record): array;
40
+
41
+
/**
42
+
* Map model attributes to record properties.
43
+
*
44
+
* @param TModel $model
45
+
* @return array<string, mixed>
46
+
*/
47
+
abstract protected function modelToRecordData(Model $model): array;
48
+
49
+
/**
50
+
* Get the lexicon NSID this mapper handles.
51
+
*/
52
+
public function lexicon(): string
53
+
{
54
+
$recordClass = $this->recordClass();
55
+
56
+
return $recordClass::getLexicon();
57
+
}
58
+
59
+
/**
60
+
* Get the column name for storing the AT Protocol URI.
61
+
*/
62
+
protected function uriColumn(): string
63
+
{
64
+
return config('parity.columns.uri', 'atp_uri');
65
+
}
66
+
67
+
/**
68
+
* Get the column name for storing the AT Protocol CID.
69
+
*/
70
+
protected function cidColumn(): string
71
+
{
72
+
return config('parity.columns.cid', 'atp_cid');
73
+
}
74
+
75
+
public function toModel(Data $record, array $meta = []): Model
76
+
{
77
+
$modelClass = $this->modelClass();
78
+
$attributes = $this->recordToAttributes($record);
79
+
$attributes = $this->applyMeta($attributes, $meta);
80
+
81
+
return new $modelClass($attributes);
82
+
}
83
+
84
+
public function toRecord(Model $model): Data
85
+
{
86
+
$recordClass = $this->recordClass();
87
+
88
+
return $recordClass::fromArray($this->modelToRecordData($model));
89
+
}
90
+
91
+
public function updateModel(Model $model, Data $record, array $meta = []): Model
92
+
{
93
+
$attributes = $this->recordToAttributes($record);
94
+
$attributes = $this->applyMeta($attributes, $meta);
95
+
$model->fill($attributes);
96
+
97
+
return $model;
98
+
}
99
+
100
+
public function findByUri(string $uri): ?Model
101
+
{
102
+
$modelClass = $this->modelClass();
103
+
104
+
return $modelClass::where($this->uriColumn(), $uri)->first();
105
+
}
106
+
107
+
public function upsert(Data $record, array $meta = []): Model
108
+
{
109
+
$uri = $meta['uri'] ?? null;
110
+
111
+
if ($uri) {
112
+
$existing = $this->findByUri($uri);
113
+
114
+
if ($existing) {
115
+
$this->updateModel($existing, $record, $meta);
116
+
$existing->save();
117
+
118
+
return $existing;
119
+
}
120
+
}
121
+
122
+
$model = $this->toModel($record, $meta);
123
+
$model->save();
124
+
125
+
return $model;
126
+
}
127
+
128
+
public function deleteByUri(string $uri): bool
129
+
{
130
+
$model = $this->findByUri($uri);
131
+
132
+
if ($model) {
133
+
return (bool) $model->delete();
134
+
}
135
+
136
+
return false;
137
+
}
138
+
139
+
/**
140
+
* Apply AT Protocol metadata to attributes.
141
+
*/
142
+
protected function applyMeta(array $attributes, array $meta): array
143
+
{
144
+
if (isset($meta['uri'])) {
145
+
$attributes[$this->uriColumn()] = $meta['uri'];
146
+
}
147
+
148
+
if (isset($meta['cid'])) {
149
+
$attributes[$this->cidColumn()] = $meta['cid'];
150
+
}
151
+
152
+
return $attributes;
153
+
}
154
+
}