Completed
Push — master ( 260504...c2ab20 )
by Alex
18s queued 11s
created

MetadataTrait::getEndpointName()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

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