Completed
Push — master ( ad7ac0...17d567 )
by Oscar
01:25
created

Table   F

Complexity

Total Complexity 63

Size/Duplication

Total Lines 412
Duplicated Lines 0 %

Coupling/Cohesion

Components 3
Dependencies 17

Importance

Changes 0
Metric Value
wmc 63
lcom 3
cbo 17
dl 0
loc 412
rs 3.36
c 0
b 0
f 0

33 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 17 2
A init() 0 4 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 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()
59
    {
60
61
    }
62
63
    public function __debugInfo(): array
64
    {
65
        return [
66
            'name' => $this->name,
67
            'fields' => $this->fields,
68
        ];
69
    }
70
71
    public function __toString()
72
    {
73
        return "`{$this->name}`";
74
    }
75
76
    public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): self
77
    {
78
        $this->eventDispatcher = $eventDispatcher;
79
80
        return $this;
81
    }
82
83
    public function getEventDispatcher(): EventDispatcherInterface
84
    {
85
        if ($this->eventDispatcher === null) {
86
            $this->eventDispatcher = new Dispatcher();
87
        }
88
89
        return $this->eventDispatcher;
90
    }
91
92
    /**
93
     * Get the devault values used in new rows
94
     */
95
    public function getDefaults(array $overrides = null): array
96
    {
97
        if (empty($overrides)) {
98
            return $this->defaults;
99
        }
100
101
        $diff = array_diff_key($overrides, $this->fields);
102
103
        if (!empty($diff)) {
104
            throw new SimpleCrudException(
105
                sprintf('The field %s does not exist in the table %s', implode(array_keys($diff)), $this)
106
            );
107
        }
108
109
        return $overrides + $this->defaults;
110
    }
111
112
    /** 
113
     * Format selected data from the database
114
    */
115
    public function format(array $values): array
116
    {
117
        foreach ($this->fields as $name => $field) {
118
            if (array_key_exists($name, $values)) {
119
                $values[$name] = $field->format($values[$name]);
120
            }
121
        }
122
123
        return $values;
124
    }
125
126
    /**
127
     * Store a row in the cache.
128
     */
129
    public function cache(Row $row): Row
130
    {
131
        if ($row->id) {
132
            $this->cache[$row->id] = $row;
133
        }
134
135
        return $row;
136
    }
137
138
    /**
139
     * Clear the current cache.
140
     */
141
    public function clearCache(): self
142
    {
143
        $this->cache = [];
144
145
        return $this;
146
    }
147
148
    /**
149
     * Returns whether the id is cached or not
150
     * @param mixed $id
151
     */
152
    public function isCached($id): bool
153
    {
154
        return array_key_exists($id, $this->cache);
155
    }
156
157
    /**
158
     * Returns a row from the cache.
159
     * @param mixed $id
160
     */
161
    public function getCached($id): ?Row
162
    {
163
        if (!$this->isCached($id)) {
164
            return null;
165
        }
166
167
        $row = $this->cache[$id];
168
169
        if ($row && !$row->id) {
170
            return $this->cache[$id] = null;
171
        }
172
173
        return $row;
174
    }
175
176
    public function delete(): Delete
177
    {
178
        $query = new Delete($this);
179
180
        if ($eventDispatcher = $this->getEventDispatcher()) {
181
            $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...
182
        }
183
184
        return $query;
185
    }
186
187
    public function insert(array $data = []): Insert
188
    {
189
        $query = new Insert($this, $data);
190
191
        if ($eventDispatcher = $this->getEventDispatcher()) {
192
            $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...
193
        }
194
195
        return $query;
196
    }
197
198
    public function select(): Select
199
    {
200
        $query = new Select($this);
201
202
        if ($eventDispatcher = $this->getEventDispatcher()) {
203
            $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...
204
        }
205
206
        return $query;
207
    }
208
209
    public function selectAggregate(string $function, string $field = 'id'): SelectAggregate
210
    {
211
        $query = new SelectAggregate($this, $function, $field);
212
213
        if ($eventDispatcher = $this->getEventDispatcher()) {
214
            $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...
215
        }
216
217
        return $query;
218
    }
219
220
    public function update(array $data = []): Update
221
    {
222
        $query = new Update($this, $data);
223
224
        if ($eventDispatcher = $this->getEventDispatcher()) {
225
            $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...
226
        }
227
228
        return $query;
229
    }
230
231
    /**
232
     * Magic method to get the Field instance of a table field
233
     */
234
    public function __get(string $name): Field
235
    {
236
        if (!isset($this->fields[$name])) {
237
            throw new SimpleCrudException(
238
                sprintf('The field `%s` does not exist in the table %s', $name, $this)
239
            );
240
        }
241
242
        return $this->fields[$name];
243
    }
244
245
    /**
246
     * Magic method to check if a field exists or not.
247
     */
248
    public function __isset(string $name): bool
249
    {
250
        return isset($this->fields[$name]);
251
    }
252
253
    /**
254
     * @see Countable
255
     */
256
    public function count(): int
257
    {
258
        return $this->selectAggregate('COUNT')->run();
259
    }
260
261
    /**
262
     * Check if a row with a specific id exists.
263
     *
264
     * @see ArrayAccess
265
     * @param mixed $offset
266
     */
267
    public function offsetExists($offset): bool
268
    {
269
        if ($this->isCached($offset)) {
270
            return $this->getCached($offset) !== null;
271
        }
272
273
        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...
274
            ->where('id = ', $offset)
275
            ->limit(1)
276
            ->run() === 1;
277
    }
278
279
    /**
280
     * Returns a row with a specific id.
281
     *
282
     * @see ArrayAccess
283
     *
284
     * @param mixed $offset
285
     */
286
    public function offsetGet($offset): ?Row
287
    {
288
        if ($this->isCached($offset)) {
289
            return $this->getCached($offset);
290
        }
291
292
        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...
293
            ->one()
294
            ->where('id = ', $offset)
295
            ->run();
296
    }
297
298
    /**
299
     * Store a row with a specific id.
300
     *
301
     * @see ArrayAccess
302
     * @param mixed $offset
303
     * @param mixed $value
304
     */
305
    public function offsetSet($offset, $value): Row
306
    {
307
        //Insert on missing offset
308
        if ($offset === null) {
309
            $value['id'] = null;
310
311
            return $this->create($value)->save();
312
        }
313
314
        //Update if the element is cached and exists
315
        $row = $this->getCached($offset);
316
317
        if ($row) {
318
            return $row->edit($value)->save();
319
        }
320
321
        //Update if the element it's not cached
322
        if (!$this->isCached($row)) {
323
            $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...
324
                ->columns($value)
325
                ->where('id = ', $offset)
326
                ->run();
327
        }
328
    }
329
330
    /**
331
     * Remove a row with a specific id.
332
     *
333
     * @see ArrayAccess
334
     * @param mixed $offset
335
     */
336
    public function offsetUnset($offset)
337
    {
338
        $this->cache[$offset] = null;
339
340
        $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...
341
            ->where('id = ', $offset)
342
            ->run();
343
    }
344
345
    /**
346
     * Returns the table name.
347
     */
348
    public function getName(): string
349
    {
350
        return $this->name;
351
    }
352
353
    /**
354
     * Returns the foreign key name.
355
     */
356
    public function getForeignKey(): string
357
    {
358
        return "{$this->name}_id";
359
    }
360
361
    /**
362
     * Returns the foreign key.
363
     */
364
    public function getJoinField(Table $table): ?Field
365
    {
366
        $field = $table->getForeignKey();
367
368
        return $this->fields[$field] ?? null;
369
    }
370
371
    public function getJoinTable(Table $table): ?Table
372
    {
373
        $name1 = $this->getName();
374
        $name2 = $table->getName();
375
        $name = $name1 < $name2 ? "{$name1}_{$name2}" : "{$name2}_{$name1}";
376
377
        $joinTable = $this->db->{$name} ?? null;
378
379
        if ($joinTable && $joinTable->getJoinField($this) && $joinTable->getJoinField($table)) {
380
            return $joinTable;
381
        }
382
383
        return null;
384
    }
385
386
    /**
387
     * Returns the Database instance associated with this table.
388
     */
389
    public function getDatabase(): Database
390
    {
391
        return $this->db;
392
    }
393
394
    /**
395
     * Returns all fields.
396
     *
397
     * @return Field[]
398
     */
399
    public function getFields()
400
    {
401
        return $this->fields;
402
    }
403
404
    public function create(array $data = []): Row
405
    {
406
        if (isset($data['id']) && ($row = $this->getCached($data['id']))) {
407
            return $row;
408
        }
409
410
        $eventDispatcher = $this->getEventDispatcher();
411
412
        if ($eventDispatcher) {
413
            $event = new BeforeCreateRow($data);
414
            $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...
415
            $data = $event->getData();
416
        }
417
418
        $class = self::ROW_CLASS;
419
        return $this->cache(new $class($this, $data));
420
    }
421
422
    public function createCollection(array $rows = []): RowCollection
423
    {
424
        $class = self::ROWCOLLECTION_CLASS;
425
        return new $class($this, ...$rows);
426
    }
427
428
    public function createField(array $info): Field
429
    {
430
        foreach ($this->fieldFactories as $fieldFactory) {
431
            if ($field = $fieldFactory->create($this, $info)) {
432
                return $field;
433
            }
434
        }
435
436
        return new Field($this, $info);
437
    }
438
}
439