Completed
Push — master ( ed4803...036ee8 )
by Ivan
02:27
created

Mapper.php$0 ➔ __get()   A

Complexity

Conditions 5

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 5.025

Importance

Changes 0
Metric Value
dl 0
loc 15
ccs 9
cts 10
cp 0.9
rs 9.4555
c 0
b 0
f 0
cc 5
crap 5.025
1
<?php
2
namespace vakata\database\schema;
3
4
use vakata\collection\Collection;
5
use vakata\database\DBInterface;
6
7
/**
8
 * A basic mapper to enable relation traversing and basic create / update / delete functionality
9
 */
10
class Mapper
11
{
12
    protected $db;
13
    protected $objects;
14
15 4
    public function __construct(DBInterface $db)
16
    {
17 4
        $this->db = $db;
18 4
    }
19
    /**
20
     * Create an entity from an array of data
21
     *
22
     * @param Table $definition
23
     * @param array $data
24
     * @param boolean $empty
25
     * @return object
26
     */
27 36
    public function entity(Table $definition, array $data, bool $empty = false)
28
    {
29 36
        if (!$empty) {
30 36
            $primary = [];
31 36
            foreach ($definition->getPrimaryKey() as $column) {
32 36
                $primary[$column] = $data[$column];
33
            }
34 36
            if (isset($this->objects[$definition->getName()][base64_encode(serialize($primary))])) {
35 36
                return $this->objects[$definition->getName()][base64_encode(serialize($primary))];
36
            }
37
        }
38
        $entity = new class ($this, $definition, $data, $empty) extends \StdClass {
0 ignored issues
show
Unused Code introduced by
The call to anonymous//src/schema/Mapper.php$0::__construct() has too many arguments starting with $empty.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
39
            protected $mapper;
40
            protected $empty;
41
            protected $definition;
42
            protected $initial = [];
43
            protected $changed = [];
44
            protected $fetched = [];
45
46 19
            public function __construct($mapper, $definition, array $data = [])
47
            {
48 19
                $this->mapper = $mapper;
49 19
                $this->definition = $definition;
50 19
                $this->initial = $data;
51 19
            }
52 22
            public function __lazyProperty(string $property, $resolve)
53
            {
54 22
                $this->fetched[$property] = $resolve;
55 22
                return $this;
56
            }
57 34
            public function __get($property)
58
            {
59 34
                if (isset($this->changed[$property])) {
60 2
                    return $this->changed[$property];
61
                }
62 34
                if (isset($this->initial[$property])) {
63 34
                    return $this->initial[$property];
64
                }
65 16
                if (isset($this->fetched[$property])) {
66 16
                    return is_callable($this->fetched[$property]) ?
67 6
                        $this->fetched[$property] = call_user_func($this->fetched[$property]) :
68 16
                        $this->fetched[$property];
69
                }
70
                return null;
71
            }
72 6
            public function __set($property, $value)
73
            {
74 6
                $this->changed[$property] = $value;
75 6
            }
76
            public function __call($method, $args)
77
            {
78
                if (isset($this->definition->getRelations()[$method])) {
79
                    if (isset($this->fetched[$method])) {
80
                        return is_callable($this->fetched[$method]) ?
81
                            $this->fetched[$method] = call_user_func($this->fetched[$method], $args[0] ?? null) :
82
                            $this->fetched[$method];
83
                    }
84
                }
85
                return null;
86
            }
87 24
            public function definition()
88
            {
89 24
                return $this->definition;
90
            }
91 6
            public function toArray(bool $fetch = false)
92
            {
93 6
                $data = [];
94 6
                foreach ($this->definition->getColumns() as $k) {
95 6
                    if (isset($this->fetched[$k])) {
96
                        if ($fetch) {
97
                            $this->fetched[$k] = call_user_func($this->fetched[$k]);
98
                        }
99
                        if (!is_callable($this->fetched[$k])) {
100
                            $data[$k] = $this->fetched[$k];
101
                        }
102
                    }
103 6
                    if (isset($this->initial[$k])) {
104 4
                        $data[$k] = $this->initial[$k];
105
                    }
106 6
                    if (isset($this->changed[$k])) {
107 6
                        $data[$k] = $this->changed[$k];
108
                    }
109
                }
110 6
                return $data;
111
            }
112 2
            public function fromArray(array $data)
113
            {
114 2
                foreach ($this->definition->getColumns() as $k) {
115 2
                    if (isset($data[$k])) {
116 2
                        $this->changed[$k] = $data[$k];
117
                    }
118
                }
119 2
                return $this;
120
            }
121 24
            public function id()
122
            {
123 24
                $primary = [];
124 24
                foreach ($this->definition->getPrimaryKey() as $k) {
125 24
                    $primary[$k] = $this->initial[$k] ?? null;
126
                }
127 24
                return $primary;
128
            }
129 6
            public function save()
130
            {
131 6
                $this->mapper->save($this);
132 6
                return $this->flatten();
133
            }
134 2
            public function delete()
135
            {
136 2
                $this->mapper->delete($this);
137 2
            }
138
            public function refresh()
139
            {
140
                $this->mapper->refresh($this);
141
                return $this->flatten();
142
            }
143 6
            public function flatten()
144
            {
145 6
                $this->initial = $this->toArray();
146 6
                $this->changed = [];
147 6
                return $this;
148
            }
149
        };
150 19
        if ($empty) {
151 2
            return $entity;
152
        }
153 17
        $this->lazy($entity, $data);
154 17
        return $this->objects[$definition->getName()][base64_encode(serialize($primary))] = $entity;
0 ignored issues
show
Bug introduced by
The variable $primary does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
155
    }
156
    /**
157
     * Get a collection of entities
158
     *
159
     * @param TableQuery $iterator
160
     * @param Table $definition
161
     * @return Collection
162
     */
163 36
    public function collection(TableQueryIterator $iterator, Table $definition) : Collection
164
    {
165 36
        return Collection::from($iterator)
166 36
            ->map(function ($v) use ($definition) {
167 36
                return $this->entity($definition, $v);
168 36
            });
169
    }
170
    /**
171
     * Persist all changes to an entity in the DB. Does not include modified relation collections.
172
     *
173
     * @param object $entity
174
     * @return object
175
     */
176 6
    public function save($entity)
177
    {
178 6
        $query = $this->db->table($entity->definition()->getName());
179 6
        $primary = $entity->id();
180 6
        if (!isset($this->objects[$entity->definition()->getName()][base64_encode(serialize($primary))])) {
181 2
            $new = $query->insert($entity->toArray());
182 2
            $entity->fromArray($new);
183 2
            $this->objects[$entity->definition()->getName()][base64_encode(serialize($new))] = $entity;
184
        } else {
185 4
            foreach ($primary as $k => $v) {
186 4
                $query->filter($k, $v);
187
            }
188 4
            $query->update($entity->toArray());
189 4
            $new = [];
190 4
            foreach ($primary as $k => $v) {
191 4
                $new[$k] = $entity->{$k};
192
            }
193 4
            if (base64_encode(serialize($new)) !== base64_encode(serialize($primary))) {
194 2
                unset($this->objects[$entity->definition()->getName()][base64_encode(serialize($primary))]);
195 2
                $this->objects[$entity->definition()->getName()][base64_encode(serialize($new))] = $entity;
196
            }
197
        }
198 6
        return $this->lazy($entity, $entity->toArray());
199
    }
200
    /**
201
     * Delete an entity from the database
202
     *
203
     * @param object $entity
204
     * @return void
205
     */
206 2
    public function delete($entity)
207
    {
208 2
        $query = $this->db->table($entity->definition()->getName());
209 2
        $primary = $entity->id();
210 2
        if (isset($this->objects[$entity->definition()->getName()][base64_encode(serialize($primary))])) {
211 2
            foreach ($primary as $k => $v) {
212 2
                $query->filter($k, $v);
213
            }
214 2
            $query->delete();
215 2
            unset($this->objects[$entity->definition()->getName()][base64_encode(serialize($primary))]);
216
        }
217 2
    }
218
    /**
219
     * Refresh an entity from the DB (includes own columns and relations).
220
     *
221
     * @param object $entity
222
     * @return object
223
     */
224
    public function refresh($entity)
225
    {
226
        $query = $this->db->table($entity->definition()->getName());
227
        $primary = $entity->id();
228
        foreach ($primary as $k => $v) {
229
            $query->filter($k, $v);
230
        }
231
        $data = $query[0] ?? [];
232
        $entity->fromArray($data);
233
        return $this->lazy($entity, $data);
234
    }
235 22
    protected function lazy($entity, $data)
236
    {
237 22
        $primary = $entity->id();
238 22
        $definition = $entity->definition();
239 22
        foreach ($definition->getColumns() as $column) {
240 22
            if (!isset($data[$column])) {
241 22
                $entity->__lazyProperty($column, function () use ($entity, $definition, $primary, $column) {
242
                    $query = $this->db->table($definition->getName());
243
                    foreach ($primary as $k => $v) {
244
                        $query->filter($k, $v);
245
                    }
246
                    return $query->select([$column])[0][$column] ?? null;
247 22
                });
248
            }
249
        }
250 22
        foreach ($definition->getRelations() as $name => $relation) {
251 22
            $entity->__lazyProperty(
252 22
                $name,
253 22
                isset($data[$name]) ?
254
                    ($relation->many ? 
255
                        array_map(function ($v) use ($relation) {
256
                            return $this->entity($relation->table, $v);
257
                        }, $data[$name]) :
258
                        $this->entity($relation->table, $data[$name])
259
                    ) :
260 22
                    function (array $columns = null) use ($entity, $definition, $primary, $relation, $data) {
261 6
                        $query = $this->db->table($relation->table->getName(), true);
0 ignored issues
show
Unused Code introduced by
The call to DBInterface::table() has too many arguments starting with true.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
262 6
                        if ($columns !== null) {
263
                            $query->columns($columns);
264
                        }
265 6
                        if ($relation->sql) {
266
                            $query->where($relation->sql, $relation->par);
267
                        }
268 6
                        if ($relation->pivot) {
269 4
                            $nm = null;
270 4
                            foreach ($relation->table->getRelations() as $rname => $rdata) {
271 4
                                if ($rdata->pivot && $rdata->pivot->getName() === $relation->pivot->getName()) {
272 4
                                    $nm = $rname;
273
                                }
274
                            }
275 4
                            if (!$nm) {
276
                                $nm = $definition->getName();
277
                                $relation->table->manyToMany(
278
                                    $this->db->table($definition->getName()),
279
                                    $relation->pivot,
280
                                    $nm,
281
                                    array_flip($relation->keymap),
282
                                    $relation->pivot_keymap
283
                                );
284
                            }
285 4
                            foreach ($definition->getPrimaryKey() as $v) {
286 4
                                $query->filter($nm . '.' . $v, $data[$v] ?? null);
287
                            }
288
                        } else {
289 6
                            foreach ($relation->keymap as $k => $v) {
290 6
                                $query->filter($v, $entity->{$k} ?? null);
291
                            }
292
                        }
293 6
                        return $relation->many ?
294 6
                            $query->iterator() :
295 6
                            $query[0];
296 22
                    }
297
            );
298
        }
299 22
        return $entity;
300
    }
301
}