Parse and validate AT Protocol Lexicons with DTO generation for Laravel
1<?php
2
3namespace SocialDept\AtpSchema\Generator;
4
5class ModelMapper
6{
7 /**
8 * Naming converter instance.
9 */
10 protected NamingConverter $naming;
11
12 /**
13 * Type mapper instance.
14 */
15 protected TypeMapper $typeMapper;
16
17 /**
18 * Create a new ModelMapper.
19 */
20 public function __construct(?NamingConverter $naming = null, ?TypeMapper $typeMapper = null)
21 {
22 $this->naming = $naming ?? new NamingConverter();
23 $this->typeMapper = $typeMapper ?? new TypeMapper($this->naming);
24 }
25
26 /**
27 * Generate toModel method body.
28 *
29 * @param array<string, array<string, mixed>> $properties
30 */
31 public function generateToModelBody(array $properties, string $modelClass = 'Model'): string
32 {
33 if (empty($properties)) {
34 return " return new {$modelClass}();";
35 }
36
37 $lines = [];
38 $lines[] = " return new {$modelClass}([";
39
40 foreach ($properties as $name => $definition) {
41 $mapping = $this->generatePropertyToModel($name, $definition);
42 $lines[] = " '{$name}' => {$mapping},";
43 }
44
45 $lines[] = ' ]);';
46
47 return implode("\n", $lines);
48 }
49
50 /**
51 * Generate fromModel method body.
52 *
53 * @param array<string, array<string, mixed>> $properties
54 */
55 public function generateFromModelBody(array $properties): string
56 {
57 if (empty($properties)) {
58 return ' return new static();';
59 }
60
61 $lines = [];
62 $lines[] = ' return new static(';
63
64 foreach ($properties as $name => $definition) {
65 $mapping = $this->generatePropertyFromModel($name, $definition);
66 $lines[] = " {$name}: {$mapping},";
67 }
68
69 // Remove trailing comma from last line
70 $lastIndex = count($lines) - 1;
71 $lines[$lastIndex] = rtrim($lines[$lastIndex], ',');
72
73 $lines[] = ' );';
74
75 return implode("\n", $lines);
76 }
77
78 /**
79 * Generate property mapping to model.
80 *
81 * @param array<string, mixed> $definition
82 */
83 protected function generatePropertyToModel(string $name, array $definition): string
84 {
85 $type = $definition['type'] ?? 'unknown';
86
87 // Handle DateTime types
88 if ($type === 'string' && isset($definition['format']) && $definition['format'] === 'datetime') {
89 return "\$this->{$name}?->format('Y-m-d H:i:s')";
90 }
91
92 // Handle blob types
93 if ($type === 'blob') {
94 return "\$this->{$name}?->toArray()";
95 }
96
97 // Handle nested refs
98 if ($type === 'ref') {
99 return "\$this->{$name}?->toArray()";
100 }
101
102 // Handle arrays of refs
103 if ($type === 'array' && isset($definition['items']['type']) && $definition['items']['type'] === 'ref') {
104 return "array_map(fn (\$item) => \$item->toArray(), \$this->{$name} ?? [])";
105 }
106
107 // Handle arrays of objects
108 if ($type === 'array' && isset($definition['items']['type']) && $definition['items']['type'] === 'object') {
109 return "\$this->{$name} ?? []";
110 }
111
112 // Simple property
113 return "\$this->{$name}";
114 }
115
116 /**
117 * Generate property mapping from model.
118 *
119 * @param array<string, mixed> $definition
120 */
121 protected function generatePropertyFromModel(string $name, array $definition): string
122 {
123 $type = $definition['type'] ?? 'unknown';
124
125 // Handle DateTime types
126 if ($type === 'string' && isset($definition['format']) && $definition['format'] === 'datetime') {
127 return "\$model->{$name} ? new \\DateTime(\$model->{$name}) : null";
128 }
129
130 // Handle blob types
131 if ($type === 'blob') {
132 return "\$model->{$name} ? \\SocialDept\\AtpSchema\\Data\\BlobReference::fromArray(\$model->{$name}) : null";
133 }
134
135 // Handle nested refs
136 if ($type === 'ref' && isset($definition['ref'])) {
137 $refClass = $this->naming->nsidToClassName($definition['ref']);
138 $className = basename(str_replace('\\', '/', $refClass));
139
140 return "\$model->{$name} ? {$className}::fromArray(\$model->{$name}) : null";
141 }
142
143 // Handle arrays of refs
144 if ($type === 'array' && isset($definition['items']['type']) && $definition['items']['type'] === 'ref') {
145 $refClass = $this->naming->nsidToClassName($definition['items']['ref']);
146 $className = basename(str_replace('\\', '/', $refClass));
147
148 return "\$model->{$name} ? array_map(fn (\$item) => {$className}::fromArray(\$item), \$model->{$name}) : []";
149 }
150
151 // Simple property with null coalescing
152 return "\$model->{$name} ?? null";
153 }
154
155 /**
156 * Get field mapping configuration.
157 *
158 * @param array<string, array<string, mixed>> $properties
159 * @return array<string, string>
160 */
161 public function getFieldMapping(array $properties): array
162 {
163 $mapping = [];
164
165 foreach ($properties as $name => $definition) {
166 // Convert camelCase to snake_case for database columns
167 $mapping[$name] = $this->naming->toSnakeCase($name);
168 }
169
170 return $mapping;
171 }
172
173 /**
174 * Check if property needs special handling.
175 *
176 * @param array<string, mixed> $definition
177 */
178 public function needsTransformer(array $definition): bool
179 {
180 $type = $definition['type'] ?? 'unknown';
181
182 if ($type === 'blob') {
183 return true;
184 }
185
186 if ($type === 'ref') {
187 return true;
188 }
189
190 if ($type === 'string' && isset($definition['format']) && $definition['format'] === 'datetime') {
191 return true;
192 }
193
194 if ($type === 'array' && isset($definition['items']['type'])) {
195 $itemType = $definition['items']['type'];
196 if (in_array($itemType, ['ref', 'object'])) {
197 return true;
198 }
199 }
200
201 return false;
202 }
203
204 /**
205 * Get transformer type for property.
206 *
207 * @param array<string, mixed> $definition
208 */
209 public function getTransformerType(array $definition): ?string
210 {
211 $type = $definition['type'] ?? 'unknown';
212
213 if ($type === 'string' && isset($definition['format']) && $definition['format'] === 'datetime') {
214 return 'datetime';
215 }
216
217 if ($type === 'blob') {
218 return 'blob';
219 }
220
221 if ($type === 'ref') {
222 return 'ref';
223 }
224
225 if ($type === 'array' && isset($definition['items']['type'])) {
226 $itemType = $definition['items']['type'];
227 if ($itemType === 'ref') {
228 return 'array_ref';
229 }
230 if ($itemType === 'object') {
231 return 'array_object';
232 }
233 }
234
235 return null;
236 }
237}