Completed
Push — master ( 3fde7a...7bfc6c )
by Joram van den
03:44
created

Ajde_Collection::add()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
c 4
b 0
f 0
dl 0
loc 4
rs 10
cc 1
eloc 2
nc 1
nop 1
1
<?php
2
3
class Ajde_Collection extends Ajde_Object_Standard implements Iterator, Countable
4
{
5
6
    /**
7
     * @var string
8
     */
9
    protected $_modelName;
10
11
    /**
12
     * @var PDO
13
     */
14
    protected $_connection;
15
16
    /**
17
     * @var PDOStatement
18
     */
19
    protected $_statement;
20
21
    /**
22
     * @var Ajde_Query
23
     */
24
    protected $_query;
25
26
    protected $_link = [];
27
28
    /**
29
     * @var Ajde_Db_Table
30
     */
31
    protected $_table;
32
33
    protected $_filters      = [];
34
    public    $_filterValues = [];
35
36
    /**
37
     * @var Ajde_Collection_View
38
     */
39
    protected $_view;
40
41
    // For Iterator
42
    protected $_items    = null;
43
    protected $_position = 0;
44
45
    private $_sqlInitialized = false;
46
    private $_queryCount;
47
48
    public static function extendController(Ajde_Controller $controller, $method, $arguments)
0 ignored issues
show
Unused Code introduced by
The parameter $controller 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...
49
    {
50
        // Register getCollection($name) function on Ajde_Controller
51
        if ($method === 'getCollection') {
52
            return self::getCollection($arguments[0]);
53
        }
54
        // TODO: if last triggered in event cueue, throw exception
55
        // throw new Ajde_Exception("Call to undefined method ".get_class($controller)."::$method()", 90006);
56
        // Now, we give other callbacks in event cueue chance to return
57
        return null;
58
    }
59
60
    public static function getCollection($name)
61
    {
62
        $collectionName = ucfirst($name) . 'Collection';
63
64
        return new $collectionName();
65
    }
66
67
    public function __construct()
68
    {
69
        $this->_modelName  = str_replace('Collection', '', get_class($this)) . 'Model';
70
        $this->_connection = Ajde_Db::getInstance()->getConnection();
71
72
        $tableNameCC = str_replace('Collection', '', get_class($this));
73
        $tableName   = $this->fromCamelCase($tableNameCC);
74
75
        $this->_table = Ajde_Db::getInstance()->getTable($tableName);
76
        $this->_query = new Ajde_Query();
77
    }
78
79
    public function reset()
80
    {
81
        parent::reset();
82
        $this->_query          = new Ajde_Query();
83
        $this->_filters        = [];
84
        $this->_filterValues   = [];
85
        $this->_items          = null;
86
        $this->_position       = 0;
87
        $this->_queryCount     = null;
88
        $this->_sqlInitialized = false;
89
    }
90
91
    public function __sleep()
92
    {
93
        return ['_modelName', '_items'];
94
    }
95
96
    public function __wakeup()
97
    {
98
    }
99
100
    public function rewind()
101
    {
102
        if (!isset($this->_items)) {
103
            $this->load();
104
        }
105
        $this->_position = 0;
106
    }
107
108
    public function current()
109
    {
110
        return $this->_items[$this->_position];
111
    }
112
113
    public function key()
114
    {
115
        return $this->_position;
116
    }
117
118
    public function next()
119
    {
120
        $this->_position++;
121
    }
122
123
    public function count($query = false)
124
    {
125
        if ($query == true) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
126
            if (!isset($this->_queryCount)) {
127
                $this->_statement = $this->getConnection()->prepare($this->getCountSql());
128 View Code Duplication
                foreach ($this->getFilterValues() as $key => $value) {
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...
129
                    if (is_null($value)) {
130
                        $this->_statement->bindValue(":$key", null, PDO::PARAM_NULL);
131
                    } else {
132
                        $this->_statement->bindValue(":$key", $value, PDO::PARAM_STR);
133
                    }
134
                }
135
                $this->_statement->execute();
136
                $result            = $this->_statement->fetch(PDO::FETCH_ASSOC);
137
                $this->_queryCount = $result['count'];
138
            }
139
140
            return $this->_queryCount;
141
        } else {
142
            if (!isset($this->_items)) {
143
                $this->load();
144
            }
145
146
            return count($this->_items);
147
        }
148
    }
149
150
    /**
151
     *
152
     * @param string $field
153
     * @param mixed  $value
154
     * @return Ajde_Model | boolean
155
     */
156
    public function find($field, $value)
157
    {
158
        foreach ($this as $item) {
159
            if ($item->{$field} == $value) {
160
                return $item;
161
            }
162
        }
163
164
        return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by Ajde_Collection::find of type Ajde_Model.

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...
165
    }
166
167
    function valid()
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
168
    {
169
        return isset($this->_items[$this->_position]);
170
    }
171
172
    /**
173
     * @return Ajde_Db_PDO
174
     */
175
    public function getConnection()
176
    {
177
        return $this->_connection;
178
    }
179
180
    /**
181
     * @return Ajde_Db_Table
182
     */
183
    public function getTable()
184
    {
185
        return $this->_table;
186
    }
187
188
    /**
189
     * @return PDOStatement
190
     */
191
    public function getStatement()
192
    {
193
        return $this->_statement;
194
    }
195
196
    /**
197
     * @return Ajde_Query
198
     */
199
    public function getQuery()
200
    {
201
        return $this->_query;
202
    }
203
204
    public function populate($array)
205
    {
206
        $this->reset();
207
        $this->_data = $array;
208
    }
209
210
    public function getLink($modelName, $value)
211
    {
212
        if (!array_key_exists($modelName, $this->_link)) {
213
            // TODO:
214
            throw new Ajde_Exception('Link not defined...');
215
        }
216
217
        return new Ajde_Filter_Link($this, $modelName, $this->_link[$modelName], $value);
218
    }
219
220
    // Chainable collection methods
221
222
    public function addFilter(Ajde_Filter $filter)
223
    {
224
        $this->_filters[] = $filter;
225
226
        return $this;
227
    }
228
229
    public function orderBy($field, $direction = Ajde_Query::ORDER_ASC)
230
    {
231
        $this->getQuery()->addOrderBy($field, $direction);
232
233
        return $this;
234
    }
235
236
    public function limit($count, $start = 0)
237
    {
238
        $this->getQuery()->limit((int)$count, (int)$start);
239
240
        return $this;
241
    }
242
243
    public function filter($field, $value, $comparison = Ajde_Filter::FILTER_EQUALS, $operator = Ajde_Query::OP_AND)
244
    {
245
        $this->addFilter(new Ajde_Filter_Where($field, $comparison, $value, $operator));
246
247
        return $this;
248
    }
249
250
    // View functions
251
252
    public function setView(Ajde_Collection_View $view)
253
    {
254
        $this->_view = $view;
255
    }
256
257
    /**
258
     * @return Ajde_Collection_View
259
     */
260
    public function getView()
261
    {
262
        return $this->_view;
263
    }
264
265
    /**
266
     * @return boolean
267
     */
268
    public function hasView()
269
    {
270
        return isset($this->_view) && $this->_view instanceof Ajde_Collection_View;
271
    }
272
273
    public function applyView(Ajde_Collection_View $view = null)
274
    {
275
        if (!$this->hasView() && !isset($view)) {
276
            // TODO:
277
            throw new Ajde_Exception('No view set');
278
        }
279
280
        if (isset($view)) {
281
            $this->setView($view);
282
        } else {
283
            $view = $this->getView();
284
        }
285
286
        // LIMIT
287
        $this->limit($view->getPageSize(), $view->getRowStart());
288
289
        // ORDER BY
290
        if (!$view->isEmpty('orderBy')) {
291
            $oldOrderBy                = $this->getQuery()->orderBy;
292
            $this->getQuery()->orderBy = [];
293
            if (in_array($view->getOrderBy(), $this->getTable()->getFieldNames())) {
294
                $this->orderBy((string)$this->getTable() . '.' . $view->getOrderBy(), $view->getOrderDir());
295
            } else {
296
                // custom column, make sure to add it to the query first!
297
                $this->orderBy($view->getOrderBy(), $view->getOrderDir());
298
            }
299
            foreach ($oldOrderBy as $orderBy) {
300
                $this->orderBy($orderBy['field'], $orderBy['direction']);
301
            }
302
        }
303
304
        // FILTER
305
        if (!$view->isEmpty('filter')) {
306
            foreach ($view->getFilter() as $fieldName => $filterValue) {
307
                if ($filterValue != '') {
308
                    $fieldType = $this->getTable()->getFieldProperties($fieldName, 'type');
309
                    if ($fieldType == Ajde_Db::FIELD_TYPE_DATE) {
310
                        // date fields
311
                        $start = $filterValue['start'] ? date('Y-m-d H:i:s',
312
                            strtotime($filterValue['start'] . ' 00:00:00')) : false;
313
                        $end   = $filterValue['end'] ? date('Y-m-d H:i:s',
314
                            strtotime($filterValue['end'] . ' 23:59:59')) : false;
315 View Code Duplication
                        if ($start) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $start of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
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...
316
                            $this->addFilter(new Ajde_Filter_Where((string)$this->getTable() . '.' . $fieldName,
317
                                Ajde_Filter::FILTER_GREATEROREQUAL, $start));
318
                        }
319 View Code Duplication
                        if ($end) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $end of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
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...
320
                            $this->addFilter(new Ajde_Filter_Where((string)$this->getTable() . '.' . $fieldName,
321
                                Ajde_Filter::FILTER_LESSOREQUAL, $end));
322
                        }
323
                    } else {
324
                        if ($fieldType == Ajde_Db::FIELD_TYPE_TEXT) {
325
                            // text fields (fuzzy)
326
                            $this->addFilter(new Ajde_Filter_Where((string)$this->getTable() . '.' . $fieldName,
327
                                Ajde_Filter::FILTER_LIKE, '%' . $filterValue . '%'));
328
                        } else {
329
                            // non-date fields (exact match)
330
                            $this->addFilter(new Ajde_Filter_Where((string)$this->getTable() . '.' . $fieldName,
331
                                Ajde_Filter::FILTER_EQUALS, $filterValue));
332
                        }
333
                    }
334
                }
335
            }
336
        }
337
338
        // SEARCH
339
        if (!$view->isEmpty('search')) {
340
            $this->addTextFilter($view->getSearch());
341
        }
342
    }
343
344
    public function addTextFilter($text, $operator = Ajde_Query::OP_AND, $condition = Ajde_Filter::CONDITION_WHERE)
345
    {
346
        $searchFilter = $this->getTextFilterGroup($text, $operator, $condition);
347
        if ($searchFilter !== false) {
348
            $this->addFilter($searchFilter);
349
        } else {
350
            $this->addFilter(new Ajde_Filter_Where('true', '=', 'false'));
351
        }
352
    }
353
354
    public function getTextFilterGroup($text, $operator = Ajde_Query::OP_AND, $condition = Ajde_Filter::CONDITION_WHERE)
355
    {
356
        $groupClass  = 'Ajde_Filter_' . ucfirst($condition) . 'Group';
357
        $filterClass = 'Ajde_Filter_' . ucfirst($condition);
358
359
        $searchFilter = new $groupClass($operator);
360
        $fieldOptions = $this->getTable()->getFieldProperties();
361
        foreach ($fieldOptions as $fieldName => $fieldProperties) {
362
            switch ($fieldProperties['type']) {
363
                case Ajde_Db::FIELD_TYPE_TEXT:
364 View Code Duplication
                case Ajde_Db::FIELD_TYPE_ENUM:
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...
365
                    $searchFilter->addFilter(new $filterClass((string)$this->getTable() . '.' . $fieldName,
366
                        Ajde_Filter::FILTER_LIKE, '%' . $text . '%', Ajde_Query::OP_OR));
367
                    break;
368 View Code Duplication
                case Ajde_Db::FIELD_TYPE_NUMERIC:
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
                    $searchFilter->addFilter(new $filterClass('CAST(' . (string)$this->getTable() . '.' . $fieldName . ' AS CHAR)',
370
                        Ajde_Filter::FILTER_LIKE, '%' . $text . '%', Ajde_Query::OP_OR));
371
                    break;
372
                default:
373
                    break;
374
            }
375
        }
376
377
        return $searchFilter->hasFilters() ? $searchFilter : false;
378
    }
379
380
    public function getSql()
381
    {
382
        if (!$this->_sqlInitialized) {
383
            foreach ($this->getTable()->getFieldNames() as $field) {
384
                $this->getQuery()->addSelect((string)$this->getTable() . '.' . $field);
385
            }
386
            if (!empty($this->_filters)) {
387
                foreach ($this->getFilter('select') as $select) {
388
                    call_user_func_array([$this->getQuery(), 'addSelect'], $select);
389
                }
390
            }
391
            $this->getQuery()->addFrom($this->_table);
392
            if (!empty($this->_filters)) {
393
                foreach ($this->getFilter('join') as $join) {
394
                    call_user_func_array([$this->getQuery(), 'addJoin'], $join);
395
                }
396
                foreach ($this->getFilter('where') as $where) {
397
                    call_user_func_array([$this->getQuery(), 'addWhere'], $where);
398
                }
399
                foreach ($this->getFilter('having') as $having) {
400
                    call_user_func_array([$this->getQuery(), 'addHaving'], $having);
401
                }
402
            }
403
        }
404
        $this->_sqlInitialized = true;
405
406
        return $this->getQuery()->getSql();
407
    }
408
409
    public function getCountSql()
410
    {
411
        // Make sure to load the filters
412
        $this->getSql();
413
        $query = clone $this->getQuery();
414
        /* @var $query Ajde_Query */
415
        $query->select  = [];
416
        $query->orderBy = [];
417
        $query->limit   = ['start' => null, 'count' => null];
418
        $query->addSelect('COUNT(*) AS count');
419
420
        return $query->getSql();
421
    }
422
423
    public function getEmulatedSql()
424
    {
425
        return Ajde_Db_PDOStatement::getEmulatedSql($this->getSql(), $this->getFilterValues());
426
    }
427
428
    public function getFilter($queryPart)
429
    {
430
        $arguments = [];
431
        foreach ($this->_filters as $filter) {
432
            $prepared = $filter->prepare($this->getTable());
433
            if (isset($prepared[$queryPart])) {
434
                if (isset($prepared[$queryPart]['values'])) {
435
                    $this->_filterValues = array_merge($this->_filterValues, $prepared[$queryPart]['values']);
436
                }
437
                $arguments[] = $prepared[$queryPart]['arguments'];
438
            }
439
        }
440
        if (empty($arguments)) {
441
            return [];
442
        } else {
443
            return $arguments;
444
        }
445
    }
446
447
    public function getFilterValues()
448
    {
449
        return $this->_filterValues;
450
    }
451
452
    // Load the collection
453
    public function load()
454
    {
455
        if (!$this->getConnection() instanceof Ajde_Db_PDO) {
456
            // return false;
457
        }
458
        $this->_statement = $this->getConnection()->prepare($this->getSql());
459 View Code Duplication
        foreach ($this->getFilterValues() as $key => $value) {
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...
460
            if (is_null($value)) {
461
                $this->_statement->bindValue(":$key", null, PDO::PARAM_NULL);
462
            } else {
463
                $this->_statement->bindValue(":$key", $value, PDO::PARAM_STR);
464
            }
465
        }
466
        $this->_statement->execute();
467
468
        return $this->_items = $this->_statement->fetchAll(PDO::FETCH_CLASS, $this->_modelName);
469
    }
470
471
    public function loadParents()
472
    {
473
        if (count($this) > 0) {
474
            foreach ($this as $model) {
475
                $model->loadParents();
476
            }
477
        }
478
    }
479
480
    public function length()
481
    {
482
        if (!isset($this->_items)) {
483
            $this->load();
484
        }
485
486
        return count($this->_items);
487
    }
488
489
    public function hash()
490
    {
491
        $str = '';
492
        /** @var $item Ajde_Model */
493
        foreach ($this as $item) {
494
            $str .= implode('', $item->valuesAsSingleDimensionArray());
495
        }
496
497
        return md5($str);
498
    }
499
500
    public function toArray()
501
    {
502
        $array = [];
503
        foreach ($this as $item) {
504
            $array[] = $item->values();
505
        }
506
507
        return $array;
508
    }
509
510
    public function items()
511
    {
512
        if (!isset($this->_items)) {
513
            $this->load();
514
        }
515
516
        return $this->_items;
517
    }
518
519
    public function add($item)
520
    {
521
        $this->_items[] = $item;
522
    }
523
524
    public function combine(Ajde_Collection $collection)
525
    {
526
        foreach ($collection as $item) {
527
            $this->add($item);
528
        }
529
    }
530
531
    public function deleteAll()
532
    {
533
        foreach ($this as $item) {
534
            $item->delete();
535
        }
536
    }
537
}
538