Completed
Push — master ( 6dac49...fd563b )
by Oscar
02:37
created

Table::addQueryModifier()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 8
rs 9.4285
cc 2
eloc 4
nc 2
nop 2
1
<?php
2
3
namespace SimpleCrud;
4
5
use ArrayAccess;
6
7
/**
8
 * Manages a database table.
9
 */
10
class Table implements ArrayAccess
11
{
12
    private $db;
13
    private $row;
14
    private $collection;
15
    private $cache = [];
16
17
    public $name;
18
    public $fields = [];
19
    public $queriesModifiers = [];
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->setCollection(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
     * Check if a row with a specific id exists.
122
     *
123
     * @see ArrayAccess
124
     *
125
     * @return bool
126
     */
127
    public function offsetExists($offset)
128
    {
129
        if (array_key_exists($offset, $this->cache)) {
130
            return !empty($this->cache[$offset]);
131
        }
132
133
        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...
134
            ->byId($offset)
135
            ->limit(1)
136
            ->run() === 1;
137
    }
138
139
    /**
140
     * Returns a row with a specific id.
141
     *
142
     * @see ArrayAccess
143
     *
144
     * @return Row|null
145
     */
146
    public function offsetGet($offset)
147
    {
148
        if (array_key_exists($offset, $this->cache)) {
149
            return $this->cache[$offset];
150
        }
151
152
        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...
153
            ->one()
154
            ->byId($offset)
155
            ->run();
156
    }
157
158
    /**
159
     * Store a row with a specific id.
160
     *
161
     * @see ArrayAccess
162
     */
163
    public function offsetSet($offset, $value)
164
    {
165
        //Insert on missing offset
166
        if ($offset === null) {
167
            $value['id'] = null;
168
169
            $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...
170
                ->data($value)
171
                ->run();
172
173
            return;
174
        }
175
176
        //Update if the element is cached
177
        if (isset($this->cache[$offset])) {
178
            $row = $this->cache[$offset];
179
180
            foreach ($value as $name => $val) {
181
                $row->$name = $val;
182
            }
183
184
            $row->save();
185
186
            return;
187
        }
188
189
        //Update if the element it's not cached
190
        if ($this->offsetExists($offset)) {
191
            $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...
192
                ->data($value)
193
                ->byId($offset)
194
                ->limit(1)
195
                ->run();
196
        }
197
    }
198
199
    /**
200
     * Remove a row with a specific id.
201
     *
202
     * @see ArrayAccess
203
     */
204
    public function offsetUnset($offset)
205
    {
206
        unset($this->cache[$offset]);
207
208
        $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...
209
            ->byId($offset)
210
            ->limit(1)
211
            ->run();
212
    }
213
214
    /**
215
     * Returns the SimpleCrud instance associated with this table.
216
     *
217
     * @return SimpleCrud
218
     */
219
    public function getDatabase()
220
    {
221
        return $this->db;
222
    }
223
224
    /**
225
     * Returns the table scheme.
226
     *
227
     * @return array
228
     */
229
    public function getScheme()
230
    {
231
        return $this->db->getScheme()[$this->name];
232
    }
233
234
    /**
235
     * Returns an attribute.
236
     *
237
     * @param string $name
238
     *
239
     * @return null|mixed
240
     */
241
    public function getAttribute($name)
242
    {
243
        return $this->db->getAttribute($name);
244
    }
245
246
    /**
247
     * Defines the Row class used by this table.
248
     *
249
     * @param Row $row
250
     */
251
    public function setRow(Row $row)
252
    {
253
        $this->row = $row;
254
    }
255
256
    /**
257
     * Defines the RowCollection class used by this table.
258
     *
259
     * @param RowCollection $collection
260
     */
261
    public function setCollection(RowCollection $collection)
262
    {
263
        $this->collection = $collection;
264
    }
265
266
    /**
267
     * Creates a new row instance.
268
     *
269
     * @param array $data The values of the row
270
     *
271
     * @return Row
272
     */
273
    public function create(array $data = [])
274
    {
275 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...
276
            return $this->cache[$data['id']];
277
        }
278
279
        $row = clone $this->row;
280
281
        foreach ($data as $name => $value) {
282
            $row->$name = $value;
283
        }
284
285
        return $row;
286
    }
287
288
    /**
289
     * Creates a new rowCollection instance.
290
     *
291
     * @param array $data Rows added to this collection
292
     *
293
     * @return RowCollection
294
     */
295
    public function createCollection(array $data = [])
296
    {
297
        $collection = clone $this->collection;
298
299
        foreach ($data as $row) {
300
            $collection[] = $row;
301
        }
302
303
        return $collection;
304
    }
305
306
    /**
307
     * Default data converter/validator from database.
308
     *
309
     * @param array $data The values before insert to database
310
     * @param bool  $new  True for inserts, false for updates
311
     */
312
    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...
313
    {
314
        return $data;
315
    }
316
317
    /**
318
     * Default data converter from database.
319
     *
320
     * @param array $data The database format values
321
     */
322
    public function dataFromDatabase(array $data)
323
    {
324
        return $data;
325
    }
326
327
    /**
328
     * Prepares the data from the result of a database selection.
329
     *
330
     * @param array $data
331
     *
332
     * @return array
333
     */
334
    public function createFromDatabase(array $data)
335
    {
336
        //Get from cache
337 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...
338
            return $this->cache[$data['id']];
339
        }
340
341
        if (!is_array($row = $this->dataFromDatabase(array_intersect_key($data, $this->fields)))) {
342
            throw new SimpleCrudException('Data not valid');
343
        }
344
345
        foreach ($this->fields as $name => $field) {
346
            $row[$name] = $field->dataFromDatabase($row[$name]);
347
        }
348
349
        $row = $this->create($row);
350
351
        $this->cache($row);
352
353
        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...
354
    }
355
356
    /**
357
     * Prepares the data before save into database (used by update and insert).
358
     *
359
     * @param array $data
360
     * @param bool  $new
361
     *
362
     * @return array
363
     */
364
    public function prepareDataToDatabase(array $data, $new)
365
    {
366
        if (!is_array($data = $this->dataToDatabase($data, $new))) {
367
            throw new SimpleCrudException('Data not valid');
368
        }
369
370
        if (array_diff_key($data, $this->fields)) {
371
            throw new SimpleCrudException('Invalid fields');
372
        }
373
374
        //Transform data before save to database
375
        foreach ($data as $key => &$value) {
376
            $value = $this->fields[$key]->dataToDatabase($value);
377
        }
378
379
        return $data;
380
    }
381
}
382