Parse and validate AT Protocol Lexicons with DTO generation for Laravel
at dev 5.4 kB view raw
1<?php 2 3namespace SocialDept\AtpSchema\Data; 4 5use Illuminate\Contracts\Support\Arrayable; 6use Illuminate\Contracts\Support\Jsonable; 7use JsonSerializable; 8use SocialDept\AtpSchema\Contracts\DiscriminatedUnion; 9use Stringable; 10 11abstract class Data implements Arrayable, DiscriminatedUnion, Jsonable, JsonSerializable, Stringable 12{ 13 /** 14 * Get the lexicon NSID for this data type. 15 */ 16 abstract public static function getLexicon(): string; 17 18 /** 19 * Get the lexicon NSID that identifies this union variant. 20 * 21 * This is an alias for getLexicon() to satisfy the DiscriminatedUnion contract. 22 */ 23 public static function getDiscriminator(): string 24 { 25 return static::getLexicon(); 26 } 27 28 /** 29 * Convert the data to an array. 30 */ 31 public function toArray(): array 32 { 33 $result = []; 34 35 foreach (get_object_vars($this) as $property => $value) { 36 // Skip null values to exclude optional fields that aren't set 37 if ($value !== null) { 38 $result[$property] = $this->serializeValue($value); 39 } 40 } 41 42 return array_filter($result); 43 } 44 45 /** 46 * Convert the data to JSON. 47 */ 48 public function toJson($options = 0): string 49 { 50 return json_encode($this->jsonSerialize(), $options); 51 } 52 53 /** 54 * Convert the data for JSON serialization. 55 */ 56 public function jsonSerialize(): mixed 57 { 58 return $this->toArray(); 59 } 60 61 /** 62 * Convert the data to a string. 63 */ 64 public function __toString(): string 65 { 66 return $this->toJson(); 67 } 68 69 /** 70 * Serialize a value for output. 71 */ 72 protected function serializeValue(mixed $value): mixed 73 { 74 // Union variants must include $type for discrimination 75 if ($value instanceof self) { 76 return $value->toRecord(); 77 } 78 79 if ($value instanceof Arrayable) { 80 return $value->toArray(); 81 } 82 83 // Preserve arrays with $type (open union data) 84 if (is_array($value)) { 85 return array_map(fn ($item) => $this->serializeValue($item), $value); 86 } 87 88 if ($value instanceof \DateTimeInterface) { 89 return $value->format(\DateTimeInterface::ATOM); 90 } 91 92 return $value; 93 } 94 95 /** 96 * Create an instance from an array. 97 */ 98 abstract public static function fromArray(array $data): static; 99 100 /** 101 * Create an instance from JSON. 102 */ 103 public static function fromJson(string $json): static 104 { 105 $data = json_decode($json, true); 106 107 if (json_last_error() !== JSON_ERROR_NONE) { 108 throw new \InvalidArgumentException('Invalid JSON: '.json_last_error_msg()); 109 } 110 111 return static::fromArray($data); 112 } 113 114 /** 115 * Create an instance from an AT Protocol record. 116 * 117 * This is an alias for fromArray for semantic clarity 118 * when working with AT Protocol records. 119 */ 120 public static function fromRecord(array $record): static 121 { 122 return static::fromArray($record['value'] ?? $record); 123 } 124 125 /** 126 * Convert to an AT Protocol record. 127 * 128 * This is an alias for toArray for semantic clarity 129 * when working with AT Protocol records. 130 */ 131 public function toRecord(): array 132 { 133 return [ 134 ...$this->toArray(), 135 '$type' => $this->getLexicon(), 136 ]; 137 } 138 139 /** 140 * Check if two data objects are equal. 141 */ 142 public function equals(self $other): bool 143 { 144 if (! $other instanceof static) { 145 return false; 146 } 147 148 return $this->toArray() === $other->toArray(); 149 } 150 151 /** 152 * Get a hash of the data. 153 */ 154 public function hash(): string 155 { 156 return hash('sha256', $this->toJson()); 157 } 158 159 /** 160 * Validate the data against its lexicon schema. 161 */ 162 public function validate(): bool 163 { 164 if (! function_exists('schema_validate')) { 165 return true; 166 } 167 168 try { 169 return schema_validate(static::getLexicon(), $this->toArray()); 170 } catch (\Throwable) { 171 return true; 172 } 173 } 174 175 /** 176 * Validate and get errors. 177 * 178 * @return array<string, array<string>> 179 */ 180 public function validateWithErrors(): array 181 { 182 if (! function_exists('SocialDept\AtpSchema\schema')) { 183 return []; 184 } 185 186 try { 187 return schema()->validateWithErrors(static::getLexicon(), $this->toArray()); 188 } catch (\Throwable) { 189 return []; 190 } 191 } 192 193 /** 194 * Get a property value dynamically. 195 */ 196 public function __get(string $name): mixed 197 { 198 if (property_exists($this, $name)) { 199 return $this->$name; 200 } 201 202 throw new \InvalidArgumentException("Property {$name} does not exist on ".static::class); 203 } 204 205 /** 206 * Check if a property exists. 207 */ 208 public function __isset(string $name): bool 209 { 210 return property_exists($this, $name); 211 } 212 213 /** 214 * Clone the data with modified properties. 215 */ 216 public function with(array $properties): static 217 { 218 $data = $this->toArray(); 219 220 foreach ($properties as $key => $value) { 221 $data[$key] = $value; 222 } 223 224 return static::fromArray($data); 225 } 226}