Test Setup Failed
Push — master ( 4ba303...4762d2 )
by Chauncey
10:40
created

CollectionLoader::processModel()   B

Complexity

Conditions 5
Paths 8

Size

Total Lines 22
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 22
rs 8.6737
c 0
b 0
f 0
cc 5
eloc 12
nc 8
nop 3
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::addFilter()}
375
     *
376
     * @param  string|array|Filter $param   A property identifier, filter array, or Filter object.
377
     * @param  mixed               $val     Optional. The value to match. Only used if the first argument is a string.
378
     * @param  array               $options Optional. Filter options. Only used if the first argument is a string.
379
     * @return CollectionLoader Chainable
380
     */
381
    public function addFilter($param, $val = null, array $options = null)
382
    {
383
        $this->source()->addFilter($param, $val, $options);
384
385
        return $this;
386
    }
387
388
    /**
389
     * Alias of {@see SourceInterface::orders()}
390
     *
391
     * @return array
392
     */
393
    public function orders()
394
    {
395
        return $this->source()->orders();
396
    }
397
398
    /**
399
     * Alias of {@see SourceInterface::setOrders()}
400
     *
401
     * @param  array $orders An array of orders.
402
     * @return CollectionLoader Chainable
403
     */
404
    public function setOrders(array $orders)
405
    {
406
        $this->source()->setOrders($orders);
407
408
        return $this;
409
    }
410
411
    /**
412
     * Alias of {@see SourceInterface::addOrder()}
413
     *
414
     * @param  string|array|Order $param        A property identifier, order array, or Order object.
415
     * @param  string             $mode         Optional. Sort order. Only used if the first argument is a string.
416
     * @param  array              $orderOptions Optional. Filter options. Only used if the first argument is a string.
417
     * @return CollectionLoader Chainable
418
     */
419
    public function addOrder($param, $mode = 'asc', array $orderOptions = null)
420
    {
421
        $this->source()->addOrder($param, $mode, $orderOptions);
422
423
        return $this;
424
    }
425
426
    /**
427
     * Alias of {@see SourceInterface::pagination()}
428
     *
429
     * @return Pagination
430
     */
431
    public function pagination()
432
    {
433
        return $this->source()->pagination();
434
    }
435
436
    /**
437
     * Alias of {@see SourceInterface::setPagination()}
438
     *
439
     * @param  mixed $param An associative array of pagination settings.
440
     * @return CollectionLoader Chainable
441
     */
442
    public function setPagination($param)
443
    {
444
        $this->source()->setPagination($param);
445
446
        return $this;
447
    }
448
449
    /**
450
     * Alias of {@see PaginationInterface::page()}
451
     *
452
     * @return integer
453
     */
454
    public function page()
455
    {
456
        return $this->pagination()->page();
457
    }
458
459
    /**
460
     * Alias of {@see PaginationInterface::pagination()}
461
     *
462
     * @param  integer $page A page number.
463
     * @return CollectionLoader Chainable
464
     */
465
    public function setPage($page)
466
    {
467
        $this->pagination()->setPage($page);
468
469
        return $this;
470
    }
471
472
    /**
473
     * Alias of {@see PaginationInterface::numPerPage()}
474
     *
475
     * @return integer
476
     */
477
    public function numPerPage()
478
    {
479
        return $this->pagination()->numPerPage();
480
    }
481
482
    /**
483
     * Alias of {@see PaginationInterface::setNumPerPage()}
484
     *
485
     * @param  integer $num The number of items to display per page.
486
     * @return CollectionLoader Chainable
487
     */
488
    public function setNumPerPage($num)
489
    {
490
        $this->pagination()->setNumPerPage($num);
491
492
        return $this;
493
    }
494
495
    /**
496
     * Set the callback routine applied to every object added to the collection.
497
     *
498
     * @param callable $callback The callback routine.
499
     * @return CollectionLoader Chainable
500
     */
501
    public function setCallback(callable $callback)
502
    {
503
        $this->callback = $callback;
504
505
        return $this;
506
    }
507
508
    /**
509
     * Retrieve the callback routine applied to every object added to the collection.
510
     *
511
     * @return callable|null
512
     */
513
    public function callback()
514
    {
515
        return $this->callback;
516
    }
517
518
    /**
519
     * Load a collection from source.
520
     *
521
     * @param  string|null   $ident    Optional. A pre-defined list to use from the model.
522
     * @param  callable|null $callback Process each entity after applying raw data.
523
     *    Leave blank to use {@see CollectionLoader::callback()}.
524
     * @param  callable|null $before   Process each entity before applying raw data.
525
     * @throws Exception If the database connection fails.
526
     * @return ModelInterface[]|ArrayAccess
527
     */
528
    public function load($ident = null, callable $callback = null, callable $before = null)
529
    {
530
        // Unused.
531
        unset($ident);
532
533
        $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...
534
535
        return $this->loadFromQuery($query, $callback, $before);
536
    }
537
538
    /**
539
     * Get the total number of items for this collection query.
540
     *
541
     * @throws RuntimeException If the database connection fails.
542
     * @return integer
543
     */
544
    public function loadCount()
545
    {
546
        $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...
547
548
        $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...
549
        if (!$db) {
550
            throw new RuntimeException(
551
                'Could not instanciate a database connection.'
552
            );
553
        }
554
        $this->logger->debug($query);
555
556
        $sth = $db->prepare($query);
557
        $sth->execute();
558
        $res = $sth->fetchColumn(0);
559
560
        return (int)$res;
561
    }
562
563
    /**
564
     * Load list from query.
565
     *
566
     * **Example — Binding values to $query**
567
     *
568
     * ```php
569
     * $this->loadFromQuery([
570
     *     'SELECT name, colour, calories FROM fruit WHERE calories < :calories AND colour = :colour',
571
     *     [
572
     *         'calories' => 150,
573
     *         'colour'   => 'red'
574
     *     ],
575
     *     [ 'calories' => PDO::PARAM_INT ]
576
     * ]);
577
     * ```
578
     *
579
     * @param  string|array  $query    The SQL query as a string or an array composed of the query,
580
     *     parameter binds, and types of parameter bindings.
581
     * @param  callable|null $callback Process each entity after applying raw data.
582
     *    Leave blank to use {@see CollectionLoader::callback()}.
583
     * @param  callable|null $before   Process each entity before applying raw data.
584
     * @throws RuntimeException If the database connection fails.
585
     * @throws InvalidArgumentException If the SQL string/set is invalid.
586
     * @return ModelInterface[]|ArrayAccess
587
     */
588
    public function loadFromQuery($query, callable $callback = null, callable $before = null)
589
    {
590
        $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...
591
592
        if (!$db) {
593
            throw new RuntimeException(
594
                'Could not instanciate a database connection.'
595
            );
596
        }
597
598
        /** @todo Filter binds */
599
        if (is_string($query)) {
600
            $this->logger->debug($query);
601
            $sth = $db->prepare($query);
602
            $sth->execute();
603
        } elseif (is_array($query)) {
604
            list($query, $binds, $types) = array_pad($query, 3, []);
605
            $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...
606
        } else {
607
            throw new InvalidArgumentException(sprintf(
608
                'The SQL query must be a string or an array: '.
609
                '[ string $query, array $binds, array $dataTypes ]; '.
610
                'received %s',
611
                is_object($query) ? get_class($query) : $query
612
            ));
613
        }
614
615
        $sth->setFetchMode(PDO::FETCH_ASSOC);
616
617
        if ($callback === null) {
618
            $callback = $this->callback();
619
        }
620
621
        return $this->processCollection($sth, $before, $callback);
622
    }
623
624
    /**
625
     * Process the collection of raw data.
626
     *
627
     * @param  mixed[]|Traversable $results The raw result set.
628
     * @param  callable|null       $before  Process each entity before applying raw data.
629
     * @param  callable|null       $after   Process each entity after applying raw data.
630
     * @return ModelInterface[]|ArrayAccess
631
     */
632
    protected function processCollection($results, callable $before = null, callable $after = null)
633
    {
634
        $modelObjType = $this->model()->objType();
0 ignored issues
show
Unused Code introduced by
$modelObjType is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
635
        $collection   = $this->createCollection();
636
        foreach ($results as $objData) {
637
            $obj = $this->processModel($objData, $before, $after);
638
639
            if ($obj instanceof ModelInterface) {
640
                $collection[] = $obj;
641
            }
642
        }
643
644
        return $collection;
645
    }
646
647
    /**
648
     * Process the raw data for one model.
649
     *
650
     * @param  mixed         $objData The raw dataset.
651
     * @param  callable|null $before  Process each entity before applying raw data.
652
     * @param  callable|null $after   Process each entity after applying raw data.
653
     * @return ModelInterface|ArrayAccess|null
654
     */
655
    protected function processModel($objData, callable $before = null, callable $after = null)
656
    {
657
        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...
658
            $objType = $objData[$this->dynamicTypeField];
659
        } else {
660
            $objType = $this->model()->objType();
661
        }
662
663
        $obj = $this->factory()->create($objType);
664
665
        if ($before !== null) {
666
            call_user_func_array($before, [ &$obj ]);
667
        }
668
669
        $obj->setFlatData($objData);
670
671
        if ($after !== null) {
672
            call_user_func_array($after, [ &$obj ]);
673
        }
674
675
        return $obj;
676
    }
677
678
    /**
679
     * Create a collection class or array.
680
     *
681
     * @throws RuntimeException If the collection class is invalid.
682
     * @return array|ArrayAccess
683
     */
684
    public function createCollection()
685
    {
686
        $collectClass = $this->collectionClass();
687
        if ($collectClass === 'array') {
688
            return [];
689
        }
690
691
        if (!class_exists($collectClass)) {
692
            throw new RuntimeException(sprintf(
693
                'Collection class [%s] does not exist.',
694
                $collectClass
695
            ));
696
        }
697
698
        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...
699
            throw new RuntimeException(sprintf(
700
                'Collection class [%s] must implement ArrayAccess.',
701
                $collectClass
702
            ));
703
        }
704
705
        $collection = new $collectClass;
706
707
        return $collection;
708
    }
709
710
    /**
711
     * Set the class name of the collection.
712
     *
713
     * @param  string $className The class name of the collection.
714
     * @throws InvalidArgumentException If the class name is not a string.
715
     * @return AbstractPropertyDisplay Chainable
716
     */
717
    public function setCollectionClass($className)
718
    {
719
        if (!is_string($className)) {
720
            throw new InvalidArgumentException(
721
                'Collection class name must be a string.'
722
            );
723
        }
724
725
        $this->collectionClass = $className;
726
727
        return $this;
728
    }
729
730
    /**
731
     * Retrieve the class name of the collection.
732
     *
733
     * @return string
734
     */
735
    public function collectionClass()
736
    {
737
        return $this->collectionClass;
738
    }
739
740
    /**
741
     * Allow an object to define how the key getter are called.
742
     *
743
     * @param string $key The key to get the getter from.
744
     * @return string The getter method name, for a given key.
745
     */
746
    protected function getter($key)
747
    {
748
        $getter = $key;
749
        return $this->camelize($getter);
750
    }
751
752
    /**
753
     * Allow an object to define how the key setter are called.
754
     *
755
     * @param string $key The key to get the setter from.
756
     * @return string The setter method name, for a given key.
757
     */
758
    protected function setter($key)
759
    {
760
        $setter = 'set_'.$key;
761
        return $this->camelize($setter);
762
    }
763
764
    /**
765
     * Transform a snake_case string to camelCase.
766
     *
767
     * @param string $str The snake_case string to camelize.
768
     * @return string The camelcase'd string.
769
     */
770
    protected function camelize($str)
771
    {
772
        return lcfirst(implode('', array_map('ucfirst', explode('_', $str))));
773
    }
774
}
775