Completed
Push — master ( 1d7365...c04903 )
by Oscar
07:16
created

Table::setRowMethod()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 6
rs 9.4285
cc 1
eloc 3
nc 1
nop 2
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 $db;
14
    private $row;
15
    private $rowCollection;
16
    private $cache = [];
17
18
    public $name;
19
    public $fields = [];
20
    public $queriesModifiers = [];
21
22
    /**
23
     * Constructor.
24
     *
25
     * @param SimpleCrud $db
26
     * @param string     $name
27
     */
28
    final public function __construct(SimpleCrud $db, $name)
29
    {
30
        $this->db = $db;
31
        $this->name = $name;
32
33
        $this->setRow(new Row($this));
34
        $this->setRowCollection(new RowCollection($this));
35
36
        $fieldFactory = $db->getFieldFactory();
37
38
        foreach (array_keys($this->getScheme()['fields']) as $name) {
39
            $this->fields[$name] = $fieldFactory->get($this, $name);
40
        }
41
42
        $this->init();
43
    }
44
45
    /**
46
     * Debug info.
47
     * 
48
     * @return array
49
     */
50
    public function __debugInfo()
51
    {
52
        return [
53
            'name' => $this->name,
54
            'fields' => $this->fields,
55
        ];
56
    }
57
58
    /**
59
     * Callback used to init the table.
60
     */
61
    protected function init()
62
    {
63
    }
64
65
    /**
66
     * Store a row in the cache.
67
     * 
68
     * @param Row $row
69
     */
70
    public function cache(Row $row)
71
    {
72
        $this->cache[$row->id] = $row;
73
    }
74
75
    /**
76
     * Clear the current cache.
77
     */
78
    public function clearCache()
79
    {
80
        $this->cache = [];
81
    }
82
83
    /**
84
     * Register a new query modifier.
85
     * 
86
     * @param string   $name
87
     * @param callable $modifier
88
     */
89
    public function addQueryModifier($name, callable $modifier)
90
    {
91
        if (!isset($this->queriesModifiers[$name])) {
92
            $this->queriesModifiers[$name] = [];
93
        }
94
95
        $this->queriesModifiers[$name][] = $modifier;
96
    }
97
98
    /**
99
     * Magic method to create queries related with this table.
100
     *
101
     * @param string $name
102
     * @param array  $arguments
103
     *
104
     * @throws SimpleCrudException
105
     *
106
     * @return Queries\Query|null
107
     */
108
    public function __call($name, $arguments)
109
    {
110
        $query = $this->getDatabase()->getQueryFactory()->get($this, $name);
111
112
        if (isset($this->queriesModifiers[$name])) {
113
            foreach ($this->queriesModifiers[$name] as $modifier) {
114
                $modifier($query);
115
            }
116
        }
117
118
        return $query;
119
    }
120
121
    /**
122
     * Check if a row with a specific id exists.
123
     *
124
     * @see ArrayAccess
125
     *
126
     * @return bool
127
     */
128
    public function offsetExists($offset)
129
    {
130
        if (array_key_exists($offset, $this->cache)) {
131
            return !empty($this->cache[$offset]);
132
        }
133
134
        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...
135
            ->byId($offset)
136
            ->limit(1)
137
            ->run() === 1;
138
    }
139
140
    /**
141
     * Returns a row with a specific id.
142
     *
143
     * @see ArrayAccess
144
     *
145
     * @return Row|null
146
     */
147
    public function offsetGet($offset)
148
    {
149
        if (array_key_exists($offset, $this->cache)) {
150
            return $this->cache[$offset];
151
        }
152
153
        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...
154
            ->one()
155
            ->byId($offset)
156
            ->run();
157
    }
158
159
    /**
160
     * Store a row with a specific id.
161
     *
162
     * @see ArrayAccess
163
     */
164
    public function offsetSet($offset, $value)
165
    {
166
        //Insert on missing offset
167
        if ($offset === null) {
168
            $value['id'] = null;
169
170
            $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...
171
                ->data($value)
172
                ->run();
173
174
            return;
175
        }
176
177
        //Update if the element is cached
178
        if (isset($this->cache[$offset])) {
179
            $row = $this->cache[$offset];
180
181
            foreach ($value as $name => $val) {
182
                $row->$name = $val;
183
            }
184
185
            $row->save();
186
187
            return;
188
        }
189
190
        //Update if the element it's not cached
191
        if ($this->offsetExists($offset)) {
192
            $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...
193
                ->data($value)
194
                ->byId($offset)
195
                ->limit(1)
196
                ->run();
197
        }
198
    }
199
200
    /**
201
     * Remove a row with a specific id.
202
     *
203
     * @see ArrayAccess
204
     */
205
    public function offsetUnset($offset)
206
    {
207
        unset($this->cache[$offset]);
208
209
        $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...
210
            ->byId($offset)
211
            ->limit(1)
212
            ->run();
213
    }
214
215
    /**
216
     * Returns the SimpleCrud instance associated with this table.
217
     *
218
     * @return SimpleCrud
219
     */
220
    public function getDatabase()
221
    {
222
        return $this->db;
223
    }
224
225
    /**
226
     * Returns the table scheme.
227
     *
228
     * @return array
229
     */
230
    public function getScheme()
231
    {
232
        return $this->db->getScheme()[$this->name];
233
    }
234
235
    /**
236
     * Returns an attribute.
237
     *
238
     * @param string $name
239
     *
240
     * @return null|mixed
241
     */
242
    public function getAttribute($name)
243
    {
244
        return $this->db->getAttribute($name);
245
    }
246
247
    /**
248
     * Defines the Row class used by this table.
249
     *
250
     * @param Row $row
251
     */
252
    public function setRow(Row $row)
253
    {
254
        $this->row = $row;
255
    }
256
257
    /**
258
     * Register a custom method to the row.
259
     *
260
     * @param string  $name
261
     * @param Closure $method
262
     *
263
     * @return self
264
     */
265
    public function setRowMethod($name, Closure $method)
266
    {
267
        $this->row->setMethod($name, $method);
268
269
        return $this;
270
    }
271
272
    /**
273
     * Defines the RowCollection class used by this table.
274
     *
275
     * @param RowCollection $rowCollection
276
     */
277
    public function setRowCollection(RowCollection $rowCollection)
278
    {
279
        $this->rowCollection = $rowCollection;
280
    }
281
282
    /**
283
     * Register a custom method to the rowCollections.
284
     *
285
     * @param string  $name
286
     * @param Closure $method
287
     *
288
     * @return self
289
     */
290
    public function setRowCollectionMethod($name, Closure $method)
291
    {
292
        $this->rowCollection->setMethod($name, $method);
293
294
        return $this;
295
    }
296
297
    /**
298
     * Creates a new row instance.
299
     *
300
     * @param array $data The values of the row
301
     *
302
     * @return Row
303
     */
304
    public function create(array $data = [])
305
    {
306 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...
307
            return $this->cache[$data['id']];
308
        }
309
310
        $row = clone $this->row;
311
312
        foreach ($data as $name => $value) {
313
            $row->$name = $value;
314
        }
315
316
        return $row;
317
    }
318
319
    /**
320
     * Creates a new rowCollection instance.
321
     *
322
     * @param array $data Rows added to this collection
323
     *
324
     * @return RowCollection
325
     */
326
    public function createCollection(array $data = [])
327
    {
328
        $rowCollection = clone $this->rowCollection;
329
330
        foreach ($data as $row) {
331
            $rowCollection[] = $row;
332
        }
333
334
        return $rowCollection;
335
    }
336
337
    /**
338
     * Default data converter/validator from database.
339
     *
340
     * @param array $data The values before insert to database
341
     * @param bool  $new  True for inserts, false for updates
342
     */
343
    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...
344
    {
345
        return $data;
346
    }
347
348
    /**
349
     * Default data converter from database.
350
     *
351
     * @param array $data The database format values
352
     */
353
    public function dataFromDatabase(array $data)
354
    {
355
        return $data;
356
    }
357
358
    /**
359
     * Prepares the data from the result of a database selection.
360
     *
361
     * @param array $data
362
     *
363
     * @return array
364
     */
365
    public function createFromDatabase(array $data)
366
    {
367
        //Get from cache
368 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...
369
            return $this->cache[$data['id']];
370
        }
371
372
        if (!is_array($row = $this->dataFromDatabase(array_intersect_key($data, $this->fields)))) {
373
            throw new SimpleCrudException('Data not valid');
374
        }
375
376
        foreach ($this->fields as $name => $field) {
377
            $row[$name] = $field->dataFromDatabase($row[$name]);
378
        }
379
380
        $row = $this->create($row);
381
382
        $this->cache($row);
383
384
        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...
385
    }
386
387
    /**
388
     * Prepares the data before save into database (used by update and insert).
389
     *
390
     * @param array $data
391
     * @param bool  $new
392
     *
393
     * @return array
394
     */
395
    public function prepareDataToDatabase(array $data, $new)
396
    {
397
        if (!is_array($data = $this->dataToDatabase($data, $new))) {
398
            throw new SimpleCrudException('Data not valid');
399
        }
400
401
        if (array_diff_key($data, $this->fields)) {
402
            throw new SimpleCrudException('Invalid fields');
403
        }
404
405
        //Transform data before save to database
406
        foreach ($data as $key => &$value) {
407
            $value = $this->fields[$key]->dataToDatabase($value);
408
        }
409
410
        return $data;
411
    }
412
}
413