Completed
Push — master ( fe423d...ab6445 )
by Oscar
01:22
created

Table   F

Complexity

Total Complexity 61

Size/Duplication

Total Lines 401
Duplicated Lines 0 %

Coupling/Cohesion

Components 3
Dependencies 16

Importance

Changes 0
Metric Value
wmc 61
lcom 3
cbo 16
dl 0
loc 401
rs 3.52
c 0
b 0
f 0

32 Methods

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