Completed
Push — master ( c3b1de...399698 )
by Oscar
01:38
created

Table   F

Complexity

Total Complexity 67

Size/Duplication

Total Lines 440
Duplicated Lines 0 %

Coupling/Cohesion

Components 3
Dependencies 17

Importance

Changes 0
Metric Value
wmc 67
lcom 3
cbo 17
dl 0
loc 440
rs 3.04
c 0
b 0
f 0

35 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 17 2
A init() 0 3 1
A __debugInfo() 0 7 1
A __toString() 0 4 1
A setEventDispatcher() 0 6 1
A getEventDispatcher() 0 8 2
A getDefaults() 0 16 3
A format() 0 10 3
A cache() 0 8 2
A clearCache() 0 6 1
A isCached() 0 4 1
A getCached() 0 14 4
A delete() 0 10 2
A insert() 0 10 2
A select() 0 10 2
A selectAggregate() 0 10 2
A update() 0 10 2
A __get() 0 10 2
A __isset() 0 4 1
A count() 0 4 1
A offsetExists() 0 11 2
A offsetGet() 0 11 2
A offsetSet() 0 24 4
A offsetUnset() 0 8 1
A getName() 0 4 1
A getForeignKey() 0 4 1
A getJoinField() 0 6 1
A getJoinTable() 0 14 5
A getDatabase() 0 4 1
A getFields() 0 4 1
A __call() 0 6 1
A getOrCreate() 0 18 3
A create() 0 17 4
A createCollection() 0 5 1
A createField() 0 10 3

How to fix   Complexity   

Complex Class

Complex classes like Table 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 Table, and based on these observations, apply Extract Interface, too.

1
<?php
2
declare(strict_types = 1);
3
4
namespace SimpleCrud;
5
6
use ArrayAccess;
7
use Countable;
8
use Psr\EventDispatcher\EventDispatcherInterface;
9
use SimpleCrud\EventDispatcher\Dispatcher;
10
use SimpleCrud\Events\BeforeCreateRow;
11
use SimpleCrud\Events\CreateDeleteQuery;
12
use SimpleCrud\Events\CreateInsertQuery;
13
use SimpleCrud\Events\CreateSelectQuery;
14
use SimpleCrud\Events\CreateUpdateQuery;
15
use SimpleCrud\Fields\Field;
16
use SimpleCrud\Queries\Delete;
17
use SimpleCrud\Queries\Insert;
18
use SimpleCrud\Queries\Select;
19
use SimpleCrud\Queries\SelectAggregate;
20
use SimpleCrud\Queries\Update;
21
22
/**
23
 * Manages a database table.
24
 *
25
 * @property Field $id
26
 */
27
class Table implements ArrayAccess, Countable
28
{
29
    private $name;
30
    private $db;
31
    private $cache = [];
32
    private $fields = [];
33
    private $defaults = [];
34
    private $eventDispatcher;
35
    private $fieldFactories;
36
37
    protected const ROW_CLASS = Row::class;
38
    protected const ROWCOLLECTION_CLASS = RowCollection::class;
39
40
    final public function __construct(Database $db, string $name)
41
    {
42
        $this->db = $db;
43
        $this->name = $name;
44
45
        $this->fieldFactories = $db->getFieldFactories();
46
        $fields = $db->getScheme()->getTableFields($name);
47
48
        foreach ($fields as $info) {
49
            $field = $this->createField($info);
50
51
            $this->fields[$field->getName()] = $field;
52
            $this->defaults[$field->getName()] = null;
53
        }
54
55
        $this->init();
56
    }
57
58
    protected function init(): void
59
    {
60
    }
61
62
    public function __debugInfo(): array
63
    {
64
        return [
65
            'name' => $this->name,
66
            'fields' => $this->fields,
67
        ];
68
    }
69
70
    public function __toString()
71
    {
72
        return "`{$this->name}`";
73
    }
74
75
    public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): self
76
    {
77
        $this->eventDispatcher = $eventDispatcher;
78
79
        return $this;
80
    }
81
82
    public function getEventDispatcher(): EventDispatcherInterface
83
    {
84
        if ($this->eventDispatcher === null) {
85
            $this->eventDispatcher = new Dispatcher();
86
        }
87
88
        return $this->eventDispatcher;
89
    }
90
91
    /**
92
     * Get the devault values used in new rows
93
     */
94
    public function getDefaults(array $overrides = null): array
95
    {
96
        if (empty($overrides)) {
97
            return $this->defaults;
98
        }
99
100
        $diff = array_diff_key($overrides, $this->fields);
101
102
        if (!empty($diff)) {
103
            throw new SimpleCrudException(
104
                sprintf('The field %s does not exist in the table %s', implode(array_keys($diff)), $this)
105
            );
106
        }
107
108
        return $overrides + $this->defaults;
109
    }
110
111
    /**
112
     * Format selected data from the database
113
    */
114
    public function format(array $values): array
115
    {
116
        foreach ($this->fields as $name => $field) {
117
            if (array_key_exists($name, $values)) {
118
                $values[$name] = $field->format($values[$name]);
119
            }
120
        }
121
122
        return $values;
123
    }
124
125
    /**
126
     * Store a row in the cache.
127
     */
128
    public function cache(Row $row): Row
129
    {
130
        if ($row->id) {
131
            $this->cache[$row->id] = $row;
132
        }
133
134
        return $row;
135
    }
136
137
    /**
138
     * Clear the current cache.
139
     */
140
    public function clearCache(): self
141
    {
142
        $this->cache = [];
143
144
        return $this;
145
    }
146
147
    /**
148
     * Returns whether the id is cached or not
149
     * @param mixed $id
150
     */
151
    public function isCached($id): bool
152
    {
153
        return array_key_exists($id, $this->cache);
154
    }
155
156
    /**
157
     * Returns a row from the cache.
158
     * @param mixed $id
159
     */
160
    public function getCached($id): ?Row
161
    {
162
        if (!$this->isCached($id)) {
163
            return null;
164
        }
165
166
        $row = $this->cache[$id];
167
168
        if ($row && !$row->id) {
169
            return $this->cache[$id] = null;
170
        }
171
172
        return $row;
173
    }
174
175
    public function delete(): Delete
176
    {
177
        $query = new Delete($this);
178
179
        if ($eventDispatcher = $this->getEventDispatcher()) {
180
            $eventDispatcher->dispatch(new CreateDeleteQuery($query));
0 ignored issues
show
Documentation introduced by
new \SimpleCrud\Events\CreateDeleteQuery($query) is of type object<SimpleCrud\Events\CreateDeleteQuery>, but the function expects a object<Psr\EventDispatcher\object>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
181
        }
182
183
        return $query;
184
    }
185
186
    public function insert(array $data = []): Insert
187
    {
188
        $query = new Insert($this, $data);
189
190
        if ($eventDispatcher = $this->getEventDispatcher()) {
191
            $eventDispatcher->dispatch(new CreateInsertQuery($query));
0 ignored issues
show
Documentation introduced by
new \SimpleCrud\Events\CreateInsertQuery($query) is of type object<SimpleCrud\Events\CreateInsertQuery>, but the function expects a object<Psr\EventDispatcher\object>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
192
        }
193
194
        return $query;
195
    }
196
197
    public function select(): Select
198
    {
199
        $query = new Select($this);
200
201
        if ($eventDispatcher = $this->getEventDispatcher()) {
202
            $eventDispatcher->dispatch(new CreateSelectQuery($query));
0 ignored issues
show
Documentation introduced by
new \SimpleCrud\Events\CreateSelectQuery($query) is of type object<SimpleCrud\Events\CreateSelectQuery>, but the function expects a object<Psr\EventDispatcher\object>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
203
        }
204
205
        return $query;
206
    }
207
208
    public function selectAggregate(string $function, string $field = 'id'): SelectAggregate
209
    {
210
        $query = new SelectAggregate($this, $function, $field);
211
212
        if ($eventDispatcher = $this->getEventDispatcher()) {
213
            $eventDispatcher->dispatch(new CreateSelectQuery($query));
0 ignored issues
show
Documentation introduced by
new \SimpleCrud\Events\CreateSelectQuery($query) is of type object<SimpleCrud\Events\CreateSelectQuery>, but the function expects a object<Psr\EventDispatcher\object>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
214
        }
215
216
        return $query;
217
    }
218
219
    public function update(array $data = []): Update
220
    {
221
        $query = new Update($this, $data);
222
223
        if ($eventDispatcher = $this->getEventDispatcher()) {
224
            $eventDispatcher->dispatch(new CreateUpdateQuery($query));
0 ignored issues
show
Documentation introduced by
new \SimpleCrud\Events\CreateUpdateQuery($query) is of type object<SimpleCrud\Events\CreateUpdateQuery>, but the function expects a object<Psr\EventDispatcher\object>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
225
        }
226
227
        return $query;
228
    }
229
230
    /**
231
     * Magic method to get the Field instance of a table field
232
     */
233
    public function __get(string $name): Field
234
    {
235
        if (!isset($this->fields[$name])) {
236
            throw new SimpleCrudException(
237
                sprintf('The field `%s` does not exist in the table %s', $name, $this)
238
            );
239
        }
240
241
        return $this->fields[$name];
242
    }
243
244
    /**
245
     * Magic method to check if a field exists or not.
246
     */
247
    public function __isset(string $name): bool
248
    {
249
        return isset($this->fields[$name]);
250
    }
251
252
    /**
253
     * @see Countable
254
     */
255
    public function count(): int
256
    {
257
        return $this->selectAggregate('COUNT')->run();
258
    }
259
260
    /**
261
     * Check if a row with a specific id exists.
262
     *
263
     * @see ArrayAccess
264
     * @param mixed $offset
265
     */
266
    public function offsetExists($offset): bool
267
    {
268
        if ($this->isCached($offset)) {
269
            return $this->getCached($offset) !== null;
270
        }
271
272
        return $this->selectAggregate('COUNT')
0 ignored issues
show
Documentation Bug introduced by
The method where does not exist on object<SimpleCrud\Queries\SelectAggregate>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
273
            ->where('id = ', $offset)
274
            ->limit(1)
275
            ->run() === 1;
276
    }
277
278
    /**
279
     * Returns a row with a specific id.
280
     *
281
     * @see ArrayAccess
282
     *
283
     * @param mixed $offset
284
     */
285
    public function offsetGet($offset): ?Row
286
    {
287
        if ($this->isCached($offset)) {
288
            return $this->getCached($offset);
289
        }
290
291
        return $this->cache[$offset] = $this->select()
0 ignored issues
show
Documentation Bug introduced by
The method where does not exist on object<SimpleCrud\Queries\Select>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
292
            ->one()
293
            ->where('id = ', $offset)
294
            ->run();
295
    }
296
297
    /**
298
     * Store a row with a specific id.
299
     *
300
     * @see ArrayAccess
301
     * @param mixed $offset
302
     * @param mixed $value
303
     */
304
    public function offsetSet($offset, $value): Row
305
    {
306
        //Insert on missing offset
307
        if ($offset === null) {
308
            $value['id'] = null;
309
310
            return $this->create($value)->save();
311
        }
312
313
        //Update if the element is cached and exists
314
        $row = $this->getCached($offset);
315
316
        if ($row) {
317
            return $row->edit($value)->save();
318
        }
319
320
        //Update if the element it's not cached
321
        if (!$this->isCached($row)) {
322
            $this->update()
0 ignored issues
show
Documentation Bug introduced by
The method columns does not exist on object<SimpleCrud\Queries\Update>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
323
                ->columns($value)
324
                ->where('id = ', $offset)
325
                ->run();
326
        }
327
    }
328
329
    /**
330
     * Remove a row with a specific id.
331
     *
332
     * @see ArrayAccess
333
     * @param mixed $offset
334
     */
335
    public function offsetUnset($offset)
336
    {
337
        $this->cache[$offset] = null;
338
339
        $this->delete()
0 ignored issues
show
Documentation Bug introduced by
The method where does not exist on object<SimpleCrud\Queries\Delete>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
340
            ->where('id = ', $offset)
341
            ->run();
342
    }
343
344
    /**
345
     * Returns the table name.
346
     */
347
    public function getName(): string
348
    {
349
        return $this->name;
350
    }
351
352
    /**
353
     * Returns the foreign key name.
354
     */
355
    public function getForeignKey(): string
356
    {
357
        return "{$this->name}_id";
358
    }
359
360
    /**
361
     * Returns the foreign key.
362
     */
363
    public function getJoinField(Table $table): ?Field
364
    {
365
        $field = $table->getForeignKey();
366
367
        return $this->fields[$field] ?? null;
368
    }
369
370
    public function getJoinTable(Table $table): ?Table
371
    {
372
        $name1 = $this->getName();
373
        $name2 = $table->getName();
374
        $name = $name1 < $name2 ? "{$name1}_{$name2}" : "{$name2}_{$name1}";
375
376
        $joinTable = $this->db->{$name} ?? null;
377
378
        if ($joinTable && $joinTable->getJoinField($this) && $joinTable->getJoinField($table)) {
379
            return $joinTable;
380
        }
381
382
        return null;
383
    }
384
385
    /**
386
     * Returns the Database instance associated with this table.
387
     */
388
    public function getDatabase(): Database
389
    {
390
        return $this->db;
391
    }
392
393
    /**
394
     * Returns all fields.
395
     *
396
     * @return Field[]
397
     */
398
    public function getFields()
399
    {
400
        return $this->fields;
401
    }
402
403
    public function __call(string $name, array $args): ?Row
404
    {
405
        $field = $this->__get($name);
406
407
        return $this->select()->one()->where("{$field} = ", $args[0])->run();
0 ignored issues
show
Documentation Bug introduced by
The method where does not exist on object<SimpleCrud\Queries\Select>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
408
    }
409
410
    /**
411
     * Search a row with some values or create one if it does not exist
412
     */
413
    public function getOrCreate(array $data): Row
414
    {
415
        $query = $this->select()->one();
416
417
        foreach ($data as $name => $value) {
418
            $field = $this->__get($name);
419
420
            $query->where("{$field} = ", $value);
0 ignored issues
show
Documentation Bug introduced by
The method where does not exist on object<SimpleCrud\Queries\Select>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
421
        }
422
423
        $row = $query->run();
424
425
        if ($row) {
426
            return $row;
427
        }
428
429
        return $this->create($data);
430
    }
431
432
    public function create(array $data = []): Row
433
    {
434
        if (isset($data['id']) && ($row = $this->getCached($data['id']))) {
435
            return $row;
436
        }
437
438
        $eventDispatcher = $this->getEventDispatcher();
439
440
        if ($eventDispatcher) {
441
            $event = new BeforeCreateRow($data);
442
            $eventDispatcher->dispatch($event);
0 ignored issues
show
Documentation introduced by
$event is of type object<SimpleCrud\Events\BeforeCreateRow>, but the function expects a object<Psr\EventDispatcher\object>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
443
            $data = $event->getData();
444
        }
445
446
        $class = static::ROW_CLASS;
447
        return $this->cache(new $class($this, $data));
448
    }
449
450
    public function createCollection(array $rows = []): RowCollection
451
    {
452
        $class = static::ROWCOLLECTION_CLASS;
453
        return new $class($this, ...$rows);
454
    }
455
456
    public function createField(array $info): Field
457
    {
458
        foreach ($this->fieldFactories as $fieldFactory) {
459
            if ($field = $fieldFactory->create($this, $info)) {
460
                return $field;
461
            }
462
        }
463
464
        return new Field($this, $info);
465
    }
466
}
467