Maintain local ⭤ remote in sync with automatic AT Protocol parity for Laravel (alpha & unstable)
at dev 3.5 kB view raw
1<?php 2 3namespace SocialDept\AtpParity\Sync; 4 5use Illuminate\Database\Eloquent\Model; 6use SocialDept\AtpParity\Contracts\RecordMapper; 7use SocialDept\AtpParity\Events\ConflictDetected; 8use SocialDept\AtpSchema\Data\Data; 9 10/** 11 * Resolves conflicts between local and remote record versions. 12 */ 13class ConflictResolver 14{ 15 /** 16 * Resolve a conflict according to the specified strategy. 17 */ 18 public function resolve( 19 Model $model, 20 Data $record, 21 array $meta, 22 RecordMapper $mapper, 23 ConflictStrategy $strategy 24 ): ConflictResolution { 25 return match ($strategy) { 26 ConflictStrategy::RemoteWins => $this->applyRemote($model, $record, $meta, $mapper), 27 ConflictStrategy::LocalWins => $this->keepLocal($model), 28 ConflictStrategy::NewestWins => $this->compareAndApply($model, $record, $meta, $mapper), 29 ConflictStrategy::Manual => $this->flagForReview($model, $record, $meta, $mapper), 30 }; 31 } 32 33 /** 34 * Apply the remote version, overwriting local changes. 35 */ 36 protected function applyRemote( 37 Model $model, 38 Data $record, 39 array $meta, 40 RecordMapper $mapper 41 ): ConflictResolution { 42 $mapper->updateModel($model, $record, $meta); 43 $model->save(); 44 45 return ConflictResolution::remoteWins($model); 46 } 47 48 /** 49 * Keep the local version, ignoring remote changes. 50 */ 51 protected function keepLocal(Model $model): ConflictResolution 52 { 53 return ConflictResolution::localWins($model); 54 } 55 56 /** 57 * Compare timestamps and apply the newest version. 58 */ 59 protected function compareAndApply( 60 Model $model, 61 Data $record, 62 array $meta, 63 RecordMapper $mapper 64 ): ConflictResolution { 65 $localUpdatedAt = $model->getAttribute('updated_at'); 66 67 // Try to get remote timestamp from record 68 $remoteCreatedAt = $record->createdAt ?? null; 69 70 // If we can't compare, default to remote wins 71 if (! $localUpdatedAt || ! $remoteCreatedAt) { 72 return $this->applyRemote($model, $record, $meta, $mapper); 73 } 74 75 // Compare timestamps 76 if ($localUpdatedAt > $remoteCreatedAt) { 77 return $this->keepLocal($model); 78 } 79 80 return $this->applyRemote($model, $record, $meta, $mapper); 81 } 82 83 /** 84 * Flag the conflict for manual review. 85 */ 86 protected function flagForReview( 87 Model $model, 88 Data $record, 89 array $meta, 90 RecordMapper $mapper 91 ): ConflictResolution { 92 // Create a pending conflict record 93 $conflict = PendingConflict::create([ 94 'model_type' => get_class($model), 95 'model_id' => $model->getKey(), 96 'uri' => $meta['uri'] ?? null, 97 'local_data' => $model->toArray(), 98 'remote_data' => $this->buildRemoteData($record, $meta, $mapper), 99 'status' => 'pending', 100 ]); 101 102 // Dispatch event for notification 103 event(new ConflictDetected($model, $record, $meta, $conflict)); 104 105 return ConflictResolution::pending($conflict); 106 } 107 108 /** 109 * Build the remote data array for storage. 110 */ 111 protected function buildRemoteData(Data $record, array $meta, RecordMapper $mapper): array 112 { 113 // Create a temporary model with the remote data 114 $tempModel = $mapper->toModel($record, $meta); 115 116 return $tempModel->toArray(); 117 } 118}