mapPropertiesFromRelations()   A
last analyzed

Complexity

Conditions 6
Paths 6

Size

Total Lines 28
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 16
c 1
b 0
f 0
nc 6
nop 1
dl 0
loc 28
ccs 17
cts 17
cp 1
crap 6
rs 9.1111
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
Bug Best Practice introduced by
The expression $this->cachedFile of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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.

Loading history...
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