ModelMapper   A
last analyzed

Complexity

Total Complexity 24

Size/Duplication

Total Lines 237
Duplicated Lines 0 %

Test Coverage

Coverage 66.67%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 84
dl 0
loc 237
ccs 50
cts 75
cp 0.6667
rs 10
c 3
b 0
f 0
wmc 24

10 Methods

Rating   Name   Duplication   Size   Complexity  
A getModelClass() 0 3 1
A getResourceType() 0 18 4
A getModel() 0 3 1
A getDefaultProperties() 0 3 1
A getResourceTypeByReturnType() 0 21 4
A __construct() 0 3 1
A getResourceTypeByMethodBody() 0 16 4
A getIncludes() 0 19 2
A getResourceTypeByReturnAnnotation() 0 23 4
A getDeclaredProperties() 0 10 2
1
<?php
2
3
namespace Rexlabs\Laravel\Smokescreen\Console;
4
5
use Illuminate\Database\Eloquent\Model;
6
use Illuminate\Support\Facades\Schema;
7
use Illuminate\Support\Str;
8
use ReflectionClass;
9
use ReflectionMethod;
10
use ReflectionNamedType;
11
12
/**
13
 * The includes listing.
14
 */
15
class ModelMapper
16
{
17
    /**
18
     * Maps eloquent relationship methods to smokescreen resource types.
19
     *
20
     * @var array
21
     */
22
    protected $relationsMap = [
23
        'HasOne'         => 'item',
24
        'MorphOne'       => 'item',
25
        'BelongsTo'      => 'item',
26
        'MorphTo'        => 'item',
27
        'HasMany'        => 'collection',
28
        'HasManyThrough' => 'collection',
29
        'MorphMany'      => 'collection',
30
        'BelongsToMany'  => 'collection',
31
        'MorphToMany'    => 'collection',
32
        'MorphedByMany'  => 'collection',
33
    ];
34
35
    /**
36
     * Maps schema field types to smokescreen property types.
37
     *
38
     * @var array
39
     */
40
    protected $schemaTypesMap = [
41
        'guid'     => 'string',
42
        'boolean'  => 'boolean',
43
        'datetime' => 'datetime',
44
        'string'   => 'string',
45
        'json'     => 'array',
46
        'integer'  => 'integer',
47
        'date'     => 'date',
48
        'smallint' => 'integer',
49
        'text'     => 'string',
50
        'decimal'  => 'float',
51
        'bigint'   => 'integer',
52
    ];
53
54
    /**
55
     * @var Model
56
     */
57
    protected $model;
58
59 2
    public function __construct(Model $model)
60
    {
61 2
        $this->model = $model;
62
    }
63
64
    /**
65
     * List the includes of the given Eloquent model.
66
     *
67
     * @throws \ReflectionException
68
     *
69
     * @return array
70
     */
71 1
    public function getIncludes(): array
72
    {
73 1
        $includes = [];
74 1
        collect((new ReflectionClass($this->getModel()))->getMethods(ReflectionMethod::IS_PUBLIC))
75 1
            ->filter(
76 1
                function (ReflectionMethod $method) {
77
                    // We're not interested in inherited methods
78 1
                    return $method->getDeclaringClass()->getName() === $this->getModelClass();
79 1
                }
80 1
            )->each(
81 1
                function (ReflectionMethod $method) use (&$includes) {
82
                    // Only include if we can resolve a resource type
83 1
                    if (($type = $this->getResourceType($method)) !== null) {
84 1
                        $includes[$method->getName()] = "relation|{$type}";
85
                    }
86 1
                }
87 1
            );
88
89 1
        return $includes;
90
    }
91
92
    /**
93
     * List the declared properties of the given Eloquent model.
94
     *
95
     * @return array
96
     */
97
    public function getDeclaredProperties(): array
98
    {
99
        $props = [];
100
        $table = $this->getModel()->getTable();
101
        foreach (Schema::getColumnListing($table) as $column) {
102
            $type = Schema::getColumnType($table, $column);
103
            $props[$column] = $this->schemaTypesMap[$type] ?? null;
104
        }
105
106
        return $props;
107
    }
108
109
    /**
110
     * Get the default properties.
111
     *
112
     * @return array
113
     */
114 1
    public function getDefaultProperties(): array
115
    {
116 1
        return [];
117
    }
118
119
    /**
120
     * @return Model
121
     */
122 1
    public function getModel(): Model
123
    {
124 1
        return $this->model;
125
    }
126
127
    /**
128
     * Return the model class.
129
     *
130
     * @return string
131
     */
132 1
    protected function getModelClass()
133
    {
134 1
        return \get_class($this->model);
135
    }
136
137
    /**
138
     * Get the resource type (item or collection) based on the return signature
139
     * of the given method.
140
     *
141
     * @param ReflectionMethod $method
142
     *
143
     * @return string|null
144
     */
145 1
    protected function getResourceType(ReflectionMethod $method)
146
    {
147
        // Try to get the type by type-hint.
148 1
        if ($type = $this->getResourceTypeByReturnType($method)) {
149 1
            return $type;
150
        }
151
152
        // Or, try to get the type by the @return annotation.
153 1
        if ($type = $this->getResourceTypeByReturnAnnotation($method)) {
154
            return $type;
155
        }
156
157
        // Or, try to get the type by the method body.
158 1
        if ($type = $this->getResourceTypeByMethodBody($method)) {
159 1
            return $type;
160
        }
161
162
        return null;
163
    }
164
165
    /**
166
     * Retrieve the type of the resource based on the given method return type.
167
     *
168
     * @param ReflectionMethod $method
169
     *
170
     * @return string|null
171
     */
172 1
    protected function getResourceTypeByReturnType(ReflectionMethod $method)
173
    {
174 1
        $refReturnType = $method->getReturnType();
175 1
        if ($refReturnType === null) {
176 1
            return null;
177
        }
178
179 1
        if (!$refReturnType instanceof ReflectionNamedType) {
180
            return null;
181
        }
182
183 1
        $returnType = $refReturnType->getName();
184 1
        $namespace = 'Illuminate\Database\Eloquent\Relations';
185
186 1
        if (!Str::startsWith($returnType, $namespace)) {
187
            return null;
188
        }
189
190 1
        $relation = class_basename($returnType);
191
192 1
        return $this->relationsMap[$relation] ?? null;
193
    }
194
195
    /**
196
     * Retrieve the type of the resource based on the method's return annotation.
197
     *
198
     * @param ReflectionMethod $method
199
     *
200
     * @return string|null
201
     */
202 1
    protected function getResourceTypeByReturnAnnotation(ReflectionMethod $method)
203
    {
204 1
        if (preg_match('/@return\s+(\S+)/', $method->getDocComment(), $match)) {
205
            list($statement, $returnTypes) = $match;
206
207
            // Build a regex suitable for matching our relationship keys. EG. hasOne|hasMany...
208
            $keyPattern = implode(
209
                '|',
210
                array_map(
211
                    function ($key) {
212
                        return preg_quote($key, '/');
213
                    },
214
                    array_keys($this->relationsMap)
215
                )
216
            );
217
            foreach (explode('|', $returnTypes) as $returnType) {
218
                if (preg_match("/($keyPattern)\$/i", $returnType, $match)) {
219
                    return $this->relationsMap[$match[1]] ?? null;
220
                }
221
            }
222
        }
223
224 1
        return null;
225
    }
226
227
    /**
228
     * Retrieve the type of the resource based on the method body.
229
     * This is a pretty crude implementation which simply looks for a method call to one
230
     * of our relationship keywords.
231
     *
232
     * @param ReflectionMethod $method
233
     *
234
     * @return string|null
235
     */
236 1
    protected function getResourceTypeByMethodBody(ReflectionMethod $method)
237
    {
238 1
        $startLine = $method->getStartLine();
239 1
        $numLines = $method->getEndLine() - $startLine;
240 1
        $body = implode('', \array_slice(file($method->getFileName()), $startLine, $numLines));
241 1
        if (preg_match('/^\s*return\s+(.+?);/ms', $body, $match)) {
242 1
            $returnStmt = $match[1];
243 1
            foreach (array_keys($this->relationsMap) as $returnType) {
244
                // Find "->hasMany(" etc.
245 1
                if (preg_match('/->' . preg_quote($returnType, '/') . '\(/i', $returnStmt)) {
246 1
                    return $this->relationsMap[$returnType];
247
                }
248
            }
249
        }
250
251
        return null;
252
    }
253
}
254