Maintain local ⭤ remote in sync with automatic AT Protocol parity for Laravel (alpha & unstable)

Add core mapping system

+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
··· 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
··· 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
··· 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 + }