1 | <?php |
||
2 | |||
3 | namespace Cerbero\LaravelDto\Console; |
||
4 | |||
5 | use Illuminate\Support\Facades\Artisan; |
||
6 | use Illuminate\Support\Facades\Schema; |
||
7 | use Illuminate\Support\Str; |
||
8 | use ReflectionClass; |
||
9 | use ReflectionMethod; |
||
10 | |||
11 | /** |
||
12 | * The model properties mapper. |
||
13 | * |
||
14 | */ |
||
15 | class ModelPropertiesMapper |
||
16 | { |
||
17 | /** |
||
18 | * The cached model file. |
||
19 | * |
||
20 | * @var array |
||
21 | */ |
||
22 | protected $cachedFile; |
||
23 | |||
24 | /** |
||
25 | * The map between schema and PHP types. |
||
26 | * |
||
27 | * @var array |
||
28 | */ |
||
29 | protected $schemaTypesMap = [ |
||
30 | 'guid' => 'string', |
||
31 | 'boolean' => 'bool', |
||
32 | 'datetime' => 'Carbon\Carbon', |
||
33 | 'string' => 'string', |
||
34 | 'json' => 'string', |
||
35 | 'integer' => 'int', |
||
36 | 'date' => 'Carbon\Carbon', |
||
37 | 'smallint' => 'int', |
||
38 | 'text' => 'string', |
||
39 | 'decimal' => 'float', |
||
40 | 'bigint' => 'int', |
||
41 | ]; |
||
42 | |||
43 | /** |
||
44 | * The map determining whether a relation involves many models. |
||
45 | * |
||
46 | * @var array |
||
47 | */ |
||
48 | protected $relationsMap = [ |
||
49 | 'hasOne' => false, |
||
50 | 'morphOne' => false, |
||
51 | 'belongsTo' => false, |
||
52 | 'morphTo' => false, |
||
53 | 'hasMany' => true, |
||
54 | 'hasManyThrough' => true, |
||
55 | 'morphMany' => true, |
||
56 | 'belongsToMany' => true, |
||
57 | 'morphToMany' => true, |
||
58 | 'morphedByMany' => true, |
||
59 | ]; |
||
60 | |||
61 | /** |
||
62 | * The manifest. |
||
63 | * |
||
64 | * @var Manifest |
||
65 | */ |
||
66 | protected $manifest; |
||
67 | |||
68 | /** |
||
69 | * The DTO qualifier. |
||
70 | * |
||
71 | * @var DtoQualifierContract |
||
72 | */ |
||
73 | protected $qualifier; |
||
74 | |||
75 | /** |
||
76 | * Instantiate the class. |
||
77 | * |
||
78 | * @param Manifest $manifest |
||
79 | * @param DtoQualifierContract $qualifier |
||
80 | */ |
||
81 | 3 | public function __construct(Manifest $manifest, DtoQualifierContract $qualifier) |
|
82 | { |
||
83 | 3 | $this->manifest = $manifest; |
|
84 | 3 | $this->qualifier = $qualifier; |
|
85 | 3 | } |
|
86 | |||
87 | /** |
||
88 | * Retrieve the properties map of the given data to generate |
||
89 | * |
||
90 | * @param DtoGenerationData $data |
||
91 | * @return array |
||
92 | */ |
||
93 | 2 | public function map(DtoGenerationData $data): array |
|
94 | { |
||
95 | 2 | $propertiesFromDatabase = $this->mapPropertiesFromDatabase($data); |
|
96 | 2 | $propertiesFromRelations = $this->mapPropertiesFromRelations($data); |
|
97 | |||
98 | 2 | return $propertiesFromDatabase + $propertiesFromRelations; |
|
99 | } |
||
100 | |||
101 | /** |
||
102 | * Retrieve the given model properties from the database |
||
103 | * |
||
104 | * @param DtoGenerationData $data |
||
105 | * @return array |
||
106 | */ |
||
107 | 2 | public function mapPropertiesFromDatabase(DtoGenerationData $data): array |
|
108 | { |
||
109 | 2 | $properties = []; |
|
110 | 2 | $table = $data->model->getTable(); |
|
111 | 2 | $connection = $data->model->getConnection(); |
|
112 | |||
113 | 2 | foreach (Schema::getColumnListing($table) as $column) { |
|
114 | 2 | $camelColumn = Str::camel($column); |
|
115 | 2 | $rawType = Schema::getColumnType($table, $column); |
|
116 | 2 | $types = [$this->schemaTypesMap[$rawType]]; |
|
117 | |||
118 | 2 | if (!$connection->getDoctrineColumn($table, $column)->getNotnull()) { |
|
119 | 2 | $types[] = 'null'; |
|
120 | } |
||
121 | |||
122 | 2 | $properties[$camelColumn] = $types; |
|
123 | } |
||
124 | |||
125 | 2 | return $properties; |
|
126 | } |
||
127 | |||
128 | /** |
||
129 | * Retrieve the given model properties from its relations |
||
130 | * |
||
131 | * @param DtoGenerationData $data |
||
132 | * @return array |
||
133 | */ |
||
134 | 3 | public function mapPropertiesFromRelations(DtoGenerationData $data): array |
|
135 | { |
||
136 | 3 | $properties = []; |
|
137 | 3 | $relations = implode('|', array_keys($this->relationsMap)); |
|
138 | 3 | $reflection = new ReflectionClass($data->model); |
|
139 | 3 | $methods = $reflection->getMethods(ReflectionMethod::IS_PUBLIC); |
|
140 | |||
141 | 3 | foreach ($methods as $method) { |
|
142 | 3 | if ($method->getFileName() != $reflection->getFileName()) { |
|
143 | 3 | continue; |
|
144 | } |
||
145 | |||
146 | 3 | if (!preg_match("/\\\$this->($relations)\W+([\w\\\]+)/", $this->getMethodBody($method), $matches)) { |
|
147 | 2 | continue; |
|
148 | } |
||
149 | |||
150 | 3 | [, $relation, $relatedModel] = $matches; |
|
151 | |||
152 | 3 | if (!$qualifiedModel = $this->qualifyModel($relatedModel, $reflection)) { |
|
153 | 1 | continue; |
|
154 | } |
||
155 | |||
156 | 2 | $dto = $this->getDtoForModelOrGenerate($qualifiedModel, $data); |
|
157 | 2 | $type = $this->relationsMap[$relation] ? $dto . '[]' : $dto; |
|
158 | 2 | $properties += [$method->getName() => [$type]]; |
|
159 | } |
||
160 | |||
161 | 3 | return $properties; |
|
162 | } |
||
163 | |||
164 | /** |
||
165 | * Retrieve the body of the given method |
||
166 | * |
||
167 | * @param ReflectionMethod $method |
||
168 | * @return string |
||
169 | */ |
||
170 | 3 | protected function getMethodBody(ReflectionMethod $method): string |
|
171 | { |
||
172 | 3 | $file = $this->getFile($method->getFileName()); |
|
173 | 3 | $offset = $method->getStartLine(); |
|
174 | 3 | $length = $method->getEndLine() - $offset; |
|
175 | |||
176 | 3 | return implode('', array_slice($file, $offset, $length)); |
|
177 | } |
||
178 | |||
179 | /** |
||
180 | * Retrieve the given file as an array |
||
181 | * |
||
182 | * @param string $filename |
||
183 | * @return array |
||
184 | */ |
||
185 | 3 | protected function getFile(string $filename): array |
|
186 | { |
||
187 | 3 | if ($this->cachedFile) { |
|
0 ignored issues
–
show
|
|||
188 | 3 | return $this->cachedFile; |
|
189 | } |
||
190 | |||
191 | 3 | return $this->cachedFile = file($filename); |
|
192 | } |
||
193 | |||
194 | /** |
||
195 | * Retrieve the fully qualified class name of the given model |
||
196 | * |
||
197 | * @param string $model |
||
198 | * @param ReflectionClass $reflection |
||
199 | * @return string|null |
||
200 | */ |
||
201 | 3 | protected function qualifyModel(string $model, ReflectionClass $reflection): ?string |
|
202 | { |
||
203 | 3 | if (class_exists($model)) { |
|
204 | 1 | return $model; |
|
205 | } |
||
206 | |||
207 | 3 | $useStatements = $this->getUseStatements($reflection); |
|
208 | |||
209 | 3 | if (isset($useStatements[$model])) { |
|
210 | 1 | return $useStatements[$model]; |
|
211 | } |
||
212 | |||
213 | 3 | return class_exists($class = $reflection->getNamespaceName() . "\\{$model}") ? $class : null; |
|
214 | } |
||
215 | |||
216 | /** |
||
217 | * Retrieve the use statements of the given class |
||
218 | * |
||
219 | * @param ReflectionClass $reflection |
||
220 | * @return array |
||
221 | */ |
||
222 | 3 | protected function getUseStatements(ReflectionClass $reflection): array |
|
223 | { |
||
224 | 3 | $class = $reflection->getName(); |
|
225 | |||
226 | 3 | if ($useStatements = $this->manifest->getUseStatements($class)) { |
|
227 | 2 | return $useStatements; |
|
228 | } |
||
229 | |||
230 | 3 | $file = $this->getFile($reflection->getFileName()); |
|
231 | |||
232 | 3 | foreach ($file as $line) { |
|
233 | 3 | if (strpos($line, 'class') === 0) { |
|
234 | 3 | break; |
|
235 | 3 | } elseif (strpos($line, 'use') === 0) { |
|
236 | 3 | preg_match_all('/([\w\\\_]+)(?:\s+as\s+([\w_]+))?;/i', $line, $matches, PREG_SET_ORDER); |
|
237 | |||
238 | 3 | foreach ($matches as $match) { |
|
239 | 3 | $segments = explode('\\', $match[1]); |
|
240 | 3 | $name = $match[2] ?? end($segments); |
|
241 | 3 | $this->manifest->addUseStatement($class, $name, $match[1]); |
|
242 | } |
||
243 | } |
||
244 | } |
||
245 | |||
246 | 3 | return $this->manifest->save()->getUseStatements($class); |
|
247 | } |
||
248 | |||
249 | /** |
||
250 | * Retrieve the DTO class name for the given model |
||
251 | * |
||
252 | * @param string $model |
||
253 | * @param DtoGenerationData $data |
||
254 | * @return string |
||
255 | */ |
||
256 | 2 | protected function getDtoForModelOrGenerate(string $model, DtoGenerationData $data): string |
|
257 | { |
||
258 | 2 | if ($dto = $this->manifest->getDto($model)) { |
|
259 | 2 | return $dto; |
|
260 | } |
||
261 | |||
262 | 2 | $dto = $this->qualifier->qualify($model); |
|
263 | |||
264 | 2 | if ($this->shouldGenerateNestedDto($dto, $data->forced)) { |
|
265 | 2 | Artisan::call('make:dto', [ |
|
266 | 2 | 'name' => str_replace('\\', '/', $model), |
|
267 | 2 | '--force' => $data->forced, |
|
268 | 2 | ], $data->output); |
|
269 | |||
270 | 2 | $this->manifest->finishGeneratingDto()->save(); |
|
271 | } |
||
272 | |||
273 | 2 | return $this->manifest->addDto($model, $dto)->save()->getDto($model); |
|
274 | } |
||
275 | |||
276 | /** |
||
277 | * Determine whether the given nested DTO should be generated |
||
278 | * |
||
279 | * @param string $dto |
||
280 | * @param bool $forced |
||
281 | * @return bool |
||
282 | */ |
||
283 | 2 | protected function shouldGenerateNestedDto(string $dto, bool $forced): bool |
|
284 | { |
||
285 | 2 | if ($this->manifest->isStartingDto($dto) || $this->manifest->generating($dto)) { |
|
286 | 2 | return false; |
|
287 | } |
||
288 | |||
289 | 2 | return $forced || !class_exists($dto); |
|
290 | } |
||
291 | } |
||
292 |
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.