MetadataTrait   B
last analyzed

Complexity

Total Complexity 45

Size/Duplication

Total Lines 363
Duplicated Lines 0 %

Test Coverage

Coverage 72.48%

Importance

Changes 13
Bugs 0 Features 0
Metric Value
wmc 45
eloc 134
c 13
b 0
f 0
dl 0
loc 363
rs 8.8
ccs 108
cts 149
cp 0.7248

12 Methods

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