Passed
Pull Request — master (#221)
by Christopher
06:36
created

MetadataTrait::extractGubbins()   B

Complexity

Conditions 8
Paths 17

Size

Total Lines 48
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 72

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 36
c 1
b 0
f 0
dl 0
loc 48
rs 8.0995
ccs 0
cts 0
cp 0
cc 8
nc 17
nop 0
crap 72
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
    /**
49 1
     * Retrieve and assemble this model's metadata for OData packaging.
50 1
     * @throws InvalidOperationException
51
     * @throws \Doctrine\DBAL\DBALException
52 1
     * @return array
53 1
     */
54 1
    public function metadata()
55
    {
56 1
        if (!$this instanceof Model) {
57
            throw new InvalidOperationException(get_class($this));
58 1
        }
59 1
60 1
        if (0 !== count(self::$tableData)) {
61 1
            return self::$tableData;
62 1
        } elseif (isset($this->odata)) {
63 1
            return self::$tableData = $this->odata;
64 1
        }
65
66 1
        // Break these out separately to enable separate reuse
67
        $connect = $this->getConnection();
68
        $builder = $connect->getSchemaBuilder();
69
70
        $table = $this->getTable();
71
72
        if (!$builder->hasTable($table)) {
73 4
            return self::$tableData = [];
74
        }
75 4
76
        /** @var array $columns */
77 4
        $columns = $this->getTableColumns();
78 4
        /** @var array $mask */
79 4
        $mask = $this->metadataMask();
80 2
        $columns = array_intersect($columns, $mask);
81 4
82 1
        $tableData = [];
83 1
84
        $rawFoo = $this->getTableDoctrineColumns();
85 4
        $foo = [];
86
        /** @var array $getters */
87
        $getters = $this->collectGetters();
88
        $getters = array_intersect($getters, $mask);
89
        $casts = $this->retrieveCasts();
90
91 5
        foreach ($rawFoo as $key => $val) {
92
            // Work around glitch in Doctrine when reading from MariaDB which added ` characters to root key value
93 5
            $key = trim($key, '`"');
94 5
            $foo[$key] = $val;
95 1
        }
96
97
        foreach ($columns as $column) {
98 4
            // Doctrine schema manager returns columns with lowercased names
99
            $rawColumn = $foo[strtolower($column)];
100 4
            /** @var IType $rawType */
101
            $rawType = $rawColumn->getType();
102 4
            $type = $rawType->getName();
103 4
            $default = $this->$column;
104 4
            $tableData[$column] = ['type' => $type,
105 4
                'nullable' => !($rawColumn->getNotNull()),
106 4
                'fillable' => in_array($column, $this->getFillable()),
107 4
                'default' => $default
108
            ];
109 4
        }
110 1
111 1
        foreach ($getters as $get) {
112 1
            if (isset($tableData[$get])) {
113 1
                continue;
114
            }
115 4
            $default = $this->$get;
116 4
            $tableData[$get] = ['type' => 'text', 'nullable' => true, 'fillable' => false, 'default' => $default];
117
        }
118 4
119
        // now, after everything's gathered up, apply Eloquent model's $cast array
120
        foreach ($casts as $key => $type) {
121
            $type = strtolower($type);
122
            if (array_key_exists($key, $tableData) && !in_array($type, self::$dontCastTypes)) {
123
                $tableData[$key]['type'] = $type;
124
            }
125
        }
126
127
        self::$tableData = $tableData;
128
        return $tableData;
129
    }
130
131
    /**
132
     * Return the set of fields that are permitted to be in metadata
133
     * - following same visible-trumps-hidden guideline as Laravel.
134
     *
135
     * @return array
136
     */
137
    public function metadataMask()
138
    {
139
        $attribs = array_keys($this->getAllAttributes());
140
141
        $visible = $this->getVisible();
142
        $hidden = $this->getHidden();
143
        if (0 < count($visible)) {
144
            $attribs = array_intersect($visible, $attribs);
145
        } elseif (0 < count($hidden)) {
146
            $attribs = array_diff($attribs, $hidden);
147
        }
148
149
        return $attribs;
150
    }
151
152
    /*
153
     * Get the endpoint name being exposed
154
     *
155
     * @return null|string;
156
     */
157
    public function getEndpointName()
158
    {
159
        $endpoint = isset($this->endpoint) ? $this->endpoint : null;
160
161
        if (!isset($endpoint)) {
162
            $bitter = get_class($this);
163
            $name = substr($bitter, strrpos($bitter, '\\')+1);
164
            return ($name);
165
        }
166
        return ($endpoint);
167
    }
168
169
    protected function getAllAttributes()
170
    {
171
        // Adapted from http://stackoverflow.com/a/33514981
172
        // $columns = $this->getFillable();
173
        // Another option is to get all columns for the table like so:
174
        $columns = $this->getTableColumns();
175 3
        // but it's safer to just get the fillable fields
176
177 3
        $attributes = $this->getAttributes();
178
179 3
        foreach ($columns as $column) {
180 3
            if (!array_key_exists($column, $attributes)) {
181 3
                $attributes[$column] = null;
182 3
            }
183 3
        }
184 3
185 3
        $methods = $this->collectGetters();
186 3
187 3
        foreach ($methods as $method) {
188 3
            $attributes[$method] = null;
189
        }
190 3
191
        return $attributes;
192 3
    }
193 3
194 3
    /**
195 3
     * Get the visible attributes for the model.
196 3
     *
197 3
     * @return array
198 3
     */
199 3
    abstract public function getVisible();
200 3
201 3
    /**
202
     * Get the hidden attributes for the model.
203 3
     *
204 3
     * @return array
205 3
     */
206 3
    abstract public function getHidden();
207 3
208 3
    /**
209 3
     * Get the primary key for the model.
210 3
     *
211
     * @return string
212 3
     */
213 3
    abstract public function getKeyName();
214 3
215
    /**
216 3
     * Get the current connection name for the model.
217 3
     *
218 3
     * @return string
219 3
     */
220 3
    abstract public function getConnectionName();
221
222 1
    /**
223 3
     * Get the database connection for the model.
224
     *
225 1
     * @return \Illuminate\Database\Connection
226
     */
227 1
    abstract public function getConnection();
228
229 1
    /**
230
     * Get all of the current attributes on the model.
231 3
     *
232 2
     * @return array
233 2
     */
234 3
    abstract public function getAttributes();
235 3
236 3
    /**
237 3
     * Get the table associated with the model.
238 3
     *
239 3
     * @return string
240 3
     */
241
    abstract public function getTable();
242
243
    /**
244
     * Get the fillable attributes for the model.
245
     *
246
     * @return array
247
     */
248
    abstract public function getFillable();
249
250
    /**
251
     * Dig up all defined getters on the model.
252
     *
253
     * @return array
254
     */
255
    protected function collectGetters()
256
    {
257
        $getterz = [];
258
        $methods = get_class_methods($this);
259
        foreach ($methods as $method) {
260
            if (12 < strlen($method) && 'get' == substr($method, 0, 3)) {
261
                if ('Attribute' == substr($method, -9)) {
262
                    $getterz[] = $method;
263
                }
264
            }
265
        }
266
        $methods = [];
267
268
        foreach ($getterz as $getter) {
269
            $residual = substr($getter, 3);
270
            $residual = substr(/* @scrutinizer ignore-type */$residual, 0, -9);
271
            $methods[] = $residual;
272
        }
273
        return $methods;
274
    }
275
276
    /**
277
     * Supplemental function to retrieve cast array for Laravel versions that do not supply hasCasts.
278
     *
279
     * @return array
280
     */
281
    public function retrieveCasts()
282
    {
283
        $exists = method_exists($this, 'getCasts');
284
        return $exists ? (array)$this->getCasts() : (array)$this->casts;
285
    }
286
287
    /**
288
     * Return list of relations to be eager-loaded by Laravel query provider.
289
     *
290
     * @return array
291
     */
292
    public function getEagerLoad()
293
    {
294
        return $this->loadEagerRelations;
295
    }
296
297
    /**
298
     * Set list of relations to be eager-loaded.
299
     *
300
     * @param array $relations
301
     */
302
    public function setEagerLoad(array $relations)
303
    {
304
        $this->loadEagerRelations = array_map('strval', $relations);
305
    }
306
307
    /**
308
     * Extract entity gubbins detail for later downstream use.
309
     *
310
     * @throws InvalidOperationException
311
     * @throws \ReflectionException
312
     * @throws \Doctrine\DBAL\DBALException
313
     * @throws \Exception
314
     * @return EntityGubbins
315
     */
316
    public function extractGubbins()
317
    {
318
        $gubbins = new EntityGubbins();
319
        $gubbins->setName($this->getEndpointName());
320
        $gubbins->setClassName(get_class($this));
321
322
        $lowerNames = [];
323
324
        $fields = $this->metadata();
325
        $entityFields = [];
326
        foreach ($fields as $name => $field) {
327
            if (in_array(strtolower($name), $lowerNames)) {
328
                $msg = 'Property names must be unique, without regard to case';
329
                throw new \Exception($msg);
330
            }
331
            $lowerNames[] = strtolower($name);
332
            $nuField = new EntityField();
333
            $nuField->setName($name);
334
            $nuField->setIsNullable($field['nullable']);
335
            $nuField->setReadOnly(false);
336
            $nuField->setCreateOnly(false);
337
            $nuField->setDefaultValue($field['default']);
338
            $nuField->setIsKeyField($this->getKeyName() == $name);
339
            $nuField->setFieldType(EntityFieldType::PRIMITIVE());
340
            $nuField->setPrimitiveType(new EntityFieldPrimitiveType($field['type']));
341
            $entityFields[$name] = $nuField;
342
        }
343
        $isEmpty = (0 === count($entityFields));
344
        if (!($isEmpty && $this->isRunningInArtisan())) {
345
            $gubbins->setFields($entityFields);
346
        }
347
348
        $rawRels = $this->getRelationships();
349
        $stubs = [];
350
        foreach ($rawRels as $propertyName) {
351
            if (in_array(strtolower($propertyName), $lowerNames)) {
352
                $msg = 'Property names must be unique, without regard to case';
353
                throw new \Exception($msg);
354
            }
355
            $stub = AssociationStubFactory::associationStubFromRelation($this, $propertyName);
0 ignored issues
show
Bug introduced by
$this of type AlgoWeb\PODataLaravel\Models\MetadataTrait is incompatible with the type Illuminate\Database\Eloquent\Model expected by parameter $parent of AlgoWeb\PODataLaravel\Mo...ationStubFromRelation(). ( Ignorable by Annotation )

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

355
            $stub = AssociationStubFactory::associationStubFromRelation(/** @scrutinizer ignore-type */ $this, $propertyName);
Loading history...
356
            if (!$stub->isOk()) {
357
                throw new InvalidOperationException('Generated stub not consistent');
358
            }
359
            $stubs[$propertyName] = $stub;
360
        }
361
        $gubbins->setStubs($stubs);
362
363
        return $gubbins;
364
    }
365
366
    public function isRunningInArtisan()
367
    {
368
        return App::runningInConsole() && !App::runningUnitTests();
369
    }
370
371
    /**
372
     * Get columns for selected table.
373
     *
374
     * @return array
375
     */
376
    protected function getTableColumns()
377
    {
378
        if (0 === count(self::$tableColumns)) {
379
            $table = $this->getTable();
380
            $connect = $this->getConnection();
381
            $builder = $connect->getSchemaBuilder();
382
            $columns = $builder->getColumnListing($table);
383
384
            self::$tableColumns = (array)$columns;
385
        }
386
        return self::$tableColumns;
387
    }
388
389
    /**
390
     * Get Doctrine columns for selected table.
391
     *
392
     * @throws \Doctrine\DBAL\DBALException
393
     * @return array
394
     */
395
    protected function getTableDoctrineColumns()
396
    {
397
        if (0 === count(self::$tableColumnsDoctrine)) {
398
            $table = $this->getTable();
399
            $connect = $this->getConnection();
400
            $columns = $connect->getDoctrineSchemaManager()->listTableColumns($table);
401
402
            self::$tableColumnsDoctrine = $columns;
403
        }
404
        return self::$tableColumnsDoctrine;
405
    }
406
407
    public function reset()
408
    {
409
        self::$tableData = [];
410
        self::$tableColumnsDoctrine = [];
411
        self::$tableColumns = [];
412
    }
413
}
414