Completed
Pull Request — master (#19)
by Alex
02:29
created

MetadataTrait   B

Complexity

Total Complexity 41

Size/Duplication

Total Lines 303
Duplicated Lines 8.58 %

Coupling/Cohesion

Components 1
Dependencies 6

Test Coverage

Coverage 72.85%

Importance

Changes 7
Bugs 0 Features 0
Metric Value
wmc 41
c 7
b 0
f 0
lcom 1
cbo 6
dl 26
loc 303
ccs 110
cts 151
cp 0.7285
rs 8.2769

15 Methods

Rating   Name   Duplication   Size   Complexity  
B metadata() 0 40 4
A metadataMask() 0 14 3
A getEndpointName() 0 9 2
B getXmlSchema() 0 29 5
C hookUpRelationships() 26 34 13
A getAllAttributes() 0 18 3
C getRelationshipsFromMethods() 0 67 11
getVisible() 0 1 ?
getHidden() 0 1 ?
getKeyName() 0 1 ?
getConnectionName() 0 1 ?
getConnection() 0 1 ?
getAttributes() 0 1 ?
getTable() 0 1 ?
getFillable() 0 1 ?

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 Illuminate\Support\Facades\App as App;
5
use Illuminate\Database\Eloquent\Relations\Relation;
6
use POData\Providers\Metadata\ResourceStreamInfo;
7
use POData\Providers\Metadata\Type\EdmPrimitiveType;
8
use Illuminate\Database\Eloquent\Model;
9
10
trait MetadataTrait
11
{
12
    /*
13
     * Array to record mapping between doctrine types and OData types
14
     */
15
    protected $mapping = [
16
        'integer' => EdmPrimitiveType::INT32,
17
        'string' => EdmPrimitiveType::STRING,
18
        'datetime' => EdmPrimitiveType::DATETIME,
19
        'decimal' => EdmPrimitiveType::DECIMAL,
20
        'text' => EdmPrimitiveType::STRING,
21
        'boolean' => EdmPrimitiveType::BOOLEAN,
22
        'blob' => "stream"
23
    ];
24
25
    /*
26
     * Retrieve and assemble this model's metadata for OData packaging
27
     */
28 3
    public function metadata()
29
    {
30 3
        assert($this instanceof Model, get_class($this));
31
32
        // Break these out separately to enable separate reuse
33 2
        $connect = $this->getConnection();
34 2
        $builder = $connect->getSchemaBuilder();
35
36 2
        $table = $this->getTable();
37
38 2
        if (!$builder->hasTable($table)) {
39 1
            return [];
40
        }
41
42 1
        $columns = $builder->getColumnListing($table);
43 1
        $mask = $this->metadataMask();
44 1
        $columns = array_intersect($columns, $mask);
45
46 1
        $tableData = [];
47
48 1
        $rawFoo = $connect->getDoctrineSchemaManager()->listTableColumns($table);
49 1
        $foo = [];
50 1
        foreach ($rawFoo as $key => $val) {
51
            // Work around glitch in Doctrine when reading from MariaDB which added ` characters to root key value
52 1
            $key = trim($key, '`');
53 1
            $foo[$key] = $val;
54 1
        }
55
56 1
        foreach ($columns as $column) {
57
            // Doctrine schema manager returns columns with lowercased names
58 1
            $rawColumn = $foo[strtolower($column)];
59 1
            $nullable = !($rawColumn->getNotNull());
60 1
            $fillable = in_array($column, $this->getFillable());
61 1
            $rawType = $rawColumn->getType();
62 1
            $type = $rawType->getName();
63 1
            $tableData[$column] = ['type' => $type, 'nullable' => $nullable, 'fillable' => $fillable];
64 1
        }
65
66 1
        return $tableData;
67
    }
68
69
    /*
70
     * Return the set of fields that are permitted to be in metadata
71
     * - following same visible-trumps-hidden guideline as Laravel
72
     */
73 4
    public function metadataMask()
74
    {
75 4
        $attribs = array_keys($this->getAllAttributes());
76
77 4
        $visible = $this->getVisible();
78 4
        $hidden = $this->getHidden();
79 4
        if (0 != count($visible)) {
80 2
            $attribs = array_intersect($visible, $attribs);
81 4
        } elseif (0 != count($hidden)) {
82 1
            $attribs = array_diff($attribs, $hidden);
83 1
        }
84
85 4
        return $attribs;
86
    }
87
88
    /*
89
     * Get the endpoint name being exposed
90
     *
91 5
     */
92
    public function getEndpointName()
93 5
    {
94 5
        if (!isset($this->endpoint)) {
95 1
            $bitter = get_class();
96
            $name = substr($bitter, strrpos($bitter, '\\')+1);
97
            return strtolower($name);
98 4
        }
99
        return strtolower($this->endpoint);
0 ignored issues
show
Bug introduced by
The property endpoint does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
100 4
    }
101
102 4
    /*
103 4
     * Assemble this model's OData metadata as xml schema
104 4
     */
105 4
    public function getXmlSchema($MetaNamespace = "Data")
106 4
    {
107 4
        $raw = $this->metadata();
108
        if ([] == $raw) {
109 4
            return null;
110 1
        }
111 1
112 1
        $metadata = App::make('metadata');
113 1
114
        $table = $this->getTable();
115 4
116 4
        $complex = $metadata->addEntityType(new \ReflectionClass(get_class($this)), $table, $MetaNamespace);
117
        $keyName = $this->getKeyName();
118 4
        $metadata->addKeyProperty($complex, $keyName, $this->mapping[$raw[$keyName]['type']]);
119
        foreach ($raw as $key => $secret) {
120
            if ($key == $keyName) {
121
                continue;
122
            }
123
            if ($secret['type'] == "blob") {
124
                $complex->setMediaLinkEntry(true);
125
                $streamInfo = new ResourceStreamInfo($key);
126
                $complex->addNamedStream($streamInfo);
127
                continue;
128
            }
129
            $metadata->addPrimitiveProperty($complex, $key, $this->mapping[$secret['type']]); // tag as isBag?
130
        }
131
132
        return $complex;
133
    }
134
135
    public function hookUpRelationships($entityTypes, $resourceSets)
136
    {
137
        $metadata = \App::make('metadata');
138
        $rel = $this->getRelationshipsFromMethods();
139
        $thisClass = get_class($this);
140 View Code Duplication
        foreach ($rel["HasOne"] as $n => $r) {
141
            if ($r[0] == "\\") {
142
                $r = substr($r, 1);
143
            }
144
            if (array_key_exists($r, $entityTypes)
145
                && array_key_exists($r, $resourceSets)
146
                && array_key_exists($thisClass, $entityTypes)
147
                && array_key_exists($thisClass, $resourceSets)) {
148
                $resourceType = $entityTypes[$thisClass];
149
                $targResourceSet = $resourceSets[$r];
150
                $metadata->addResourceReferenceProperty($resourceType, $n, $targResourceSet);
151
            }
152
        }
153 View Code Duplication
        foreach ($rel["HasMany"] as $n => $r) {
154
            if ($r[0] == "\\") {
155
                $r = substr($r, 1);
156
            }
157
            if (array_key_exists($r, $entityTypes)
158
                 && array_key_exists($r, $resourceSets)
159
                 && array_key_exists($thisClass, $entityTypes)
160
                 && array_key_exists($thisClass, $resourceSets)) {
161
                $resourceType = $entityTypes[$thisClass];
162
                $targResourceSet = $resourceSets[$r];
163
                $metadata->addResourceSetReferenceProperty($resourceType, $n, $targResourceSet);
164
            }
165
        }
166
167
        return $rel;
168
    }
169
170
    protected function getAllAttributes()
171
    {
172
        // Adapted from http://stackoverflow.com/a/33514981
173
        // $columns = $this->getFillable();
174
        // Another option is to get all columns for the table like so:
175 3
        $builder = $this->getConnection()->getSchemaBuilder();
176
        $columns = $builder->getColumnListing($this->getTable());
177 3
        // but it's safer to just get the fillable fields
178
179 3
        $attributes = $this->getAttributes();
180 3
181 3
        foreach ($columns as $column) {
182 3
            if (!array_key_exists($column, $attributes)) {
183 3
                $attributes[$column] = null;
184 3
            }
185 3
        }
186 3
        return $attributes;
187 3
    }
188 3
189
    protected function getRelationshipsFromMethods()
190 3
    {
191
        $model = $this;
192 3
        $relationships = array(
193 3
            "HasOne" => array(),
194 3
            "UnknownPolyMorphSide"=>array(),
195 3
            "HasMany"=>array(),
196 3
            "KnownPolyMorphSide"=>array()
197 3
        );
198 3
        $methods = get_class_methods($model);
199 3
        if (!empty($methods)) {
200 3
            foreach ($methods as $method) {
201 3
                if (!method_exists('Illuminate\Database\Eloquent\Model', $method)
202
                ) {
203 3
                    //Use reflection to inspect the code, based on Illuminate/Support/SerializableClosure.php
204 3
                    $reflection = new \ReflectionMethod($model, $method);
205 3
206 3
                    $file = new \SplFileObject($reflection->getFileName());
207 3
                    $file->seek($reflection->getStartLine()-1);
208 3
                    $code = '';
209 3
                    while ($file->key() < $reflection->getEndLine()) {
210 3
                        $code .= $file->current();
211
                        $file->next();
212 3
                    }
213 3
                    $code = trim(preg_replace('/\s\s+/', '', $code));
214 3
                    $begin = strpos($code, 'function(');
215
                    $code = substr($code, $begin, strrpos($code, '}')-$begin+1);
216 3
                    foreach (array(
217 3
                                'hasMany',
218 3
                                'hasManyThrough',
219 3
                                'belongsToMany',
220 3
                                'hasOne',
221
                                'belongsTo',
222 1
                                'morphOne',
223 3
                                'morphTo',
224
                                'morphMany',
225 1
                                'morphToMany'
226
                                ) as $relation) {
227 1
                        $search = '$this->'.$relation.'(';
228
                        if ($pos = stripos($code, $search)) {
229 1
                            //Resolve the relation's model to a Relation object.
230
                            $relationObj = $model->$method();
231 3
                            if ($relationObj instanceof Relation) {
232 2
                                $relatedModel = '\\'.get_class($relationObj->getRelated());
233 2
                                $relations = ['hasManyThrough', 'belongsToMany', 'hasMany', 'morphMany', 'morphToMany'];
234 3
                                if (in_array($relation, $relations)) {
235 3
                                    //Collection or array of models (because Collection is Arrayable)
236 3
                                    $relationships["HasMany"][$method] = $relatedModel;
237 3
                                } elseif ($relation === "morphTo") {
238 3
                                    // Model isn't specified because relation is polymorphic
239 3
                                    $relationships["UnknownPolyMorphSide"][$method] =
240 3
                                        '\Illuminate\Database\Eloquent\Model|\Eloquent';
241
                                } else {
242
                                    //Single model is returned
243
                                    $relationships["HasOne"][$method] = $relatedModel;
244
                                }
245
                                if (in_array($relation, ["morphMany", "morphOne"])) {
246
                                    $relationships["KnownPolyMorphSide"][$method] = $relatedModel;
247
                                }
248
                            }
249
                        }
250
                    }
251
                }
252
            }
253
        }
254
        return $relationships;
255
    }
256
257
    /**
258
     * Get the visible attributes for the model.
259
     *
260
     * @return array
261
     */
262
    public abstract function getVisible();
263
264
    /**
265
     * Get the hidden attributes for the model.
266
     *
267
     * @return array
268
     */
269
    public abstract function getHidden();
270
271
    /**
272
     * Get the primary key for the model.
273
     *
274
     * @return string
275
     */
276
    public abstract function getKeyName();
277
278
    /**
279
     * Get the current connection name for the model.
280
     *
281
     * @return string
282
     */
283
    public abstract function getConnectionName();
284
285
    /**
286
     * Get the database connection for the model.
287
     *
288
     * @return \Illuminate\Database\Connection
289
     */
290
    public abstract function getConnection();
291
292
    /**
293
     * Get all of the current attributes on the model.
294
     *
295
     * @return array
296
     */
297
    public abstract function getAttributes();
298
299
    /**
300
     * Get the table associated with the model.
301
     *
302
     * @return string
303
     */
304
    public abstract function getTable();
305
306
    /**
307
     * Get the fillable attributes for the model.
308
     *
309
     * @return array
310
     */
311
    public abstract function getFillable();
312
}
313