Completed
Push — master ( d0a6a4...f2ca18 )
by Lachlan
06:27
created

ModelMapper   A

Complexity

Total Complexity 24

Size/Duplication

Total Lines 225
Duplicated Lines 0 %

Test Coverage

Coverage 82.54%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 79
c 3
b 0
f 0
dl 0
loc 225
ccs 52
cts 63
cp 0.8254
rs 10
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 getIncludes() 0 15 2
A getResourceTypeByReturnType() 0 21 4
A getDeclaredProperties() 0 10 2
A getResourceTypeByReturnAnnotation() 0 17 4
A __construct() 0 3 1
A getResourceTypeByMethodBody() 0 16 4
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
    /** @var Model */
55
    protected $model;
56
57 3
    public function __construct(Model $model)
58
    {
59 3
        $this->model = $model;
60 3
    }
61
62
    /**
63
     * List the includes of the given Eloquent model.
64
     *
65
     * @throws \ReflectionException
66
     *
67
     * @return array
68
     */
69 1
    public function getIncludes(): array
70
    {
71 1
        $includes = [];
72 1
        collect((new ReflectionClass($this->getModel()))->getMethods(ReflectionMethod::IS_PUBLIC))
73
            ->filter(function (ReflectionMethod $method) {
74
                // We're not interested in inherited methods
75 1
                return $method->getDeclaringClass()->getName() === $this->getModelClass();
76
            })->each(function (ReflectionMethod $method) use (&$includes) {
77
                // Only include if we can resolve a resource type
78 1
                if (($type = $this->getResourceType($method)) !== null) {
79 1
                    $includes[$method->getName()] = "relation|{$type}";
80
                }
81 1
            });
82
83 1
        return $includes;
84
    }
85
86
    /**
87
     * List the declared properties of the given Eloquent model.
88
     *
89
     * @return array
90
     */
91 1
    public function getDeclaredProperties(): array
92
    {
93 1
        $props = [];
94 1
        $table = $this->getModel()->getTable();
95 1
        foreach (Schema::getColumnListing($table) as $column) {
96 1
            $type = Schema::getColumnType($table, $column);
97 1
            $props[$column] = $this->schemaTypesMap[$type] ?? null;
98
        }
99
100 1
        return $props;
101
    }
102
103
    /**
104
     * Get the default properties.
105
     *
106
     * @return array
107
     */
108 1
    public function getDefaultProperties(): array
109
    {
110 1
        return [];
111
    }
112
113
    /**
114
     * @return Model
115
     */
116 2
    public function getModel(): Model
117
    {
118 2
        return $this->model;
119
    }
120
121
    /**
122
     * Return the model class.
123
     *
124
     * @return string
125
     */
126 1
    protected function getModelClass()
127
    {
128 1
        return \get_class($this->model);
129
    }
130
131
    /**
132
     * Get the resource type (item or collection) based on the return signature
133
     * of the given method.
134
     *
135
     * @param ReflectionMethod $method
136
     *
137
     * @return string|null
138
     */
139 1
    protected function getResourceType(ReflectionMethod $method)
140
    {
141
        // Try to get the type by type-hint.
142 1
        if ($type = $this->getResourceTypeByReturnType($method)) {
143 1
            return $type;
144
        }
145
146
        // Or, try to get the type by the @return annotation.
147 1
        if ($type = $this->getResourceTypeByReturnAnnotation($method)) {
148
            return $type;
149
        }
150
151
        // Or, try to get the type by the method body.
152 1
        if ($type = $this->getResourceTypeByMethodBody($method)) {
153 1
            return $type;
154
        }
155
156
        return null;
157
    }
158
159
    /**
160
     * Retrieve the type of the resource based on the given method return type.
161
     *
162
     * @param ReflectionMethod $method
163
     *
164
     * @return string|null
165
     */
166 1
    protected function getResourceTypeByReturnType(ReflectionMethod $method)
167
    {
168 1
        $refReturnType = $method->getReturnType();
169 1
        if ($refReturnType === null) {
170 1
            return null;
171
        }
172
173 1
        if (!$refReturnType instanceof ReflectionNamedType) {
174
            return null;
175
        }
176
177 1
        $returnType = $refReturnType->getName();
178 1
        $namespace = 'Illuminate\Database\Eloquent\Relations';
179
180 1
        if (!Str::startsWith($returnType, $namespace)) {
181
            return null;
182
        }
183
184 1
        $relation = class_basename($returnType);
185
186 1
        return $this->relationsMap[$relation] ?? null;
187
    }
188
189
    /**
190
     * Retrieve the type of the resource based on the method's return annotation.
191
     *
192
     * @param ReflectionMethod $method
193
     *
194
     * @return string|null
195
     */
196 1
    protected function getResourceTypeByReturnAnnotation(ReflectionMethod $method)
197
    {
198 1
        if (preg_match('/@return\s+(\S+)/', $method->getDocComment(), $match)) {
199
            list($statement, $returnTypes) = $match;
200
201
            // Build a regex suitable for matching our relationship keys. EG. hasOne|hasMany...
202
            $keyPattern = implode('|', array_map(function ($key) {
203
                return preg_quote($key, '/');
204
            }, array_keys($this->relationsMap)));
205
            foreach (explode('|', $returnTypes) as $returnType) {
206
                if (preg_match("/($keyPattern)\$/i", $returnType, $match)) {
207
                    return $this->relationsMap[$match[1]] ?? null;
208
                }
209
            }
210
        }
211
212 1
        return null;
213
    }
214
215
    /**
216
     * Retrieve the type of the resource based on the method body.
217
     * This is a pretty crude implementation which simply looks for a method call to one
218
     * of our relationship keywords.
219
     *
220
     * @param ReflectionMethod $method
221
     *
222
     * @return string|null
223
     */
224 1
    protected function getResourceTypeByMethodBody(ReflectionMethod $method)
225
    {
226 1
        $startLine = $method->getStartLine();
227 1
        $numLines = $method->getEndLine() - $startLine;
228 1
        $body = implode('', \array_slice(file($method->getFileName()), $startLine, $numLines));
0 ignored issues
show
Bug introduced by
It seems like file($method->getFileName()) can also be of type false; however, parameter $array of array_slice() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

228
        $body = implode('', \array_slice(/** @scrutinizer ignore-type */ file($method->getFileName()), $startLine, $numLines));
Loading history...
229 1
        if (preg_match('/^\s*return\s+(.+?);/ms', $body, $match)) {
230 1
            $returnStmt = $match[1];
231 1
            foreach (array_keys($this->relationsMap) as $returnType) {
232
                // Find "->hasMany(" etc.
233 1
                if (preg_match('/->'.preg_quote($returnType, '/').'\(/i', $returnStmt)) {
234 1
                    return $this->relationsMap[$returnType];
235
                }
236
            }
237
        }
238
239
        return null;
240
    }
241
}
242