Completed
Pull Request — master (#221)
by Christopher
11:18 queued 15s
created

MetadataTrait   B

Complexity

Total Complexity 47

Size/Duplication

Total Lines 402
Duplicated Lines 0 %

Test Coverage

Coverage 72.85%

Importance

Changes 14
Bugs 0 Features 0
Metric Value
wmc 47
eloc 164
c 14
b 0
f 0
dl 0
loc 402
ccs 110
cts 151
cp 0.7285
rs 8.64

13 Methods

Rating   Name   Duplication   Size   Complexity  
A retrieveCasts() 0 4 2
A metadataMask() 0 13 3
A getAllAttributes() 0 23 4
A collectGetters() 0 19 6
A setEagerLoad() 0 3 1
A getTableDoctrineColumns() 0 10 2
A reset() 0 5 1
A getEndpointName() 0 10 3
A getTableColumns() 0 11 2
A getEagerLoad() 0 3 1
C metadata() 0 75 12
B extractGubbins() 0 54 8
A isRunningInArtisan() 0 3 2

How to fix   Complexity   

Complex Class

Complex classes like MetadataTrait often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use MetadataTrait, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace AlgoWeb\PODataLaravel\Models;
3
4
use AlgoWeb\PODataLaravel\Models\ObjectMap\Entities\Associations\AssociationStubFactory;
5
use AlgoWeb\PODataLaravel\Models\ObjectMap\Entities\Associations\AssociationStubMonomorphic;
6
use AlgoWeb\PODataLaravel\Models\ObjectMap\Entities\Associations\AssociationStubPolymorphic;
7
use AlgoWeb\PODataLaravel\Models\ObjectMap\Entities\Associations\AssociationStubRelationType;
8
use AlgoWeb\PODataLaravel\Models\ObjectMap\Entities\EntityField;
9
use AlgoWeb\PODataLaravel\Models\ObjectMap\Entities\EntityFieldPrimitiveType;
10
use AlgoWeb\PODataLaravel\Models\ObjectMap\Entities\EntityFieldType;
11
use AlgoWeb\PODataLaravel\Models\ObjectMap\Entities\EntityGubbins;
12
use Illuminate\Database\Eloquent\Model;
13
use Illuminate\Database\Eloquent\Relations\BelongsTo;
14
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
15
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
16
use Illuminate\Database\Eloquent\Relations\MorphMany;
17
use Illuminate\Database\Eloquent\Relations\MorphOne;
18
use Illuminate\Database\Eloquent\Relations\MorphToMany;
19
use Illuminate\Database\Eloquent\Relations\Relation;
20
use Illuminate\Support\Facades\App;
21
use Mockery\Mock;
22
use POData\Common\InvalidOperationException;
23
use POData\Providers\Metadata\Type\IType;
24
25
trait MetadataTrait
26
{
27
    use MetadataRelationsTrait;
28 3
29
    protected $loadEagerRelations = [];
30 3
    protected static $tableColumns = [];
31
    protected static $tableColumnsDoctrine = [];
32
    protected static $tableData = [];
33 2
    protected static $dontCastTypes = ['object', 'array', 'collection', 'int'];
34 2
35
    protected static $relTypes = [
36 2
        'hasMany',
37
        'hasManyThrough',
38 2
        'belongsToMany',
39 1
        'hasOne',
40
        'belongsTo',
41
        'morphOne',
42 1
        'morphTo',
43 1
        'morphMany',
44 1
        'morphToMany',
45
        'morphedByMany'
46 1
    ];
47
48 1
    protected static $manyRelTypes = [
49 1
        'hasManyThrough',
50 1
        'belongsToMany',
51
        'hasMany',
52 1
        'morphMany',
53 1
        'morphToMany',
54 1
        'morphedByMany'
55
    ];
56 1
57
    /**
58 1
     * Retrieve and assemble this model's metadata for OData packaging.
59 1
     * @throws InvalidOperationException
60 1
     * @throws \Doctrine\DBAL\DBALException
61 1
     * @return array
62 1
     */
63 1
    public function metadata()
64 1
    {
65
        if (!$this instanceof Model) {
66 1
            throw new InvalidOperationException(get_class($this));
67
        }
68
69
        if (0 !== count(self::$tableData)) {
70
            return self::$tableData;
71
        } elseif (isset($this->odata)) {
72
            return self::$tableData = $this->odata;
73 4
        }
74
75 4
        // Break these out separately to enable separate reuse
76
        $connect = $this->getConnection();
77 4
        $builder = $connect->getSchemaBuilder();
78 4
79 4
        $table = $this->getTable();
80 2
81 4
        if (!$builder->hasTable($table)) {
82 1
            return self::$tableData = [];
83 1
        }
84
85 4
        /** @var array $columns */
86
        $columns = $this->getTableColumns();
87
        /** @var array $mask */
88
        $mask = $this->metadataMask();
89
        $columns = array_intersect($columns, $mask);
90
91 5
        $tableData = [];
92
93 5
        $rawFoo = $this->getTableDoctrineColumns();
94 5
        $foo = [];
95 1
        /** @var array $getters */
96
        $getters = $this->collectGetters();
97
        $getters = array_intersect($getters, $mask);
98 4
        $casts = $this->retrieveCasts();
99
100 4
        foreach ($rawFoo as $key => $val) {
101
            // Work around glitch in Doctrine when reading from MariaDB which added ` characters to root key value
102 4
            $key = trim($key, '`"');
103 4
            $foo[$key] = $val;
104 4
        }
105 4
106 4
        foreach ($columns as $column) {
107 4
            // Doctrine schema manager returns columns with lowercased names
108
            $rawColumn = $foo[strtolower($column)];
109 4
            /** @var IType $rawType */
110 1
            $rawType = $rawColumn->getType();
111 1
            $type = $rawType->getName();
112 1
            $default = $this->$column;
113 1
            $tableData[$column] = ['type' => $type,
114
                'nullable' => !($rawColumn->getNotNull()),
115 4
                'fillable' => in_array($column, $this->getFillable()),
116 4
                'default' => $default
117
            ];
118 4
        }
119
120
        foreach ($getters as $get) {
121
            if (isset($tableData[$get])) {
122
                continue;
123
            }
124
            $default = $this->$get;
125
            $tableData[$get] = ['type' => 'text', 'nullable' => true, 'fillable' => false, 'default' => $default];
126
        }
127
128
        // now, after everything's gathered up, apply Eloquent model's $cast array
129
        foreach ($casts as $key => $type) {
130
            $type = strtolower($type);
131
            if (array_key_exists($key, $tableData) && !in_array($type, self::$dontCastTypes)) {
132
                $tableData[$key]['type'] = $type;
133
            }
134
        }
135
136
        self::$tableData = $tableData;
137
        return $tableData;
138
    }
139
140
    /**
141
     * Return the set of fields that are permitted to be in metadata
142
     * - following same visible-trumps-hidden guideline as Laravel.
143
     *
144
     * @return array
145
     */
146
    public function metadataMask()
147
    {
148
        $attribs = array_keys($this->getAllAttributes());
149
150
        $visible = $this->getVisible();
151
        $hidden = $this->getHidden();
152
        if (0 < count($visible)) {
153
            $attribs = array_intersect($visible, $attribs);
154
        } elseif (0 < count($hidden)) {
155
            $attribs = array_diff($attribs, $hidden);
156
        }
157
158
        return $attribs;
159
    }
160
161
    /*
162
     * Get the endpoint name being exposed
163
     *
164
     * @return null|string;
165
     */
166
    public function getEndpointName()
167
    {
168
        $endpoint = isset($this->endpoint) ? $this->endpoint : null;
169
170
        if (!isset($endpoint)) {
171
            $bitter = get_class($this);
172
            $name = substr($bitter, strrpos($bitter, '\\')+1);
173
            return ($name);
174
        }
175 3
        return ($endpoint);
176
    }
177 3
178
    protected function getAllAttributes()
179 3
    {
180 3
        // Adapted from http://stackoverflow.com/a/33514981
181 3
        // $columns = $this->getFillable();
182 3
        // Another option is to get all columns for the table like so:
183 3
        $columns = $this->getTableColumns();
184 3
        // but it's safer to just get the fillable fields
185 3
186 3
        $attributes = $this->getAttributes();
187 3
188 3
        foreach ($columns as $column) {
189
            if (!array_key_exists($column, $attributes)) {
190 3
                $attributes[$column] = null;
191
            }
192 3
        }
193 3
194 3
        $methods = $this->collectGetters();
195 3
196 3
        foreach ($methods as $method) {
197 3
            $attributes[$method] = null;
198 3
        }
199 3
200 3
        return $attributes;
201 3
    }
202
203 3
    /**
204 3
     * Get the visible attributes for the model.
205 3
     *
206 3
     * @return array
207 3
     */
208 3
    abstract public function getVisible();
209 3
210 3
    /**
211
     * Get the hidden attributes for the model.
212 3
     *
213 3
     * @return array
214 3
     */
215
    abstract public function getHidden();
216 3
217 3
    /**
218 3
     * Get the primary key for the model.
219 3
     *
220 3
     * @return string
221
     */
222 1
    abstract public function getKeyName();
223 3
224
    /**
225 1
     * Get the current connection name for the model.
226
     *
227 1
     * @return string
228
     */
229 1
    abstract public function getConnectionName();
230
231 3
    /**
232 2
     * Get the database connection for the model.
233 2
     *
234 3
     * @return \Illuminate\Database\Connection
235 3
     */
236 3
    abstract public function getConnection();
237 3
238 3
    /**
239 3
     * Get all of the current attributes on the model.
240 3
     *
241
     * @return array
242
     */
243
    abstract public function getAttributes();
244
245
    /**
246
     * Get the table associated with the model.
247
     *
248
     * @return string
249
     */
250
    abstract public function getTable();
251
252
    /**
253
     * Get the fillable attributes for the model.
254
     *
255
     * @return array
256
     */
257
    abstract public function getFillable();
258
259
    /**
260
     * Dig up all defined getters on the model.
261
     *
262
     * @return array
263
     */
264
    protected function collectGetters()
265
    {
266
        $getterz = [];
267
        $methods = get_class_methods($this);
268
        foreach ($methods as $method) {
269
            if (12 < strlen($method) && 'get' == substr($method, 0, 3)) {
270
                if ('Attribute' == substr($method, -9)) {
271
                    $getterz[] = $method;
272
                }
273
            }
274
        }
275
        $methods = [];
276
277
        foreach ($getterz as $getter) {
278
            $residual = substr($getter, 3);
279
            $residual = substr(/* @scrutinizer ignore-type */$residual, 0, -9);
280
            $methods[] = $residual;
281
        }
282
        return $methods;
283
    }
284
285
    /**
286
     * Supplemental function to retrieve cast array for Laravel versions that do not supply hasCasts.
287
     *
288
     * @return array
289
     */
290
    public function retrieveCasts()
291
    {
292
        $exists = method_exists($this, 'getCasts');
293
        return $exists ? (array)$this->getCasts() : (array)$this->casts;
294
    }
295
296
    /**
297
     * Return list of relations to be eager-loaded by Laravel query provider.
298
     *
299
     * @return array
300
     */
301
    public function getEagerLoad()
302
    {
303
        return $this->loadEagerRelations;
304
    }
305
306
    /**
307
     * Set list of relations to be eager-loaded.
308
     *
309
     * @param array $relations
310
     */
311
    public function setEagerLoad(array $relations)
312
    {
313
        $this->loadEagerRelations = array_map('strval', $relations);
314
    }
315
316
    /**
317
     * Extract entity gubbins detail for later downstream use.
318
     *
319
     * @throws InvalidOperationException
320
     * @throws \ReflectionException
321
     * @throws \Doctrine\DBAL\DBALException
322
     * @throws \Exception
323
     * @return EntityGubbins
324
     */
325
    public function extractGubbins()
326
    {
327
        $multArray = [
0 ignored issues
show
Unused Code introduced by
The assignment to $multArray is dead and can be removed.
Loading history...
328
            '*' => AssociationStubRelationType::MANY(),
329
            '1' => AssociationStubRelationType::ONE(),
330
            '0..1' => AssociationStubRelationType::NULL_ONE()
331
        ];
332
333
        $gubbins = new EntityGubbins();
334
        $gubbins->setName($this->getEndpointName());
335
        $gubbins->setClassName(get_class($this));
336
337
        $lowerNames = [];
338
339
        $fields = $this->metadata();
340
        $entityFields = [];
341
        foreach ($fields as $name => $field) {
342
            if (in_array(strtolower($name), $lowerNames)) {
343
                $msg = 'Property names must be unique, without regard to case';
344
                throw new \Exception($msg);
345
            }
346
            $lowerNames[] = strtolower($name);
347
            $nuField = new EntityField();
348
            $nuField->setName($name);
349
            $nuField->setIsNullable($field['nullable']);
350
            $nuField->setReadOnly(false);
351
            $nuField->setCreateOnly(false);
352
            $nuField->setDefaultValue($field['default']);
353
            $nuField->setIsKeyField($this->getKeyName() == $name);
354
            $nuField->setFieldType(EntityFieldType::PRIMITIVE());
355
            $nuField->setPrimitiveType(new EntityFieldPrimitiveType($field['type']));
356
            $entityFields[$name] = $nuField;
357
        }
358
        $isEmpty = (0 === count($entityFields));
359
        if (!($isEmpty && $this->isRunningInArtisan())) {
360
            $gubbins->setFields($entityFields);
361
        }
362
363
        $rawRels = $this->getRelationships();
364
        $stubs = [];
365
        foreach ($rawRels as $propertyName) {
366
            if (in_array(strtolower($propertyName), $lowerNames)) {
367
                $msg = 'Property names must be unique, without regard to case';
368
                throw new \Exception($msg);
369
            }
370
            $stub = AssociationStubFactory::associationStubFromRelation($propertyName, $this->{$propertyName}());
371
            if (!$stub->isOk()) {
372
                throw new InvalidOperationException('Generated stub not consistent');
373
            }
374
            $stubs[$propertyName] = $stub;
375
        }
376
        $gubbins->setStubs($stubs);
377
378
        return $gubbins;
379
    }
380
381
    public function isRunningInArtisan()
382
    {
383
        return App::runningInConsole() && !App::runningUnitTests();
384
    }
385
386
    /**
387
     * Get columns for selected table.
388
     *
389
     * @return array
390
     */
391
    protected function getTableColumns()
392
    {
393
        if (0 === count(self::$tableColumns)) {
394
            $table = $this->getTable();
395
            $connect = $this->getConnection();
396
            $builder = $connect->getSchemaBuilder();
397
            $columns = $builder->getColumnListing($table);
398
399
            self::$tableColumns = (array)$columns;
400
        }
401
        return self::$tableColumns;
402
    }
403
404
    /**
405
     * Get Doctrine columns for selected table.
406
     *
407
     * @throws \Doctrine\DBAL\DBALException
408
     * @return array
409
     */
410
    protected function getTableDoctrineColumns()
411
    {
412
        if (0 === count(self::$tableColumnsDoctrine)) {
413
            $table = $this->getTable();
414
            $connect = $this->getConnection();
415
            $columns = $connect->getDoctrineSchemaManager()->listTableColumns($table);
416
417
            self::$tableColumnsDoctrine = $columns;
418
        }
419
        return self::$tableColumnsDoctrine;
420
    }
421
422
    public function reset()
423
    {
424
        self::$tableData = [];
425
        self::$tableColumnsDoctrine = [];
426
        self::$tableColumns = [];
427
    }
428
}
429