Completed
Push — master ( 20e1fc...2f76e5 )
by Ivan
02:17
created

Mapper.php$0 ➔ save()   A

Complexity

Conditions 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 5
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 1
crap 1
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 32
    public function entity(Table $definition, array $data, bool $empty = false)
28
    {
29 32
        if (!$empty) {
30 32
            $primary = [];
31 32
            foreach ($definition->getPrimaryKey() as $column) {
32 32
                $primary[$column] = $data[$column];
33
            }
34 32
            if (isset($this->objects[$definition->getName()][base64_encode(serialize($primary))])) {
35 32
                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 16
            public function __construct($mapper, $definition, array $data = [])
47
            {
48 16
                $this->mapper = $mapper;
49 16
                $this->definition = $definition;
50 16
                $this->initial = $data;
51 16
            }
52 19
            public function __lazyProperty(string $property, callable $resolve)
53
            {
54 19
                $this->fetched[$property] = $resolve;
55 19
                return $this;
56
            }
57 31
            public function __get($property)
58
            {
59 31
                if (isset($this->changed[$property])) {
60 2
                    return $this->changed[$property];
61
                }
62 31
                if (isset($this->initial[$property])) {
63 31
                    return $this->initial[$property];
64
                }
65 14
                if (isset($this->fetched[$property])) {
66 14
                    return is_callable($this->fetched[$property]) ?
67 5
                        $this->fetched[$property] = call_user_func($this->fetched[$property]) :
68 14
                        $this->fetched[$property];
69
                }
70
                return null;
71
            }
72 5
            public function __set($property, $value)
73
            {
74 5
                $this->changed[$property] = $value;
75 5
            }
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 20
            public function definition()
88
            {
89 20
                return $this->definition;
90
            }
91 19
            public function toArray(bool $fetch = false)
92
            {
93 19
                $data = [];
94 19
                foreach ($this->definition->getColumns() as $k) {
95 19
                    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 19
                    if (isset($this->initial[$k])) {
104 18
                        $data[$k] = $this->initial[$k];
105
                    }
106 19
                    if (isset($this->changed[$k])) {
107 19
                        $data[$k] = $this->changed[$k];
108
                    }
109
                }
110 19
                return $data;
111
            }
112 1
            public function fromArray(array $data)
113
            {
114 1
                foreach ($this->definition->getColumns() as $k) {
115 1
                    if (isset($data[$k])) {
116 1
                        $this->changed[$k] = $data[$k];
117
                    }
118
                }
119 1
                return $this;
120
            }
121 20
            public function id()
122
            {
123 20
                $primary = [];
124 20
                foreach ($this->definition->getPrimaryKey() as $k) {
125 20
                    $primary[$k] = $this->initial[$k] ?? null;
126
                }
127 20
                return $primary;
128
            }
129 5
            public function save()
130
            {
131 5
                $this->mapper->save($this);
132 5
                return $this->flatten();
133
            }
134 1
            public function delete()
135
            {
136 1
                $this->mapper->delete($this);
137 1
            }
138
            public function refresh()
139
            {
140
                $this->mapper->refresh($this);
141
                return $this->flatten();
142
            }
143 5
            public function flatten()
144
            {
145 5
                $this->initial = $this->toArray();
146 5
                $this->changed = [];
147 5
                return $this;
148
            }
149
        };
150 16
        if ($empty) {
151 1
            return $entity;
152
        }
153 15
        $this->lazy($entity);
154 15
        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 32
    public function collection(TableQueryIterator $iterator, Table $definition) : Collection
164
    {
165 32
        return Collection::from($iterator)
166 32
            ->map(function ($v) use ($definition) {
167 32
                return $this->entity($definition, $v);
168 32
            });
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 5
    public function save($entity)
177
    {
178 5
        $query = $this->db->table($entity->definition()->getName());
179 5
        $primary = $entity->id();
180 5
        if (!isset($this->objects[$entity->definition()->getName()][base64_encode(serialize($primary))])) {
181 1
            $new = $query->insert($entity->toArray());
182 1
            $entity->fromArray($new);
183 1
            $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 5
        return $this->lazy($entity);
199
    }
200
    /**
201
     * Delete an entity from the database
202
     *
203
     * @param object $entity
204
     * @return void
205
     */
206 1
    public function delete($entity)
207
    {
208 1
        $query = $this->db->table($entity->definition()->getName());
209 1
        $primary = $entity->id();
210 1
        if (isset($this->objects[$entity->definition()->getName()][base64_encode(serialize($primary))])) {
211 1
            foreach ($primary as $k => $v) {
212 1
                $query->filter($k, $v);
213
            }
214 1
            $query->delete();
215 1
            unset($this->objects[$entity->definition()->getName()][base64_encode(serialize($primary))]);
216
        }
217 1
    }
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
        $entity->fromArray($query[0] ?? []);
232
        return $this->lazy($entity);
233
    }
234 19
    protected function lazy($entity)
235
    {
236 19
        $data = $entity->toArray();
237 19
        $primary = $entity->id();
238 19
        $definition = $entity->definition();
239 19
        foreach ($definition->getColumns() as $column) {
240 19
            if (!isset($data[$column])) {
241 19
                $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 19
                });
248
            }
249
        }
250 19
        foreach ($definition->getRelations() as $name => $relation) {
251 19
            if (isset($data[$name])) {
252
                $entity->{$name} = $relation->many ? 
253
                    array_map(function ($v) use ($relation) {
254
                        return $this->entity($relation->table, $v);
255
                    }, $data[$name]) :
256
                    $this->entity($relation->table, $data[$name]);
257
            } else {
258 19
                $entity->__lazyProperty($name, function (array $columns = null) use ($entity, $definition, $primary, $relation, $data) {
259 5
                    $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...
260 5
                    if ($columns !== null) {
261
                        $query->columns($columns);
262
                    }
263 5
                    if ($relation->sql) {
264
                        $query->where($relation->sql, $relation->par);
265
                    }
266 5
                    if ($relation->pivot) {
267 4
                        $nm = null;
268 4
                        foreach ($relation->table->getRelations() as $rname => $rdata) {
269 4
                            if ($rdata->pivot && $rdata->pivot->getName() === $relation->pivot->getName()) {
270 4
                                $nm = $rname;
271
                            }
272
                        }
273 4
                        if (!$nm) {
274
                            $nm = $definition->getName();
275
                            $relation->table->manyToMany(
276
                                $this->db->table($definition->getName()),
277
                                $relation->pivot,
278
                                $nm,
279
                                array_flip($relation->keymap),
280
                                $relation->pivot_keymap
281
                            );
282
                        }
283 4
                        foreach ($definition->getPrimaryKey() as $v) {
284 4
                            $query->filter($nm . '.' . $v, $data[$v] ?? null);
285
                        }
286
                    } else {
287 5
                        foreach ($relation->keymap as $k => $v) {
288 5
                            $query->filter($v, $entity->{$k} ?? null);
289
                        }
290
                    }
291 5
                    return $relation->many ?
292 5
                        $query->iterator() :
293 5
                        $query[0];
294 19
                });
295
            }
296
        }
297 19
        return $entity;
298
    }
299
}