Passed
Pull Request — master (#221)
by Alex
05:19
created

MetadataTrait   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 363
Duplicated Lines 0 %

Test Coverage

Coverage 72.67%

Importance

Changes 13
Bugs 0 Features 0
Metric Value
wmc 44
eloc 136
c 13
b 0
f 0
dl 0
loc 363
ccs 109
cts 150
cp 0.7267
rs 8.8798

12 Methods

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

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
/**
26
 * Trait MetadataTrait
27
 * @package AlgoWeb\PODataLaravel\Models
28 3
 * @mixin Model
29
 */
30 3
trait MetadataTrait
31
{
32
    use MetadataRelationsTrait;
33 2
34 2
    protected $loadEagerRelations = [];
35
    protected static $tableColumns = [];
36 2
    protected static $tableColumnsDoctrine = [];
37
    protected static $tableData = [];
38 2
    protected static $dontCastTypes = ['object', 'array', 'collection', 'int'];
39 1
40
    /**
41
     * Retrieve and assemble this model's metadata for OData packaging.
42 1
     * @throws InvalidOperationException
43 1
     * @throws \Doctrine\DBAL\DBALException
44 1
     * @return array
45
     */
46 1
    public function metadata()
47
    {
48 1
        if (!$this instanceof Model) {
49 1
            throw new InvalidOperationException(get_class($this));
50 1
        }
51
52 1
        if (0 !== count(self::$tableData)) {
53 1
            return self::$tableData;
54 1
        } elseif (isset($this->odata)) {
55
            return self::$tableData = $this->odata;
56 1
        }
57
58 1
        // Break these out separately to enable separate reuse
59 1
        $connect = $this->getConnection();
60 1
        $builder = $connect->getSchemaBuilder();
61 1
62 1
        $table = $this->getTable();
63 1
64 1
        if (!$builder->hasTable($table)) {
65
            return self::$tableData = [];
66 1
        }
67
68
        /** @var array $columns */
69
        $columns = $this->getTableColumns();
70
        /** @var array $mask */
71
        $mask = $this->metadataMask();
72
        $columns = array_intersect($columns, $mask);
73 4
74
        $tableData = [];
75 4
76
        $rawFoo = $this->getTableDoctrineColumns();
77 4
        $foo = [];
78 4
        /** @var array $getters */
79 4
        $getters = $this->collectGetters();
80 2
        $getters = array_intersect($getters, $mask);
81 4
        $casts = $this->retrieveCasts();
82 1
83 1
        foreach ($rawFoo as $key => $val) {
84
            // Work around glitch in Doctrine when reading from MariaDB which added ` characters to root key value
85 4
            $key = trim($key, '`"');
86
            $foo[$key] = $val;
87
        }
88
89
        foreach ($columns as $column) {
90
            // Doctrine schema manager returns columns with lowercased names
91 5
            $rawColumn = $foo[strtolower($column)];
92
            /** @var IType $rawType */
93 5
            $rawType = $rawColumn->getType();
94 5
            $type = $rawType->getName();
95 1
            $default = $this->$column;
96
            $tableData[$column] = ['type' => $type,
97
                'nullable' => !($rawColumn->getNotNull()),
98 4
                'fillable' => in_array($column, $this->getFillable()),
99
                'default' => $default
100 4
            ];
101
        }
102 4
103 4
        foreach ($getters as $get) {
104 4
            if (isset($tableData[$get])) {
105 4
                continue;
106 4
            }
107 4
            $default = $this->$get;
108
            $tableData[$get] = ['type' => 'text', 'nullable' => true, 'fillable' => false, 'default' => $default];
109 4
        }
110 1
111 1
        // now, after everything's gathered up, apply Eloquent model's $cast array
112 1
        foreach ($casts as $key => $type) {
113 1
            $type = strtolower($type);
114
            if (array_key_exists($key, $tableData) && !in_array($type, self::$dontCastTypes)) {
115 4
                $tableData[$key]['type'] = $type;
116 4
            }
117
        }
118 4
119
        self::$tableData = $tableData;
120
        return $tableData;
121
    }
122
123
    /**
124
     * Return the set of fields that are permitted to be in metadata
125
     * - following same visible-trumps-hidden guideline as Laravel.
126
     *
127
     * @return array
128
     */
129
    public function metadataMask()
130
    {
131
        $attribs = array_keys($this->getAllAttributes());
132
133
        $visible = $this->getVisible();
134
        $hidden = $this->getHidden();
135
        if (0 < count($visible)) {
136
            $attribs = array_intersect($visible, $attribs);
137
        } elseif (0 < count($hidden)) {
138
            $attribs = array_diff($attribs, $hidden);
139
        }
140
141
        return $attribs;
142
    }
143
144
    /*
145
     * Get the endpoint name being exposed
146
     *
147
     * @return null|string;
148
     */
149
    public function getEndpointName()
150
    {
151
        $endpoint = isset($this->endpoint) ? $this->endpoint : null;
152
153
        if (!isset($endpoint)) {
154
            $bitter = get_class($this);
155
            $name = substr($bitter, strrpos($bitter, '\\')+1);
156
            return ($name);
157
        }
158
        return ($endpoint);
159
    }
160
161
    protected function getAllAttributes()
162
    {
163
        // Adapted from http://stackoverflow.com/a/33514981
164
        // $columns = $this->getFillable();
165
        // Another option is to get all columns for the table like so:
166
        $columns = $this->getTableColumns();
167
        // but it's safer to just get the fillable fields
168
169
        $attributes = $this->getAttributes();
170
171
        foreach ($columns as $column) {
172
            if (!array_key_exists($column, $attributes)) {
173
                $attributes[$column] = null;
174
            }
175 3
        }
176
177 3
        $methods = $this->collectGetters();
178
179 3
        foreach ($methods as $method) {
180 3
            $attributes[$method] = null;
181 3
        }
182 3
183 3
        return $attributes;
184 3
    }
185 3
186 3
    /**
187 3
     * Get the visible attributes for the model.
188 3
     *
189
     * @return array
190 3
     */
191
    abstract public function getVisible();
192 3
193 3
    /**
194 3
     * Get the hidden attributes for the model.
195 3
     *
196 3
     * @return array
197 3
     */
198 3
    abstract public function getHidden();
199 3
200 3
    /**
201 3
     * Get the primary key for the model.
202
     *
203 3
     * @return string
204 3
     */
205 3
    abstract public function getKeyName();
206 3
207 3
    /**
208 3
     * Get the current connection name for the model.
209 3
     *
210 3
     * @return string
211
     */
212 3
    abstract public function getConnectionName();
213 3
214 3
    /**
215
     * Get the database connection for the model.
216 3
     *
217 3
     * @return \Illuminate\Database\Connection
218 3
     */
219 3
    abstract public function getConnection();
220 3
221
    /**
222 1
     * Get all of the current attributes on the model.
223 3
     *
224
     * @return array
225 1
     */
226
    abstract public function getAttributes();
227 1
228
    /**
229 1
     * Get the table associated with the model.
230
     *
231 3
     * @return string
232 2
     */
233 2
    abstract public function getTable();
234 3
235 3
    /**
236 3
     * Get the fillable attributes for the model.
237 3
     *
238 3
     * @return array
239 3
     */
240 3
    abstract public function getFillable();
241
242
    /**
243
     * Dig up all defined getters on the model.
244
     *
245
     * @return array
246
     */
247
    protected function collectGetters()
248
    {
249
        $getterz = [];
250
        $methods = get_class_methods($this);
251
        foreach ($methods as $method) {
252
            if (12 < strlen($method) && 'get' == substr($method, 0, 3)) {
253
                if ('Attribute' == substr($method, -9)) {
254
                    $getterz[] = $method;
255
                }
256
            }
257
        }
258
        $methods = [];
259
260
        foreach ($getterz as $getter) {
261
            $residual = substr($getter, 3);
262
            $residual = substr(/* @scrutinizer ignore-type */$residual, 0, -9);
263
            $methods[] = $residual;
264
        }
265
        return $methods;
266
    }
267
268
    /**
269
     * Used to be supplemental function to retrieve cast array for Laravel versions that do not supply hasCasts.
270
     *
271
     * @return array
272
     */
273
    public function retrieveCasts()
274
    {
275
        return $this->getCasts();
0 ignored issues
show
Bug introduced by
It seems like getCasts() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

275
        return $this->/** @scrutinizer ignore-call */ getCasts();
Loading history...
276
    }
277
278
    /**
279
     * Return list of relations to be eager-loaded by Laravel query provider.
280
     *
281
     * @return array
282
     */
283
    public function getEagerLoad()
284
    {
285
        return $this->loadEagerRelations;
286
    }
287
288
    /**
289
     * Set list of relations to be eager-loaded.
290
     *
291
     * @param array $relations
292
     */
293
    public function setEagerLoad(array $relations)
294
    {
295
        $this->loadEagerRelations = array_map('strval', $relations);
296
    }
297
298
    /**
299
     * Extract entity gubbins detail for later downstream use.
300
     *
301
     * @throws InvalidOperationException
302
     * @throws \ReflectionException
303
     * @throws \Doctrine\DBAL\DBALException
304
     * @throws \Exception
305
     * @return EntityGubbins
306
     */
307
    public function extractGubbins()
308
    {
309
        $gubbins = new EntityGubbins();
310
        $gubbins->setName($this->getEndpointName());
311
        $gubbins->setClassName(get_class($this));
312
313
        $lowerNames = [];
314
315
        $fields = $this->metadata();
316
        $entityFields = [];
317
        foreach ($fields as $name => $field) {
318
            if (in_array(strtolower($name), $lowerNames)) {
319
                $msg = 'Property names must be unique, without regard to case';
320
                throw new \Exception($msg);
321
            }
322
            $lowerNames[] = strtolower($name);
323
            $nuField = new EntityField();
324
            $nuField->setName($name);
325
            $nuField->setIsNullable($field['nullable']);
326
            $nuField->setReadOnly(false);
327
            $nuField->setCreateOnly(false);
328
            $nuField->setDefaultValue($field['default']);
329
            $nuField->setIsKeyField($this->getKeyName() == $name);
330
            $nuField->setFieldType(EntityFieldType::PRIMITIVE());
331
            $nuField->setPrimitiveType(new EntityFieldPrimitiveType($field['type']));
332
            $entityFields[$name] = $nuField;
333
        }
334
        $isEmpty = (0 === count($entityFields));
335
        if (!($isEmpty && $this->isRunningInArtisan())) {
336
            $gubbins->setFields($entityFields);
337
        }
338
339
        $rawRels = $this->getRelationships();
340
        $stubs = [];
341
        foreach ($rawRels as $propertyName) {
342
            if (in_array(strtolower($propertyName), $lowerNames)) {
343
                $msg = 'Property names must be unique, without regard to case';
344
                throw new \Exception($msg);
345
            }
346
            $stub = AssociationStubFactory::associationStubFromRelation(/** @scrutinizer ignore-type */$this, $propertyName);
347
            $stubs[$propertyName] = $stub;
348
        }
349
        $gubbins->setStubs($stubs);
350
351
        return $gubbins;
352
    }
353
354
    public function isRunningInArtisan()
355
    {
356
        return App::runningInConsole() && !App::runningUnitTests();
357
    }
358
359
    /**
360
     * Get columns for selected table.
361
     *
362
     * @return array
363
     */
364
    protected function getTableColumns()
365
    {
366
        if (0 === count(self::$tableColumns)) {
367
            $table = $this->getTable();
368
            $connect = $this->getConnection();
369
            $builder = $connect->getSchemaBuilder();
370
            $columns = $builder->getColumnListing($table);
371
372
            self::$tableColumns = (array)$columns;
373
        }
374
        return self::$tableColumns;
375
    }
376
377
    /**
378
     * Get Doctrine columns for selected table.
379
     *
380
     * @throws \Doctrine\DBAL\DBALException
381
     * @return array
382
     */
383
    protected function getTableDoctrineColumns()
384
    {
385
        if (0 === count(self::$tableColumnsDoctrine)) {
386
            $table = $this->getTable();
387
            $connect = $this->getConnection();
388
            $columns = $connect->getDoctrineSchemaManager()->listTableColumns($table);
389
390
            self::$tableColumnsDoctrine = $columns;
391
        }
392
        return self::$tableColumnsDoctrine;
393
    }
394
}
395