Passed
Pull Request — master (#221)
by Christopher
06:51 queued 45s
created

MetadataTrait::isRunningInArtisan()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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