Ajde_Collection::rewind()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 0
dl 0
loc 7
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
class Ajde_Collection extends Ajde_Object_Standard implements Iterator, Countable
4
{
5
    /**
6
     * @var string
7
     */
8
    protected $_modelName;
9
10
    /**
11
     * @var PDO
12
     */
13
    protected $_connection;
14
15
    /**
16
     * @var PDOStatement
17
     */
18
    protected $_statement;
19
20
    /**
21
     * @var Ajde_Query
22
     */
23
    protected $_query;
24
25
    protected $_link = [];
26
27
    /**
28
     * @var Ajde_Db_Table
29
     */
30
    protected $_table;
31
32
    protected $_filters = [];
33
    public $_filterValues = [];
34
35
    /**
36
     * @var Ajde_Collection_View
37
     */
38
    protected $_view;
39
40
    // For Iterator
41
    protected $_items = null;
42
    protected $_position = 0;
43
44
    private $_sqlInitialized = false;
45
    private $_queryCount;
46
47
    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...
48
    {
49
        // Register getCollection($name) function on Ajde_Controller
50
        if ($method === 'getCollection') {
51
            return self::getCollection($arguments[0]);
52
        }
53
        // TODO: if last triggered in event cueue, throw exception
54
        // throw new Ajde_Exception("Call to undefined method ".get_class($controller)."::$method()", 90006);
55
        // Now, we give other callbacks in event cueue chance to return
56
    }
57
58
    public static function getCollection($name)
59
    {
60
        $collectionName = ucfirst($name).'Collection';
61
62
        return new $collectionName();
63
    }
64
65
    public function __construct()
66
    {
67
        $this->_modelName = str_replace('Collection', '', get_class($this)).'Model';
68
        $this->_connection = Ajde_Db::getInstance()->getConnection();
69
70
        $tableNameCC = str_replace('Collection', '', get_class($this));
71
        $tableName = $this->fromCamelCase($tableNameCC);
72
73
        $this->_table = Ajde_Db::getInstance()->getTable($tableName);
74
        $this->_query = new Ajde_Query();
75
    }
76
77
    public function reset()
78
    {
79
        parent::reset();
80
        $this->_query = new Ajde_Query();
81
        $this->_filters = [];
82
        $this->_filterValues = [];
83
        $this->_items = null;
84
        $this->_position = 0;
85
        $this->_queryCount = null;
86
        $this->_sqlInitialized = false;
87
    }
88
89
    public function __sleep()
90
    {
91
        return ['_modelName', '_items'];
92
    }
93
94
    public function __wakeup()
95
    {
96
    }
97
98
    public function rewind()
99
    {
100
        if (!isset($this->_items)) {
101
            $this->load();
102
        }
103
        $this->_position = 0;
104
    }
105
106
    public function current()
107
    {
108
        return $this->_items[$this->_position];
109
    }
110
111
    public function key()
112
    {
113
        return $this->_position;
114
    }
115
116
    public function next()
117
    {
118
        $this->_position++;
119
    }
120
121
    public function count($query = false)
122
    {
123
        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...
124
            if (!isset($this->_queryCount)) {
125
                $this->_statement = $this->getConnection()->prepare($this->getCountSql());
126 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...
127
                    if (is_null($value)) {
128
                        $this->_statement->bindValue(":$key", null, PDO::PARAM_NULL);
129
                    } else {
130
                        $this->_statement->bindValue(":$key", $value, PDO::PARAM_STR);
131
                    }
132
                }
133
                $this->_statement->execute();
134
                $result = $this->_statement->fetch(PDO::FETCH_ASSOC);
135
                $this->_queryCount = $result['count'];
136
            }
137
138
            return $this->_queryCount;
139
        } else {
140
            if (!isset($this->_items)) {
141
                $this->load();
142
            }
143
144
            return count($this->_items);
145
        }
146
    }
147
148
    /**
149
     * @param string $field
150
     * @param mixed  $value
151
     *
152
     * @return Ajde_Model | boolean
153
     */
154
    public function find($field, $value)
155
    {
156
        foreach ($this as $item) {
157
            if ($item->{$field} == $value) {
158
                return $item;
159
            }
160
        }
161
162
        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...
163
    }
164
165
    public function valid()
166
    {
167
        return isset($this->_items[$this->_position]);
168
    }
169
170
    /**
171
     * @return Ajde_Db_PDO
172
     */
173
    public function getConnection()
174
    {
175
        return $this->_connection;
176
    }
177
178
    /**
179
     * @return Ajde_Db_Table
180
     */
181
    public function getTable()
182
    {
183
        return $this->_table;
184
    }
185
186
    /**
187
     * @return PDOStatement
188
     */
189
    public function getStatement()
190
    {
191
        return $this->_statement;
192
    }
193
194
    /**
195
     * @return Ajde_Query
196
     */
197
    public function getQuery()
198
    {
199
        return $this->_query;
200
    }
201
202
    public function populate($array)
203
    {
204
        $this->reset();
205
        $this->_data = $array;
206
    }
207
208
    public function getLink($modelName, $value)
209
    {
210
        if (!array_key_exists($modelName, $this->_link)) {
211
            // TODO:
212
            throw new Ajde_Exception('Link not defined...');
213
        }
214
215
        return new Ajde_Filter_Link($this, $modelName, $this->_link[$modelName], $value);
216
    }
217
218
    // Chainable collection methods
219
220
    public function addFilter(Ajde_Filter $filter)
221
    {
222
        $this->_filters[] = $filter;
223
224
        return $this;
225
    }
226
227
    public function orderBy($field, $direction = Ajde_Query::ORDER_ASC)
228
    {
229
        $this->getQuery()->addOrderBy($field, $direction);
230
231
        return $this;
232
    }
233
234
    public function limit($count, $start = 0)
235
    {
236
        $this->getQuery()->limit((int) $count, (int) $start);
237
238
        return $this;
239
    }
240
241
    public function filter($field, $value, $comparison = Ajde_Filter::FILTER_EQUALS, $operator = Ajde_Query::OP_AND)
242
    {
243
        $this->addFilter(new Ajde_Filter_Where($field, $comparison, $value, $operator));
244
245
        return $this;
246
    }
247
248
    // View functions
249
250
    public function setView(Ajde_Collection_View $view)
251
    {
252
        $this->_view = $view;
253
    }
254
255
    /**
256
     * @return Ajde_Collection_View
257
     */
258
    public function getView()
259
    {
260
        return $this->_view;
261
    }
262
263
    /**
264
     * @return bool
265
     */
266
    public function hasView()
267
    {
268
        return isset($this->_view) && $this->_view instanceof Ajde_Collection_View;
269
    }
270
271
    public function applyView(Ajde_Collection_View $view = null)
272
    {
273
        if (!$this->hasView() && !isset($view)) {
274
            // TODO:
275
            throw new Ajde_Exception('No view set');
276
        }
277
278
        if (isset($view)) {
279
            $this->setView($view);
280
        } else {
281
            $view = $this->getView();
282
        }
283
284
        // LIMIT
285
        $this->limit($view->getPageSize(), $view->getRowStart());
286
287
        // ORDER BY
288
        if (!$view->isEmpty('orderBy')) {
289
            $oldOrderBy = $this->getQuery()->orderBy;
290
            $this->getQuery()->orderBy = [];
291
            if (in_array($view->getOrderBy(), $this->getTable()->getFieldNames())) {
292
                $this->orderBy((string) $this->getTable().'.'.$view->getOrderBy(), $view->getOrderDir());
293
            } else {
294
                // custom column, make sure to add it to the query first!
295
                $this->orderBy($view->getOrderBy(), $view->getOrderDir());
296
            }
297
            foreach ($oldOrderBy as $orderBy) {
298
                $this->orderBy($orderBy['field'], $orderBy['direction']);
299
            }
300
        }
301
302
        // FILTER
303
        if (!$view->isEmpty('filter')) {
304
            foreach ($view->getFilter() as $fieldName => $filterValue) {
305
                if ($filterValue != '') {
306
                    $fieldType = $this->getTable()->getFieldProperties($fieldName, 'type');
307
                    if ($fieldType == Ajde_Db::FIELD_TYPE_DATE) {
308
                        // date fields
309
                        $start = $filterValue['start'] ? date('Y-m-d H:i:s',
310
                            strtotime($filterValue['start'].' 00:00:00')) : false;
311
                        $end = $filterValue['end'] ? date('Y-m-d H:i:s',
312
                            strtotime($filterValue['end'].' 23:59:59')) : false;
313 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...
314
                            $this->addFilter(new Ajde_Filter_Where((string) $this->getTable().'.'.$fieldName,
315
                                Ajde_Filter::FILTER_GREATEROREQUAL, $start));
316
                        }
317 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...
318
                            $this->addFilter(new Ajde_Filter_Where((string) $this->getTable().'.'.$fieldName,
319
                                Ajde_Filter::FILTER_LESSOREQUAL, $end));
320
                        }
321
                    } else {
322
                        if ($fieldType == Ajde_Db::FIELD_TYPE_TEXT) {
323
                            // text fields (fuzzy)
324
                            $this->addFilter(new Ajde_Filter_Where((string) $this->getTable().'.'.$fieldName,
325
                                Ajde_Filter::FILTER_LIKE, '%'.$filterValue.'%'));
326
                        } else {
327
                            // non-date fields (exact match)
328
                            $this->addFilter(new Ajde_Filter_Where((string) $this->getTable().'.'.$fieldName,
329
                                Ajde_Filter::FILTER_EQUALS, $filterValue));
330
                        }
331
                    }
332
                }
333
            }
334
        }
335
336
        // SEARCH
337
        if (!$view->isEmpty('search')) {
338
            $this->addTextFilter($view->getSearch());
339
        }
340
    }
341
342
    public function addTextFilter($text, $operator = Ajde_Query::OP_AND, $condition = Ajde_Filter::CONDITION_WHERE)
343
    {
344
        $searchFilter = $this->getTextFilterGroup($text, $operator, $condition);
345
        if ($searchFilter !== false) {
346
            $this->addFilter($searchFilter);
347
        } else {
348
            $this->addFilter(new Ajde_Filter_Where('true', '=', 'false'));
349
        }
350
    }
351
352
    public function getTextFilterGroup($text, $operator = Ajde_Query::OP_AND, $condition = Ajde_Filter::CONDITION_WHERE)
353
    {
354
        $groupClass = 'Ajde_Filter_'.ucfirst($condition).'Group';
355
        $filterClass = 'Ajde_Filter_'.ucfirst($condition);
356
357
        $searchFilter = new $groupClass($operator);
358
        $fieldOptions = $this->getTable()->getFieldProperties();
359
        foreach ($fieldOptions as $fieldName => $fieldProperties) {
360
            switch ($fieldProperties['type']) {
361
                case Ajde_Db::FIELD_TYPE_TEXT:
362 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...
363
                    $searchFilter->addFilter(new $filterClass((string) $this->getTable().'.'.$fieldName,
364
                        Ajde_Filter::FILTER_LIKE, '%'.$text.'%', Ajde_Query::OP_OR));
365
                    break;
366 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...
367
                    $searchFilter->addFilter(new $filterClass('CAST('.(string) $this->getTable().'.'.$fieldName.' AS CHAR)',
368
                        Ajde_Filter::FILTER_LIKE, '%'.$text.'%', Ajde_Query::OP_OR));
369
                    break;
370
                default:
371
                    break;
372
            }
373
        }
374
375
        return $searchFilter->hasFilters() ? $searchFilter : false;
376
    }
377
378
    public function getSql()
379
    {
380
        if (!$this->_sqlInitialized) {
381
            foreach ($this->getTable()->getFieldNames() as $field) {
382
                $this->getQuery()->addSelect((string) $this->getTable().'.'.$field);
383
            }
384
            if (!empty($this->_filters)) {
385
                foreach ($this->getFilter('select') as $select) {
386
                    call_user_func_array([$this->getQuery(), 'addSelect'], $select);
387
                }
388
            }
389
            $this->getQuery()->addFrom($this->_table);
390
            if (!empty($this->_filters)) {
391
                foreach ($this->getFilter('join') as $join) {
392
                    call_user_func_array([$this->getQuery(), 'addJoin'], $join);
393
                }
394
                foreach ($this->getFilter('where') as $where) {
395
                    call_user_func_array([$this->getQuery(), 'addWhere'], $where);
396
                }
397
                foreach ($this->getFilter('having') as $having) {
398
                    call_user_func_array([$this->getQuery(), 'addHaving'], $having);
399
                }
400
            }
401
        }
402
        $this->_sqlInitialized = true;
403
404
        return $this->getQuery()->getSql();
405
    }
406
407
    public function getCountSql()
408
    {
409
        // Make sure to load the filters
410
        $this->getSql();
411
        $query = clone $this->getQuery();
412
        /* @var $query Ajde_Query */
413
        $query->select = [];
414
        $query->orderBy = [];
415
        $query->limit = ['start' => null, 'count' => null];
416
        $query->addSelect('COUNT(*) AS count');
417
418
        return $query->getSql();
419
    }
420
421
    public function getEmulatedSql()
422
    {
423
        return Ajde_Db_PDOStatement::getEmulatedSql($this->getSql(), $this->getFilterValues());
424
    }
425
426
    public function getFilter($queryPart)
427
    {
428
        $arguments = [];
429
        foreach ($this->_filters as $filter) {
430
            $prepared = $filter->prepare($this->getTable());
431
            if (isset($prepared[$queryPart])) {
432
                if (isset($prepared[$queryPart]['values'])) {
433
                    $this->_filterValues = array_merge($this->_filterValues, $prepared[$queryPart]['values']);
434
                }
435
                $arguments[] = $prepared[$queryPart]['arguments'];
436
            }
437
        }
438
        if (empty($arguments)) {
439
            return [];
440
        } else {
441
            return $arguments;
442
        }
443
    }
444
445
    public function getFilterValues()
446
    {
447
        return $this->_filterValues;
448
    }
449
450
    // Load the collection
451
    public function load()
452
    {
453
        if (!$this->getConnection() instanceof Ajde_Db_PDO) {
454
            // return false;
455
        }
456
        $this->_statement = $this->getConnection()->prepare($this->getSql());
457 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...
458
            if (is_null($value)) {
459
                $this->_statement->bindValue(":$key", null, PDO::PARAM_NULL);
460
            } else {
461
                $this->_statement->bindValue(":$key", $value, PDO::PARAM_STR);
462
            }
463
        }
464
        $this->_statement->execute();
465
466
        return $this->_items = $this->_statement->fetchAll(PDO::FETCH_CLASS, $this->_modelName);
467
    }
468
469
    public function first()
470
    {
471
        $this->limit(1);
472
        $items = $this->load();
473
474
        if (count($items)) {
475
            return $items[0];
476
        }
477
    }
478
479
    public function loadParents()
480
    {
481
        if (count($this) > 0) {
482
            foreach ($this as $model) {
483
                $model->loadParents();
484
            }
485
        }
486
    }
487
488
    public function length()
489
    {
490
        if (!isset($this->_items)) {
491
            $this->load();
492
        }
493
494
        return count($this->_items);
495
    }
496
497
    public function hash()
498
    {
499
        $str = '';
500
        /** @var $item Ajde_Model */
501
        foreach ($this as $item) {
502
            $str .= implode('', $item->valuesAsSingleDimensionArray());
503
        }
504
505
        return md5($str);
506
    }
507
508
    public function toArray()
509
    {
510
        $array = [];
511
        foreach ($this as $item) {
512
            $array[] = $item->values();
513
        }
514
515
        return $array;
516
    }
517
518
    public function items()
519
    {
520
        if (!isset($this->_items)) {
521
            $this->load();
522
        }
523
524
        return $this->_items;
525
    }
526
527
    public function add($item)
528
    {
529
        $this->_items[] = $item;
530
    }
531
532
    public function combine(Ajde_Collection $collection)
533
    {
534
        foreach ($collection as $item) {
535
            $this->add($item);
536
        }
537
538
        return $this;
539
    }
540
541
    public function deleteAll()
542
    {
543
        foreach ($this as $item) {
544
            $item->delete();
545
        }
546
    }
547
}
548