Completed
Pull Request — master (#11)
by Jodie
02:31
created

ModelMapper::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
rs 10
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
class ModelMapper
14
{
15
    /**
16
     * Maps eloquent relationship methods to smokescreen resource types.
17
     *
18
     * @var array
19
     */
20
    protected $relationsMap = [
21
        'HasOne'         => 'item',
22
        'MorphOne'       => 'item',
23
        'BelongsTo'      => 'item',
24
        'MorphTo'        => 'item',
25
        'HasMany'        => 'collection',
26
        'HasManyThrough' => 'collection',
27
        'MorphMany'      => 'collection',
28
        'BelongsToMany'  => 'collection',
29
        'MorphToMany'    => 'collection',
30
        'MorphedByMany'  => 'collection',
31
    ];
32
33
    /**
34
     * Maps schema field types to smokescreen property types.
35
     *
36
     * @var array
37
     */
38
    protected $schemaTypesMap = [
39
        'guid'     => 'string',
40
        'boolean'  => 'boolean',
41
        'datetime' => 'datetime',
42
        'string'   => 'string',
43
        'json'     => 'array',
44
        'integer'  => 'integer',
45
        'date'     => 'date',
46
        'smallint' => 'integer',
47
        'text'     => 'string',
48
        'decimal'  => 'float',
49
        'bigint'   => 'integer',
50
    ];
51
52
    /** @var Model */
53
    protected $model;
54
55
    public function __construct(Model $model)
56
    {
57
        $this->model = $model;
58
    }
59
60
    /**
61
     * List the includes of the given Eloquent model.
62
     *
63
     * @throws \ReflectionException
64
     *
65
     * @return array
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
155
    /**
156
     * Retrieve the type of the resource based on the given method return type.
157
     *
158
     * @param ReflectionMethod $method
159
     *
160
     * @return string|null
161
     */
162
    protected function getResourceTypeByReturnType(ReflectionMethod $method)
163
    {
164
        $returnType = (string) $method->getReturnType();
165
        $namespace = 'Illuminate\Database\Eloquent\Relations';
166
167
        if (!starts_with($returnType, $namespace)) {
168
            return;
169
        }
170
171
//        $relation = lcfirst(class_basename($returnType));
172
        $relation = class_basename($returnType);
173
174
        return $this->relationsMap[$relation] ?? null;
175
    }
176
177
    /**
178
     * Retrieve the type of the resource based on the method's return annotation.
179
     *
180
     * @param ReflectionMethod $method
181
     *
182
     * @return string|null
183
     */
184
    protected function getResourceTypeByReturnAnnotation(ReflectionMethod $method)
185
    {
186
        if (preg_match('/@return\s+(\S+)/', $method->getDocComment(), $match)) {
187
            list($statement, $returnTypes) = $match;
188
189
            // Build a regex suitable for matching our relationship keys. EG. hasOne|hasMany...
190
            $keyPattern = implode('|', array_map(function ($key) {
191
                return preg_quote($key, '/');
192
            }, array_keys($this->relationsMap)));
193
            foreach (explode('|', $returnTypes) as $returnType) {
194
                if (preg_match("/($keyPattern)\$/i", $returnType, $match)) {
195
                    return $this->relationsMap[$match[1]] ?? null;
196
                }
197
            }
198
        }
199
    }
200
201
    /**
202
     * Retrieve the type of the resource based on the method body.
203
     * This is a pretty crude implementation which simply looks for a method call to one
204
     * of our relationship keywords.
205
     *
206
     * @param ReflectionMethod $method
207
     *
208
     * @return string|null
209
     */
210
    protected function getResourceTypeByMethodBody(ReflectionMethod $method)
211
    {
212
        $startLine = $method->getStartLine();
213
        $numLines = $method->getEndLine() - $startLine;
214
        $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

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