Completed
Push — master ( 2a3f7a...4081a1 )
by Oscar
02:17
created

Table::offsetExists()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 11
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 7
nc 2
nop 1
1
<?php
2
3
namespace SimpleCrud;
4
5
use ArrayAccess;
6
use Closure;
7
8
/**
9
 * Manages a database table.
10
 */
11
class Table implements ArrayAccess
12
{
13
    private $name;
14
    private $db;
15
    private $row;
16
    private $rowCollection;
17
    private $cache = [];
18
    private $queriesModifiers = [];
19
    private $fields = [];
20
21
    /**
22
     * Constructor.
23
     *
24
     * @param SimpleCrud $db
25
     * @param string     $name
26
     */
27
    final public function __construct(SimpleCrud $db, $name)
28
    {
29
        $this->db = $db;
30
        $this->name = $name;
31
32
        $this->setRow(new Row($this));
33
        $this->setRowCollection(new RowCollection($this));
34
35
        $fieldFactory = $db->getFieldFactory();
36
37
        foreach (array_keys($this->getScheme()['fields']) as $name) {
38
            $this->fields[$name] = $fieldFactory->get($this, $name);
39
        }
40
41
        $this->init();
42
    }
43
44
    /**
45
     * Debug info.
46
     * 
47
     * @return array
48
     */
49
    public function __debugInfo()
50
    {
51
        return [
52
            'name' => $this->name,
53
            'fields' => $this->fields,
54
        ];
55
    }
56
57
    /**
58
     * Callback used to init the table.
59
     */
60
    protected function init()
61
    {
62
    }
63
64
    /**
65
     * Store a row in the cache.
66
     * 
67
     * @param Row $row
68
     */
69
    public function cache(Row $row)
70
    {
71
        $this->cache[$row->id] = $row;
72
    }
73
74
    /**
75
     * Clear the current cache.
76
     */
77
    public function clearCache()
78
    {
79
        $this->cache = [];
80
    }
81
82
    /**
83
     * Register a new query modifier.
84
     * 
85
     * @param string   $name
86
     * @param callable $modifier
87
     */
88
    public function addQueryModifier($name, callable $modifier)
89
    {
90
        if (!isset($this->queriesModifiers[$name])) {
91
            $this->queriesModifiers[$name] = [];
92
        }
93
94
        $this->queriesModifiers[$name][] = $modifier;
95
    }
96
97
    /**
98
     * Magic method to create queries related with this table.
99
     *
100
     * @param string $name
101
     * @param array  $arguments
102
     *
103
     * @throws SimpleCrudException
104
     *
105
     * @return Queries\Query|null
106
     */
107
    public function __call($name, $arguments)
108
    {
109
        $query = $this->getDatabase()->getQueryFactory()->get($this, $name);
110
111
        if (isset($this->queriesModifiers[$name])) {
112
            foreach ($this->queriesModifiers[$name] as $modifier) {
113
                $modifier($query);
114
            }
115
        }
116
117
        return $query;
118
    }
119
120
    /**
121
     * Magic method to get the Field instance of a table field
122
     *
123
     * @param string $name The field name
124
     *
125
     * @throws SimpleCrudException
126
     *
127
     * @return Fields\Field
128
     */
129
    public function __get($name)
130
    {
131
        if (!isset($this->fields[$name])) {
132
            throw new SimpleCrudException(sprintf('The field "%s" does not exist in the table "%s"', $name, $this->name));
133
        }
134
135
        return $this->fields[$name];
136
    }
137
138
    /**
139
     * Magic method to check if a field exists or not.
140
     *
141
     * @param string $name
142
     *
143
     * @return bool
144
     */
145
    public function __isset($name)
146
    {
147
        return isset($this->fields[$name]);
148
    }
149
150
    /**
151
     * Check if a row with a specific id exists.
152
     *
153
     * @see ArrayAccess
154
     *
155
     * @return bool
156
     */
157
    public function offsetExists($offset)
158
    {
159
        if (array_key_exists($offset, $this->cache)) {
160
            return !empty($this->cache[$offset]);
161
        }
162
163
        return $this->count()
0 ignored issues
show
Documentation Bug introduced by
The method count does not exist on object<SimpleCrud\Table>? 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...
164
            ->byId($offset)
165
            ->limit(1)
166
            ->run() === 1;
167
    }
168
169
    /**
170
     * Returns a row with a specific id.
171
     *
172
     * @see ArrayAccess
173
     *
174
     * @return Row|null
175
     */
176
    public function offsetGet($offset)
177
    {
178
        if (array_key_exists($offset, $this->cache)) {
179
            return $this->cache[$offset];
180
        }
181
182
        return $this->cache[$offset] = $this->select()
0 ignored issues
show
Documentation Bug introduced by
The method select does not exist on object<SimpleCrud\Table>? 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...
183
            ->one()
184
            ->byId($offset)
185
            ->run();
186
    }
187
188
    /**
189
     * Store a row with a specific id.
190
     *
191
     * @see ArrayAccess
192
     */
193
    public function offsetSet($offset, $value)
194
    {
195
        //Insert on missing offset
196
        if ($offset === null) {
197
            $value['id'] = null;
198
199
            $this->insert()
0 ignored issues
show
Documentation Bug introduced by
The method insert does not exist on object<SimpleCrud\Table>? 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...
200
                ->data($value)
201
                ->run();
202
203
            return;
204
        }
205
206
        //Update if the element is cached
207
        if (isset($this->cache[$offset])) {
208
            $row = $this->cache[$offset];
209
210
            foreach ($value as $name => $val) {
211
                $row->$name = $val;
212
            }
213
214
            $row->save();
215
216
            return;
217
        }
218
219
        //Update if the element it's not cached
220
        if ($this->offsetExists($offset)) {
221
            $this->update()
0 ignored issues
show
Documentation Bug introduced by
The method update does not exist on object<SimpleCrud\Table>? 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...
222
                ->data($value)
223
                ->byId($offset)
224
                ->limit(1)
225
                ->run();
226
        }
227
    }
228
229
    /**
230
     * Remove a row with a specific id.
231
     *
232
     * @see ArrayAccess
233
     */
234
    public function offsetUnset($offset)
235
    {
236
        unset($this->cache[$offset]);
237
238
        $this->delete()
0 ignored issues
show
Documentation Bug introduced by
The method delete does not exist on object<SimpleCrud\Table>? 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...
239
            ->byId($offset)
240
            ->limit(1)
241
            ->run();
242
    }
243
244
    /**
245
     * Returns the table name.
246
     *
247
     * @return string
248
     */
249
    public function getName()
250
    {
251
        return $this->name;
252
    }
253
254
    /**
255
     * Returns the SimpleCrud instance associated with this table.
256
     *
257
     * @return SimpleCrud
258
     */
259
    public function getDatabase()
260
    {
261
        return $this->db;
262
    }
263
264
    /**
265
     * Returns the table scheme.
266
     *
267
     * @return array
268
     */
269
    public function getScheme()
270
    {
271
        return $this->db->getScheme()[$this->name];
272
    }
273
274
    /**
275
     * Returns an attribute.
276
     *
277
     * @param string $name
278
     *
279
     * @return null|mixed
280
     */
281
    public function getAttribute($name)
282
    {
283
        return $this->db->getAttribute($name);
284
    }
285
286
    /**
287
     * Defines the Row class used by this table.
288
     *
289
     * @param Row $row
290
     */
291
    public function setRow(Row $row)
292
    {
293
        $this->row = $row;
294
    }
295
296
    /**
297
     * Register a custom method to the row.
298
     *
299
     * @param string  $name
300
     * @param Closure $method
301
     *
302
     * @return self
303
     */
304
    public function setRowMethod($name, Closure $method)
305
    {
306
        $this->row->setMethod($name, $method);
307
308
        return $this;
309
    }
310
311
    /**
312
     * Defines the RowCollection class used by this table.
313
     *
314
     * @param RowCollection $rowCollection
315
     */
316
    public function setRowCollection(RowCollection $rowCollection)
317
    {
318
        $this->rowCollection = $rowCollection;
319
    }
320
321
    /**
322
     * Register a custom method to the rowCollections.
323
     *
324
     * @param string  $name
325
     * @param Closure $method
326
     *
327
     * @return self
328
     */
329
    public function setRowCollectionMethod($name, Closure $method)
330
    {
331
        $this->rowCollection->setMethod($name, $method);
332
333
        return $this;
334
    }
335
336
    /**
337
     * Creates a new row instance.
338
     *
339
     * @param array $data The values of the row
340
     *
341
     * @return Row
342
     */
343
    public function create(array $data = [])
344
    {
345 View Code Duplication
        if (isset($data['id']) && isset($this->cache[$data['id']]) && is_object($this->cache[$data['id']])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
346
            return $this->cache[$data['id']];
347
        }
348
349
        $row = clone $this->row;
350
351
        foreach ($data as $name => $value) {
352
            $row->$name = $value;
353
        }
354
355
        return $row;
356
    }
357
358
    /**
359
     * Creates a new rowCollection instance.
360
     *
361
     * @param array $data Rows added to this collection
362
     *
363
     * @return RowCollection
364
     */
365
    public function createCollection(array $data = [])
366
    {
367
        $rowCollection = clone $this->rowCollection;
368
369
        foreach ($data as $row) {
370
            $rowCollection[] = $row;
371
        }
372
373
        return $rowCollection;
374
    }
375
376
    /**
377
     * Default data converter/validator from database.
378
     *
379
     * @param array $data The values before insert to database
380
     * @param bool  $new  True for inserts, false for updates
381
     */
382
    public function dataToDatabase(array $data, $new)
0 ignored issues
show
Unused Code introduced by
The parameter $new is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
383
    {
384
        return $data;
385
    }
386
387
    /**
388
     * Default data converter from database.
389
     *
390
     * @param array $data The database format values
391
     */
392
    public function dataFromDatabase(array $data)
393
    {
394
        return $data;
395
    }
396
397
    /**
398
     * Prepares the data from the result of a database selection.
399
     *
400
     * @param array $data
401
     *
402
     * @return array
403
     */
404
    public function createFromDatabase(array $data)
405
    {
406
        //Get from cache
407 View Code Duplication
        if (isset($this->cache[$data['id']]) && is_object(isset($this->cache[$data['id']]))) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
408
            return $this->cache[$data['id']];
409
        }
410
411
        foreach ($this->fields as $name => $field) {
412
            $data[$name] = $field->dataFromDatabase($data[$name]);
413
        }
414
415
        if (!is_array($data = $this->dataFromDatabase(array_intersect_key($data, $this->fields)))) {
416
            throw new SimpleCrudException('Data not valid');
417
        }
418
419
        $row = $this->create($data);
420
421
        $this->cache($row);
422
423
        return $row;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $row; (SimpleCrud\Row) is incompatible with the return type documented by SimpleCrud\Table::createFromDatabase of type array.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
424
    }
425
426
    /**
427
     * Prepares the data before save into database (used by update and insert).
428
     *
429
     * @param array $data
430
     * @param bool  $new
431
     *
432
     * @return array
433
     */
434
    public function prepareDataToDatabase(array $data, $new)
435
    {
436
        if (!is_array($data = $this->dataToDatabase($data, $new))) {
437
            throw new SimpleCrudException('Data not valid');
438
        }
439
440
        if (array_diff_key($data, $this->fields)) {
441
            throw new SimpleCrudException('Invalid fields');
442
        }
443
444
        //Transform data before save to database
445
        foreach ($data as $key => &$value) {
446
            $value = $this->fields[$key]->dataToDatabase($value);
447
        }
448
449
        return $data;
450
    }
451
}
452