GenericDataMapper::toArray()   D
last analyzed

Complexity

Conditions 17
Paths 8

Size

Total Lines 43
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 116.1269

Importance

Changes 0
Metric Value
cc 17
eloc 30
nc 8
nop 2
dl 0
loc 43
ccs 9
cts 30
cp 0.3
crap 116.1269
rs 4.9807
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
namespace vakata\orm;
3
4
use vakata\database\DBInterface;
5
6
/**
7
 * A generic class mapping an instance creation function to a table in the DB.
8
 */
9
// NOTICE: if not using Unit of Work toArray() and updatePivots() may fail if saving is not in the right order!!!
10
class GenericDataMapper implements DataMapper
11
{
12
    protected $manager;
13
    protected $db;
14
    protected $table;
15
    protected $create;
16
    protected $definition;
17
    protected $map = [];
18
    
19
    /**
20
     * Create an instance
21
     *
22
     * @param Manager $manager the manager object
23
     * @param DBInterface $db the database access object
24
     * @param string $table the table name to query
25
     * @param callable $create invoked with an array of fields when a new instance needs to be created
26
     */
27 14
    public function __construct(Manager $manager, DBInterface $db, string $table, callable $create) {
28 14
        $this->manager = $manager;
29 14
        $this->db = $db;
30 14
        $this->table = $table;
31 14
        $this->create = $create;
32 14
        $this->definition = $this->db->table($table)->getDefinition();
33 14
    }
34
35
    // READ METHODS
36 13
    protected function hash($data) : string
37
    {
38 13
        if (is_object($data)) {
39 4
            $data = $this->toArray($data, false);
40
        }
41 13
        $pkey = [];
42 13
        foreach ($this->definition->getPrimaryKey() as $field) {
43 13
            $pkey[$field] = $data[$field] ?? null;
44
        }
45 13
        return json_encode($pkey, JSON_UNESCAPED_UNICODE);
46
    }
47 13
    protected function instance(array $data = [])
48
    {
49 13
        return call_user_func($this->create, $data);
50
    }
51 13
    protected function populate($entity, array $data = [])
52
    {
53
        // populate basic columns
54 13
        foreach ($data as $field => $value) {
55
            try {
56 13
                $method = 'set' . ucfirst(strtolower($field));
57 13
                if (method_exists($entity, $method)) {
58
                    $entity->{$method}($value);
59
                } else {
60 13
                    $entity->{$field} = $value;
61
                }
62 13
            } catch (\Exception $ignore) {}
63
        }
64 13
    }
65 13
    protected function populateRelations($entity, array $data = [])
66
    {
67 13
        foreach ($this->definition->getRelations() as $name => $relation) {
68 13
            if (isset($data[$name])) {
69 1
                $mapper = $this->manager->getMapper($relation->table->getName());
70 1
                $entity->{$name} = $relation->many ? 
71
                    array_map(function ($v) use ($mapper) {
72
                        return $mapper->entity($v);
73
                    }, $data[$name]) :
74 1
                    $mapper->entity($data[$name]);
75
            } else {
76 13
                $query = $this->db->table($relation->table->getName());
77 13
                if ($relation->sql) {
78
                    $query->where($relation->sql, $relation->par);
79
                }
80 13
                if ($relation->pivot) {
81 8
                    $nm = null;
82 8
                    foreach ($relation->table->getRelations() as $rname => $rdata) {
83 8
                        if ($rdata->pivot && $rdata->pivot->getName() === $relation->pivot->getName()) {
84 8
                            $nm = $rname;
85
                        }
86
                    }
87 8
                    if (!$nm) {
88
                        $nm = $this->table->getName();
89
                        $relation->table->manyToMany(
90
                            $this->table,
91
                            $relation->pivot,
92
                            $nm,
93
                            array_flip($relation->keymap),
94
                            $relation->pivot_keymap
95
                        );
96
                    }
97 8
                    foreach ($this->definition->getPrimaryKey() as $v) {
98 8
                        $query->filter($nm . '.' . $v, $data[$v] ?? null);
99
                    }
100
                } else {
101 12
                    foreach ($relation->keymap as $k => $v) {
102 12
                        $query->filter($v, $data[$k] ?? null);
103
                    }
104
                }
105 13
                $query = $this->manager->fromQuery($query);
106 13
                if ($relation->many) {
107 13
                    $entity->{$name} = $query;
108
                } else {
109 6
                    if ($entity instanceof LazyLoadable) {
110 6
                        $entity->lazyProperty($name, function () use ($query) {
111 2
                            return $query[0];
112 6
                        });
113
                    } else {
114 13
                        $entity->{$name} = $query[0];
115
                    }
116
                }
117
            }
118
        }
119 13
    }
120
    /**
121
     * Convert an entity to an array of fields, optionally including relation fields. 
122
     *
123
     * @param mixed $entity the entity to convert
124
     * @param bool $relations should the 1 end of relations be included, defaults to `true`
125
     * @return array
126
     */
127 4
    public function toArray($entity, bool $relations = true) : array
128
    {
129 4
        $data = [];
130 4
        foreach ($this->definition->getColumns() as $column) {
131 4
            $method = 'get' . ucfirst(strtolower($column));
132 4
            if (method_exists($entity, $method)) {
133
                $data[$column] = $entity->{$method}();
134 4
            } else if (property_exists($entity, $column) || method_exists($entity, '__get')) {
135 4
                $data[$column] = $entity->{$column};
136
             }
137
        }
138
        // gather data from relations
139 4
        if ($relations) {
140
            foreach ($this->definition->getRelations() as $name => $relation) {
141
                if ($relation->many) {
142
                    continue;
143
                }
144
                $value = null;
145
                $method = 'get' . ucfirst(strtolower($name));
146
                if (method_exists($entity, $method)) {
147
                    $value = $entity->{$method}();
148
                } else if (property_exists($entity, $name) || method_exists($entity, '__get')) {
149
                    $value = $entity->{$name};
150
                } else {
151
                    continue;
152
                }
153
                $pkfields = $this->definition->getPrimaryKey();
154
                foreach ($relation->keymap as $local => $remote) {
155
                    if (!in_array($local, $pkfields)) {
156
                        $data[$local] = null;
157
                        $method = 'get' . ucfirst(strtolower($remote));
158
                        if (is_object($value)) {
159
                            if (method_exists($value, $method)) {
160
                                $data[$local] = $value->{$method}();
161
                            } else if (property_exists($value, $remote) || method_exists($value, '__get')) {
162
                                $data[$local] = $value->{$remote};
163
                            }
164
                        }
165
                    }
166
                }
167
            }
168
        }
169 4
        return $data;
170
    }
171
    /**
172
     * Get an entity from an array of fields
173
     *
174
     * @param array $row
175
     * @return mixed
176
     */
177 13
    public function entity(array $row)
178
    {
179
        // create a primary key hash
180 13
        $hash = $this->hash($row);
181 13
        if (isset($this->map[$hash])) {
182 10
            return $this->map[$hash];
183
        }
184
        // create an instance
185 13
        $entity = $this->instance($row);
186
        // populate basic fields
187 13
        $this->populate($entity, $row);
188
        // save in map (before fetching relations as some relations may be circular)
189 13
        $this->map[$hash] = $entity;
190
        // populate relations
191 13
        $this->populateRelations($entity, $row);
192
        // return entity
193 13
        return $entity;
194
    }
195
196
    // WRITE METHODS
197 3
    protected function updateRelations($entity, array $pkey)
198
    {
199 3
        foreach ($this->definition->getRelations() as $name => $relation) {
200 3
            if ($relation->pivot) {
201 1
                continue;
202
            }
203
            // only relations like book (with author_id)
204 3
            if (!count(array_diff(array_keys($relation->keymap), array_keys($pkey)))) {
205 3
                $data = null;
206 3
                $method = 'get' . ucfirst(strtolower($name));
207 3
                if (method_exists($entity, $method)) {
208
                    $data = $entity->{$method}();
209 3
                } else if (property_exists($entity, $name) || method_exists($entity, '__get')) {
210 3
                    $data = $entity->{$name};
211
                } else {
212
                    continue;
213
                }
214 3
                if (!isset($data)) {
215 1
                    continue;
216
                }
217 2
                $data = $relation->many ? $data : [ $data ];
218 2
                foreach ($data as $item) {
219 1
                    foreach ($relation->keymap as $local => $remote) {
220 1
                        if (isset($pkey[$local])) {
221
                            try {
222 1
                                $method = 'set' . ucfirst(strtolower($remote));
223 1
                                if (method_exists($item, $method)) {
224
                                    $item->{$method}($pkey[$local]);
225
                                } else {
226 1
                                    $item->{$remote} = $pkey[$local];
227
                                }
228 2
                            } catch (\Exception $ignore) {}
229
                        }
230
                    }
231
                }
232
            }
233
        }
234 3
    }
235 4
    protected function updatePivots($entity, array $pkey, bool $force = false)
236
    {
237 4
        foreach ($this->definition->getRelations() as $name => $relation) {
238 4
            if (!$relation->pivot) {
239 4
                continue;
240
            }
241 1
            $data = null;
242 1
            $method = 'get' . ucfirst(strtolower($name));
243 1
            if (method_exists($entity, $method)) {
244
                $data = $entity->{$method}();
245 1
            } else if (property_exists($entity, $name) || method_exists($entity, '__get')) {
246 1
                $data = $entity->{$name};
247
            } else {
248
                continue;
249
            }
250 1
            if (!isset($data)) {
251
                continue;
252
            }
253 1
            if ($force || !($data instanceof Repository) || $data->isModified()) {
254 1
                $query = $this->db->table($relation->pivot->getName());
255 1
                foreach ($relation->keymap as $local => $remote) {
256 1
                    $query->filter($remote, $pkey[$local]);
257
                }
258 1
                $query->delete();
259 1
                $insert = [];
260 1
                foreach ($relation->keymap as $local => $remote) {
261 1
                    $insert[$remote] = $pkey[$local];
262
                }
263 1
                foreach ($data as $item) {
264 1
                    $query->reset();
265 1
                    foreach ($relation->pivot_keymap as $local => $remote) {
266 1
                        $insert[$local] = null;
267 1
                        $method = 'get' . ucfirst(strtolower($remote));
268 1
                        if (method_exists($item, $method)) {
269
                            $insert[$local] = $item->{$method}();
270 1
                        } else if (property_exists($item, $remote) || method_exists($item, '__get')) {
271 1
                            $insert[$local] = $item->{$remote};
272
                        }
273
                    }
274 1
                    $query->insert($insert);
275
                }
276
            }
277
        }
278 4
    }
279 1
    protected function deleteRelations($entity, array $pkey)
280
    {
281 1
        foreach ($this->definition->getRelations() as $name => $relation) {
282 1
            if (!count(array_diff(array_keys($relation->keymap), array_keys($pkey)))) {
283 1
                if ($relation->pivot) {
284 1
                    $query = $this->db->table($relation->pivot->getName());
285 1
                    foreach ($relation->keymap as $local => $remote) {
286 1
                        $query->filter($remote, $pkey[$local] ?? null);
287
                    }
288 1
                    $query->delete();
289
                } else {
290 1
                    $data = null;
291 1
                    $method = 'get' . ucfirst(strtolower($name));
292 1
                    if (method_exists($entity, $method)) {
293
                        $data = $entity->{$method}();
294 1
                    } else if (property_exists($entity, $name) || method_exists($entity, '__get')) {
295 1
                        $data = $entity->{$name};
296
                    } else {
297
                        continue;
298
                    }
299 1
                    if (!isset($data)) {
300
                        continue;
301
                    }
302 1
                    $repository = $this->manager->fromTable(
303 1
                        $relation->pivot ? $relation->pivot->getName() : $relation->table->getName()
304
                    );
305 1
                    $data = $relation->many ? $data : [ $data ];
306 1
                    foreach ($data as $item) {
307 1
                        $repository->remove($item);
308
                    }
309
                }
310
            }
311
        }
312 1
    }
313
    /**
314
     * Insert an entity, returning the primary key fields and their value
315
     *
316
     * @param mixed $entity
317
     * @return array a key value map of the primary key columns
318
     */
319 2
    public function insert($entity) : array
320
    {
321 2
        $data = $this->toArray($entity, false);
322 2
        $pkey = $this->db->table($this->table)->insert($data);
323 2
        $this->populate($entity, $pkey);
324 2
        $this->map[$this->hash($entity)] = $entity;
325 2
        $this->updateRelations($entity, $pkey);
326 2
        $this->updatePivots($entity, $pkey);
327 2
        return $pkey;
328
    }
329
    /**
330
     * Update an entity
331
     *
332
     * @param mixed $entity
333
     * @return int the number of affected rows
334
     */
335 3
    public function update($entity) : array
336
    {
337
        // get the current primary key
338 3
        $hash = array_search($entity, $this->map, true);
339 3
        if ($hash === null || $hash === false) {
340
            $hash = $this->hash($entity);
341
            $this->map[$hash] = $entity;
342
        }
343 3
        $pkey = json_decode($hash, true);
344
        // create a query and filter to match primary key
345 3
        $query = $this->db->table($this->table);
346 3
        foreach ($this->definition->getPrimaryKey() as $field) {
347 3
            $query->filter($field, $pkey[$field] ?? null);
348
        }
349 3
        $data = $this->toArray($entity, false);
350 3
        $query->update($data);
351
        // check for primary key changes
352 3
        $newHash = $this->hash($entity);
353 3
        if ($hash !== $newHash) {
354 1
            unset($this->map[$hash]);
355 1
            $this->map[$newHash] = $entity;
356 1
            $this->updateRelations($entity, json_decode($newHash, true));
357
        }
358 3
        $this->updatePivots($entity, json_decode($newHash, true), $hash !== $newHash);
359 3
        return json_decode($newHash, true);
360
    }
361
    /**
362
     * Delete an entity
363
     *
364
     * @param mixed $entity
365
     * @return int the number of deleted rows
366
     */
367 1
    public function delete($entity) : array
368
    {
369
        // get current primary key
370 1
        $hash = array_search($entity, $this->map, true);
371 1
        if ($hash === null || $hash === false) {
372
            $hash = $this->hash($entity);
373
            $this->map[$hash] = $entity;
374
        }
375 1
        $pkey = json_decode($hash, true);
376
        // create a query and filter to match primary key
377 1
        $query = $this->db->table($this->table);
378 1
        foreach ($this->definition->getPrimaryKey() as $field) {
379 1
            $query->filter($field, $pkey[$field] ?? null);
380
        }
381 1
        $query->delete();
382 1
        unset($this->map[$hash]);
383
        // delete data in related tables (probably not needed with cascade FK)
384 1
        $this->deleteRelations($entity, $pkey);
385 1
        return $pkey;
386
    }
387
}
388