Test Setup Failed
Push — master ( 000e9e...f38e71 )
by
unknown
09:27
created

CollectionLoader::addFilters()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 8
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 4
nc 2
nop 1
1
<?php
2
3
namespace Charcoal\Loader;
4
5
use InvalidArgumentException;
6
use RuntimeException;
7
use ArrayAccess;
8
use Traversable;
9
use PDO;
10
11
// From PSR-3
12
use Psr\Log\LoggerAwareInterface;
13
use Psr\Log\LoggerAwareTrait;
14
use Psr\Log\NullLogger;
15
16
// From 'charcoal-factory'
17
use Charcoal\Factory\FactoryInterface;
18
19
// From 'charcoal-core'
20
use Charcoal\Model\ModelInterface;
21
use Charcoal\Model\Collection;
22
use Charcoal\Source\SourceInterface;
23
24
/**
25
 * Object Collection Loader
26
 */
27
class CollectionLoader implements LoggerAwareInterface
28
{
29
    use LoggerAwareTrait;
30
31
    /**
32
     * The source to load objects from.
33
     *
34
     * @var SourceInterface
35
     */
36
    private $source;
37
38
    /**
39
     * The model to load the collection from.
40
     *
41
     * @var ModelInterface
42
     */
43
    private $model;
44
45
    /**
46
     * Store the factory instance for the current class.
47
     *
48
     * @var FactoryInterface
49
     */
50
    private $factory;
51
52
    /**
53
     * The callback routine applied to every object added to the collection.
54
     *
55
     * @var callable|null
56
     */
57
    private $callback;
58
59
    /**
60
     * The field which defines the data's model.
61
     *
62
     * @var string|null
63
     */
64
    private $dynamicTypeField;
65
66
    /**
67
     * The class name of the collection to use.
68
     *
69
     * Must be a fully-qualified PHP namespace and an implementation of {@see ArrayAccess}.
70
     *
71
     * @var string
72
     */
73
    private $collectionClass = Collection::class;
74
75
    /**
76
     * Return a new CollectionLoader object.
77
     *
78
     * @param array $data The loader's dependencies.
79
     */
80
    public function __construct(array $data)
81
    {
82
        if (!isset($data['logger'])) {
83
            $data['logger'] = new NullLogger();
84
        }
85
86
        $this->setLogger($data['logger']);
87
88
        if (isset($data['collection'])) {
89
            $this->setCollectionClass($data['collection']);
90
        }
91
92
        if (isset($data['factory'])) {
93
            $this->setFactory($data['factory']);
94
        }
95
96
        if (isset($data['model'])) {
97
            $this->setModel($data['model']);
98
        }
99
    }
100
101
    /**
102
     * Set an object model factory.
103
     *
104
     * @param FactoryInterface $factory The model factory, to create objects.
105
     * @return CollectionLoader Chainable
106
     */
107
    public function setFactory(FactoryInterface $factory)
108
    {
109
        $this->factory = $factory;
110
111
        return $this;
112
    }
113
114
    /**
115
     * Retrieve the object model factory.
116
     *
117
     * @throws RuntimeException If the model factory was not previously set.
118
     * @return FactoryInterface
119
     */
120
    protected function factory()
121
    {
122
        if ($this->factory === null) {
123
            throw new RuntimeException(
124
                sprintf('Model Factory is not defined for "%s"', get_class($this))
125
            );
126
        }
127
128
        return $this->factory;
129
    }
130
131
    /**
132
     * Set the loader data.
133
     *
134
     * @param  array $data Data to assign to the loader.
135
     * @return CollectionLoader Chainable
136
     */
137
    public function setData(array $data)
138
    {
139
        foreach ($data as $key => $val) {
140
            $setter = $this->setter($key);
141
142
            if (is_callable([$this, $setter])) {
143
                $this->{$setter}($val);
144
            } else {
145
                $this->{$key} = $val;
146
            }
147
        }
148
149
        return $this;
150
    }
151
152
    /**
153
     * Retrieve the source to load objects from.
154
     *
155
     * @throws RuntimeException If no source has been defined.
156
     * @return mixed
157
     */
158
    public function source()
159
    {
160
        if ($this->source === null) {
161
            throw new RuntimeException('No source set.');
162
        }
163
164
        return $this->source;
165
    }
166
167
    /**
168
     * Set the source to load objects from.
169
     *
170
     * @param  SourceInterface $source A data source.
171
     * @return CollectionLoader Chainable
172
     */
173
    public function setSource(SourceInterface $source)
174
    {
175
        $source->reset();
176
177
        $this->source = $source;
178
179
        return $this;
180
    }
181
182
    /**
183
     * Reset everything but the model.
184
     *
185
     * @return CollectionLoader Chainable
186
     */
187
    public function reset()
188
    {
189
        if ($this->source) {
190
            $this->source()->reset();
191
        }
192
193
        $this->callback = null;
194
        $this->dynamicTypeField = null;
195
196
        return $this;
197
    }
198
199
    /**
200
     * Retrieve the object model.
201
     *
202
     * @throws RuntimeException If no model has been defined.
203
     * @return Model
204
     */
205
    public function model()
206
    {
207
        if ($this->model === null) {
208
            throw new RuntimeException('The collection loader must have a model.');
209
        }
210
211
        return $this->model;
212
    }
213
214
    /**
215
     * Determine if the loader has an object model.
216
     *
217
     * @return boolean
218
     */
219
    public function hasModel()
220
    {
221
        return !!$this->model;
222
    }
223
224
    /**
225
     * Set the model to use for the loaded objects.
226
     *
227
     * @param  string|ModelInterface $model An object model.
228
     * @throws InvalidArgumentException If the given argument is not a model.
229
     * @return CollectionLoader CHainable
230
     */
231
    public function setModel($model)
232
    {
233
        if (is_string($model)) {
234
            $model = $this->factory()->get($model);
235
        }
236
237
        if (!$model instanceof ModelInterface) {
238
            throw new InvalidArgumentException(
239
                sprintf(
240
                    'The model must be an instance of "%s"',
241
                    ModelInterface::class
242
                )
243
            );
244
        }
245
246
        $this->model = $model;
247
248
        $this->setSource($model->source());
249
250
        return $this;
251
    }
252
253
    /**
254
     * @param string $field The field to use for dynamic object type.
255
     * @throws InvalidArgumentException If the field is not a string.
256
     * @return CollectionLoader Chainable
257
     */
258
    public function setDynamicTypeField($field)
259
    {
260
        if (!is_string($field)) {
261
            throw new InvalidArgumentException(
262
                'Dynamic type field must be a string'
263
            );
264
        }
265
266
        $this->dynamicTypeField = $field;
267
268
        return $this;
269
    }
270
271
    /**
272
     * Alias of {@see SourceInterface::properties()}
273
     *
274
     * @return array
275
     */
276
    public function properties()
277
    {
278
        return $this->source()->properties();
279
    }
280
281
    /**
282
     * Alias of {@see SourceInterface::setProperties()}
283
     *
284
     * @param  array $properties An array of property identifiers.
285
     * @return CollectionLoader Chainable
286
     */
287
    public function setProperties(array $properties)
288
    {
289
        $this->source()->setProperties($properties);
290
291
        return $this;
292
    }
293
294
    /**
295
     * Alias of {@see SourceInterface::addProperty()}
296
     *
297
     * @param  string $property A property identifier.
298
     * @return CollectionLoader Chainable
299
     */
300
    public function addProperty($property)
301
    {
302
        $this->source()->addProperty($property);
303
304
        return $this;
305
    }
306
307
    /**
308
     * Set "search" keywords to filter multiple properties.
309
     *
310
     * @param  array $keywords An array of keywords and properties.
311
     * @return CollectionLoader Chainable
312
     */
313
    public function setKeywords(array $keywords)
314
    {
315
        foreach ($keywords as $k) {
316
            $keyword = $k[0];
317
            $properties = (isset($k[1]) ? $k[1] : null);
318
            $this->addKeyword($keyword, $properties);
319
        }
320
321
        return $this;
322
    }
323
324
    /**
325
     * Add a "search" keyword filter to multiple properties.
326
     *
327
     * @param  string $keyword    A value to match among $properties.
328
     * @param  array  $properties An array of property identifiers.
329
     * @return CollectionLoader Chainable
330
     */
331
    public function addKeyword($keyword, array $properties = null)
332
    {
333
        if (!is_array($properties) || empty($properties)) {
334
            $properties = [];
335
        }
336
337
        foreach ($properties as $propertyIdent) {
338
            $val = ('%'.$keyword.'%');
339
            $this->addFilter([
340
                'property' => $propertyIdent,
341
                'val'      => $val,
342
                'operator' => 'LIKE',
343
                'operand'  => 'OR'
344
            ]);
345
        }
346
347
        return $this;
348
    }
349
350
    /**
351
     * Alias of {@see SourceInterface::filters()}
352
     *
353
     * @return array
354
     */
355
    public function filters()
356
    {
357
        return $this->source()->filters();
358
    }
359
360
    /**
361
     * Alias of {@see SourceInterface::setFilters()}
362
     *
363
     * @param  array $filters An array of filters.
364
     * @return Collection Chainable
365
     */
366
    public function setFilters(array $filters)
367
    {
368
        $this->source()->setFilters($filters);
369
370
        return $this;
371
    }
372
373
    /**
374
     * Alias of {@see SourceInterface::addFilters()}
375
     *
376
     * @param  array $filters An array of filters.
377
     * @return Collection Chainable
378
     */
379
    public function addFilters(array $filters)
380
    {
381
        foreach ($filters as $f) {
382
            $this->addFilter($f);
383
        }
384
385
        return $this;
386
    }
387
388
    /**
389
     * Alias of {@see SourceInterface::addFilter()}
390
     *
391
     * @param  string|array|Filter $param   A property identifier, filter array, or Filter object.
392
     * @param  mixed               $val     Optional. The value to match. Only used if the first argument is a string.
393
     * @param  array               $options Optional. Filter options. Only used if the first argument is a string.
394
     * @return CollectionLoader Chainable
395
     */
396
    public function addFilter($param, $val = null, array $options = null)
397
    {
398
        $this->source()->addFilter($param, $val, $options);
399
400
        return $this;
401
    }
402
403
    /**
404
     * Alias of {@see SourceInterface::orders()}
405
     *
406
     * @return array
407
     */
408
    public function orders()
409
    {
410
        return $this->source()->orders();
411
    }
412
413
    /**
414
     * Alias of {@see SourceInterface::setOrders()}
415
     *
416
     * @param  array $orders An array of orders.
417
     * @return CollectionLoader Chainable
418
     */
419
    public function setOrders(array $orders)
420
    {
421
        $this->source()->setOrders($orders);
422
423
        return $this;
424
    }
425
426
    /**
427
     * Alias of {@see SourceInterface::addOrders()}
428
     *
429
     * @param  array $orders An array of orders.
430
     * @return Collection Chainable
431
     */
432
    public function addOrders(array $orders)
433
    {
434
        foreach ($orders as $o) {
435
            $this->addOrder($o);
436
        }
437
438
        return $this;
439
    }
440
441
    /**
442
     * Alias of {@see SourceInterface::addOrder()}
443
     *
444
     * @param  string|array|Order $param        A property identifier, order array, or Order object.
445
     * @param  string             $mode         Optional. Sort order. Only used if the first argument is a string.
446
     * @param  array              $orderOptions Optional. Filter options. Only used if the first argument is a string.
447
     * @return CollectionLoader Chainable
448
     */
449
    public function addOrder($param, $mode = 'asc', array $orderOptions = null)
450
    {
451
        $this->source()->addOrder($param, $mode, $orderOptions);
452
453
        return $this;
454
    }
455
456
    /**
457
     * Alias of {@see SourceInterface::pagination()}
458
     *
459
     * @return Pagination
460
     */
461
    public function pagination()
462
    {
463
        return $this->source()->pagination();
464
    }
465
466
    /**
467
     * Alias of {@see SourceInterface::setPagination()}
468
     *
469
     * @param  mixed $param An associative array of pagination settings.
470
     * @return CollectionLoader Chainable
471
     */
472
    public function setPagination($param)
473
    {
474
        $this->source()->setPagination($param);
475
476
        return $this;
477
    }
478
479
    /**
480
     * Alias of {@see PaginationInterface::page()}
481
     *
482
     * @return integer
483
     */
484
    public function page()
485
    {
486
        return $this->pagination()->page();
487
    }
488
489
    /**
490
     * Alias of {@see PaginationInterface::pagination()}
491
     *
492
     * @param  integer $page A page number.
493
     * @return CollectionLoader Chainable
494
     */
495
    public function setPage($page)
496
    {
497
        $this->pagination()->setPage($page);
498
499
        return $this;
500
    }
501
502
    /**
503
     * Alias of {@see PaginationInterface::numPerPage()}
504
     *
505
     * @return integer
506
     */
507
    public function numPerPage()
508
    {
509
        return $this->pagination()->numPerPage();
510
    }
511
512
    /**
513
     * Alias of {@see PaginationInterface::setNumPerPage()}
514
     *
515
     * @param  integer $num The number of items to display per page.
516
     * @return CollectionLoader Chainable
517
     */
518
    public function setNumPerPage($num)
519
    {
520
        $this->pagination()->setNumPerPage($num);
521
522
        return $this;
523
    }
524
525
    /**
526
     * Set the callback routine applied to every object added to the collection.
527
     *
528
     * @param callable $callback The callback routine.
529
     * @return CollectionLoader Chainable
530
     */
531
    public function setCallback(callable $callback)
532
    {
533
        $this->callback = $callback;
534
535
        return $this;
536
    }
537
538
    /**
539
     * Retrieve the callback routine applied to every object added to the collection.
540
     *
541
     * @return callable|null
542
     */
543
    public function callback()
544
    {
545
        return $this->callback;
546
    }
547
548
    /**
549
     * Load a collection from source.
550
     *
551
     * @param  string|null   $ident    Optional. A pre-defined list to use from the model.
552
     * @param  callable|null $callback Process each entity after applying raw data.
553
     *    Leave blank to use {@see CollectionLoader::callback()}.
554
     * @param  callable|null $before   Process each entity before applying raw data.
555
     * @throws Exception If the database connection fails.
556
     * @return ModelInterface[]|ArrayAccess
557
     */
558
    public function load($ident = null, callable $callback = null, callable $before = null)
559
    {
560
        // Unused.
561
        unset($ident);
562
563
        $query = $this->source()->sqlLoad();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Charcoal\Source\SourceInterface as the method sqlLoad() does only exist in the following implementations of said interface: Charcoal\Source\DatabaseSource.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
564
565
        return $this->loadFromQuery($query, $callback, $before);
566
    }
567
568
    /**
569
     * Get the total number of items for this collection query.
570
     *
571
     * @throws RuntimeException If the database connection fails.
572
     * @return integer
573
     */
574
    public function loadCount()
575
    {
576
        $query = $this->source()->sqlLoadCount();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Charcoal\Source\SourceInterface as the method sqlLoadCount() does only exist in the following implementations of said interface: Charcoal\Source\DatabaseSource.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
577
578
        $db = $this->source()->db();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Charcoal\Source\SourceInterface as the method db() does only exist in the following implementations of said interface: Charcoal\Source\DatabaseSource.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
579
        if (!$db) {
580
            throw new RuntimeException(
581
                'Could not instanciate a database connection.'
582
            );
583
        }
584
        $this->logger->debug($query);
585
586
        $sth = $db->prepare($query);
587
        $sth->execute();
588
        $res = $sth->fetchColumn(0);
589
590
        return (int)$res;
591
    }
592
593
    /**
594
     * Load list from query.
595
     *
596
     * **Example — Binding values to $query**
597
     *
598
     * ```php
599
     * $this->loadFromQuery([
600
     *     'SELECT name, colour, calories FROM fruit WHERE calories < :calories AND colour = :colour',
601
     *     [
602
     *         'calories' => 150,
603
     *         'colour'   => 'red'
604
     *     ],
605
     *     [ 'calories' => PDO::PARAM_INT ]
606
     * ]);
607
     * ```
608
     *
609
     * @param  string|array  $query    The SQL query as a string or an array composed of the query,
610
     *     parameter binds, and types of parameter bindings.
611
     * @param  callable|null $callback Process each entity after applying raw data.
612
     *    Leave blank to use {@see CollectionLoader::callback()}.
613
     * @param  callable|null $before   Process each entity before applying raw data.
614
     * @throws RuntimeException If the database connection fails.
615
     * @throws InvalidArgumentException If the SQL string/set is invalid.
616
     * @return ModelInterface[]|ArrayAccess
617
     */
618
    public function loadFromQuery($query, callable $callback = null, callable $before = null)
619
    {
620
        $db = $this->source()->db();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Charcoal\Source\SourceInterface as the method db() does only exist in the following implementations of said interface: Charcoal\Source\DatabaseSource.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
621
622
        if (!$db) {
623
            throw new RuntimeException(
624
                'Could not instanciate a database connection.'
625
            );
626
        }
627
628
        /** @todo Filter binds */
629
        if (is_string($query)) {
630
            $this->logger->debug($query);
631
            $sth = $db->prepare($query);
632
            $sth->execute();
633
        } elseif (is_array($query)) {
634
            list($query, $binds, $types) = array_pad($query, 3, []);
635
            $sth = $this->source()->dbQuery($query, $binds, $types);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Charcoal\Source\SourceInterface as the method dbQuery() does only exist in the following implementations of said interface: Charcoal\Source\DatabaseSource.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
636
        } else {
637
            throw new InvalidArgumentException(sprintf(
638
                'The SQL query must be a string or an array: '.
639
                '[ string $query, array $binds, array $dataTypes ]; '.
640
                'received %s',
641
                is_object($query) ? get_class($query) : $query
642
            ));
643
        }
644
645
        $sth->setFetchMode(PDO::FETCH_ASSOC);
646
647
        if ($callback === null) {
648
            $callback = $this->callback();
649
        }
650
651
        return $this->processCollection($sth, $before, $callback);
652
    }
653
654
    /**
655
     * Process the collection of raw data.
656
     *
657
     * @param  mixed[]|Traversable $results The raw result set.
658
     * @param  callable|null       $before  Process each entity before applying raw data.
659
     * @param  callable|null       $after   Process each entity after applying raw data.
660
     * @return ModelInterface[]|ArrayAccess
661
     */
662
    protected function processCollection($results, callable $before = null, callable $after = null)
663
    {
664
        $collection   = $this->createCollection();
665
        foreach ($results as $objData) {
666
            $obj = $this->processModel($objData, $before, $after);
667
668
            if ($obj instanceof ModelInterface) {
669
                $collection[] = $obj;
670
            }
671
        }
672
673
        return $collection;
674
    }
675
676
    /**
677
     * Process the raw data for one model.
678
     *
679
     * @param  mixed         $objData The raw dataset.
680
     * @param  callable|null $before  Process each entity before applying raw data.
681
     * @param  callable|null $after   Process each entity after applying raw data.
682
     * @return ModelInterface|ArrayAccess|null
683
     */
684
    protected function processModel($objData, callable $before = null, callable $after = null)
685
    {
686
        if ($this->dynamicTypeField && isset($objData[$this->dynamicTypeField])) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->dynamicTypeField of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null 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...
687
            $objType = $objData[$this->dynamicTypeField];
688
        } else {
689
            $objType = get_class($this->model());
690
        }
691
692
        $obj = $this->factory()->create($objType);
693
694
        if ($before !== null) {
695
            call_user_func_array($before, [ &$obj ]);
696
        }
697
698
        $obj->setFlatData($objData);
699
700
        if ($after !== null) {
701
            call_user_func_array($after, [ &$obj ]);
702
        }
703
704
        return $obj;
705
    }
706
707
    /**
708
     * Create a collection class or array.
709
     *
710
     * @throws RuntimeException If the collection class is invalid.
711
     * @return array|ArrayAccess
712
     */
713
    public function createCollection()
714
    {
715
        $collectClass = $this->collectionClass();
716
        if ($collectClass === 'array') {
717
            return [];
718
        }
719
720
        if (!class_exists($collectClass)) {
721
            throw new RuntimeException(sprintf(
722
                'Collection class [%s] does not exist.',
723
                $collectClass
724
            ));
725
        }
726
727
        if (!is_subclass_of($collectClass, ArrayAccess::class)) {
0 ignored issues
show
Bug introduced by
Due to PHP Bug #53727, is_subclass_of might return inconsistent results on some PHP versions if \ArrayAccess::class can be an interface. If so, you could instead use ReflectionClass::implementsInterface.
Loading history...
728
            throw new RuntimeException(sprintf(
729
                'Collection class [%s] must implement ArrayAccess.',
730
                $collectClass
731
            ));
732
        }
733
734
        $collection = new $collectClass;
735
736
        return $collection;
737
    }
738
739
    /**
740
     * Set the class name of the collection.
741
     *
742
     * @param  string $className The class name of the collection.
743
     * @throws InvalidArgumentException If the class name is not a string.
744
     * @return AbstractPropertyDisplay Chainable
745
     */
746
    public function setCollectionClass($className)
747
    {
748
        if (!is_string($className)) {
749
            throw new InvalidArgumentException(
750
                'Collection class name must be a string.'
751
            );
752
        }
753
754
        $this->collectionClass = $className;
755
756
        return $this;
757
    }
758
759
    /**
760
     * Retrieve the class name of the collection.
761
     *
762
     * @return string
763
     */
764
    public function collectionClass()
765
    {
766
        return $this->collectionClass;
767
    }
768
769
    /**
770
     * Allow an object to define how the key getter are called.
771
     *
772
     * @param string $key The key to get the getter from.
773
     * @return string The getter method name, for a given key.
774
     */
775
    protected function getter($key)
776
    {
777
        $getter = $key;
778
        return $this->camelize($getter);
779
    }
780
781
    /**
782
     * Allow an object to define how the key setter are called.
783
     *
784
     * @param string $key The key to get the setter from.
785
     * @return string The setter method name, for a given key.
786
     */
787
    protected function setter($key)
788
    {
789
        $setter = 'set_'.$key;
790
        return $this->camelize($setter);
791
    }
792
793
    /**
794
     * Transform a snake_case string to camelCase.
795
     *
796
     * @param string $str The snake_case string to camelize.
797
     * @return string The camelcase'd string.
798
     */
799
    protected function camelize($str)
800
    {
801
        return lcfirst(implode('', array_map('ucfirst', explode('_', $str))));
802
    }
803
}
804