Test Failed
Pull Request — master (#11)
by Jodie
03:02
created

ModelMapper::getIncludes()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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

218
        $body = implode('', \array_slice(/** @scrutinizer ignore-type */ file($method->getFileName()), $startLine, $numLines));
Loading history...
219
        if (preg_match('/^\s*return\s+(.+?);/ms', $body, $match)) {
220
            $returnStmt = $match[1];
221
            foreach (array_keys($this->relationsMap) as $returnType) {
222
                // Find "->hasMany(" etc.
223
                if (preg_match('/->' . preg_quote($returnType, '/') . '\(/i', $returnStmt)) {
224
                    return $this->relationsMap[$returnType];
225
                }
226
            }
227
        }
228
229
        return null;
230
    }
231
}
232