Passed
Branch master (018ba4)
by Mathieu
06:43
created

CollectionLoader   F

Complexity

Total Complexity 78

Size/Duplication

Total Lines 775
Duplicated Lines 1.68 %

Importance

Changes 0
Metric Value
dl 13
loc 775
rs 1.6901
c 0
b 0
f 0
wmc 78

43 Methods

Rating   Name   Duplication   Size   Complexity  
A pagination() 0 3 1
A page() 0 3 1
A addOrders() 0 6 2
B processModel() 0 21 5
A setOrders() 0 4 1
A camelize() 0 3 1
A setDynamicTypeField() 0 11 2
B loadFromQuery() 0 34 6
A setNumPerPage() 0 5 1
A addOrder() 0 4 1
A setFactory() 0 5 1
A setProperties() 0 5 1
A getter() 0 4 1
A processCollection() 0 12 3
A addFilters() 0 6 2
A setCallback() 0 5 1
A setFilters() 0 4 1
A loadCount() 0 17 2
A setter() 0 4 1
A callback() 0 3 1
A setModel() 0 20 3
A orders() 0 3 1
A setPagination() 0 5 1
A numPerPage() 0 3 1
B createCollection() 0 24 4
A setKeywords() 0 9 3
A hasModel() 0 3 1
A source() 0 7 2
A addProperty() 0 5 1
B __construct() 0 18 5
A factory() 0 9 2
A setCollectionClass() 0 11 2
A filters() 0 3 1
A addFilter() 0 4 1
A setData() 12 12 3
A properties() 0 3 1
A reset() 0 10 2
A setSource() 0 7 1
A model() 0 7 2
A addKeyword() 0 17 3
A collectionClass() 0 3 1
A setPage() 0 5 1
A load() 0 8 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like CollectionLoader often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use CollectionLoader, and based on these observations, apply Extract Interface, too.

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\FilterInterface;
23
use Charcoal\Source\OrderInterface;
24
use Charcoal\Source\PaginationInterface;
25
use Charcoal\Source\SourceInterface;
26
27
/**
28
 * Object Collection Loader
29
 */
30
class CollectionLoader implements LoggerAwareInterface
31
{
32
    use LoggerAwareTrait;
33
34
    /**
35
     * The source to load objects from.
36
     *
37
     * @var SourceInterface
38
     */
39
    private $source;
40
41
    /**
42
     * The model to load the collection from.
43
     *
44
     * @var ModelInterface
45
     */
46
    private $model;
47
48
    /**
49
     * Store the factory instance for the current class.
50
     *
51
     * @var FactoryInterface
52
     */
53
    private $factory;
54
55
    /**
56
     * The callback routine applied to every object added to the collection.
57
     *
58
     * @var callable|null
59
     */
60
    private $callback;
61
62
    /**
63
     * The field which defines the data's model.
64
     *
65
     * @var string|null
66
     */
67
    private $dynamicTypeField;
68
69
    /**
70
     * The class name of the collection to use.
71
     *
72
     * Must be a fully-qualified PHP namespace and an implementation of {@see ArrayAccess}.
73
     *
74
     * @var string
75
     */
76
    private $collectionClass = Collection::class;
77
78
    /**
79
     * Return a new CollectionLoader object.
80
     *
81
     * @param array $data The loader's dependencies.
82
     */
83
    public function __construct(array $data)
84
    {
85
        if (!isset($data['logger'])) {
86
            $data['logger'] = new NullLogger();
87
        }
88
89
        $this->setLogger($data['logger']);
90
91
        if (isset($data['collection'])) {
92
            $this->setCollectionClass($data['collection']);
93
        }
94
95
        if (isset($data['factory'])) {
96
            $this->setFactory($data['factory']);
97
        }
98
99
        if (isset($data['model'])) {
100
            $this->setModel($data['model']);
101
        }
102
    }
103
104
    /**
105
     * Set an object model factory.
106
     *
107
     * @param  FactoryInterface $factory The model factory, to create objects.
108
     * @return self
109
     */
110
    public function setFactory(FactoryInterface $factory)
111
    {
112
        $this->factory = $factory;
113
114
        return $this;
115
    }
116
117
    /**
118
     * Retrieve the object model factory.
119
     *
120
     * @throws RuntimeException If the model factory was not previously set.
121
     * @return FactoryInterface
122
     */
123
    protected function factory()
124
    {
125
        if ($this->factory === null) {
126
            throw new RuntimeException(
127
                sprintf('Model Factory is not defined for "%s"', get_class($this))
128
            );
129
        }
130
131
        return $this->factory;
132
    }
133
134
    /**
135
     * Set the loader settings.
136
     *
137
     * @param  array $data Data to assign to the loader.
138
     * @return self
139
     */
140 View Code Duplication
    public function setData(array $data)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
141
    {
142
        foreach ($data as $key => $val) {
143
            $setter = $this->setter($key);
144
            if (is_callable([ $this, $setter ])) {
145
                $this->{$setter}($val);
146
            } else {
147
                $this->{$key} = $val;
148
            }
149
        }
150
151
        return $this;
152
    }
153
154
    /**
155
     * Retrieve the source to load objects from.
156
     *
157
     * @throws RuntimeException If no source has been defined.
158
     * @return SourceInterface
159
     */
160
    public function source()
161
    {
162
        if ($this->source === null) {
163
            throw new RuntimeException('No source set.');
164
        }
165
166
        return $this->source;
167
    }
168
169
    /**
170
     * Set the source to load objects from.
171
     *
172
     * @param  SourceInterface $source A data source.
173
     * @return self
174
     */
175
    public function setSource(SourceInterface $source)
176
    {
177
        $source->reset();
0 ignored issues
show
Bug introduced by
The method reset() does not exist on Charcoal\Source\SourceInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Charcoal\Source\SourceInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

177
        $source->/** @scrutinizer ignore-call */ 
178
                 reset();
Loading history...
178
179
        $this->source = $source;
180
181
        return $this;
182
    }
183
184
    /**
185
     * Reset everything but the model.
186
     *
187
     * @return self
188
     */
189
    public function reset()
190
    {
191
        if ($this->source) {
192
            $this->source()->reset();
193
        }
194
195
        $this->callback = null;
196
        $this->dynamicTypeField = null;
197
198
        return $this;
199
    }
200
201
    /**
202
     * Retrieve the object model.
203
     *
204
     * @throws RuntimeException If no model has been defined.
205
     * @return ModelInterface
206
     */
207
    public function model()
208
    {
209
        if ($this->model === null) {
210
            throw new RuntimeException('The collection loader must have a model.');
211
        }
212
213
        return $this->model;
214
    }
215
216
    /**
217
     * Determine if the loader has an object model.
218
     *
219
     * @return boolean
220
     */
221
    public function hasModel()
222
    {
223
        return !!$this->model;
224
    }
225
226
    /**
227
     * Set the model to use for the loaded objects.
228
     *
229
     * @param  string|ModelInterface $model An object model.
230
     * @throws InvalidArgumentException If the given argument is not a model.
231
     * @return self
232
     */
233
    public function setModel($model)
234
    {
235
        if (is_string($model)) {
236
            $model = $this->factory()->get($model);
237
        }
238
239
        if (!$model instanceof ModelInterface) {
240
            throw new InvalidArgumentException(
241
                sprintf(
242
                    'The model must be an instance of "%s"',
243
                    ModelInterface::class
244
                )
245
            );
246
        }
247
248
        $this->model = $model;
249
250
        $this->setSource($model->source());
251
252
        return $this;
253
    }
254
255
    /**
256
     * @param  string $field The field to use for dynamic object type.
257
     * @throws InvalidArgumentException If the field is not a string.
258
     * @return self
259
     */
260
    public function setDynamicTypeField($field)
261
    {
262
        if (!is_string($field)) {
263
            throw new InvalidArgumentException(
264
                'Dynamic type field must be a string'
265
            );
266
        }
267
268
        $this->dynamicTypeField = $field;
269
270
        return $this;
271
    }
272
273
    /**
274
     * Alias of {@see SourceInterface::properties()}
275
     *
276
     * @return array
277
     */
278
    public function properties()
279
    {
280
        return $this->source()->properties();
281
    }
282
283
    /**
284
     * Alias of {@see SourceInterface::setProperties()}
285
     *
286
     * @param  array $properties An array of property identifiers.
287
     * @return self
288
     */
289
    public function setProperties(array $properties)
290
    {
291
        $this->source()->setProperties($properties);
292
293
        return $this;
294
    }
295
296
    /**
297
     * Alias of {@see SourceInterface::addProperty()}
298
     *
299
     * @param  string $property A property identifier.
300
     * @return self
301
     */
302
    public function addProperty($property)
303
    {
304
        $this->source()->addProperty($property);
305
306
        return $this;
307
    }
308
309
    /**
310
     * Set "search" keywords to filter multiple properties.
311
     *
312
     * @param  array $keywords An array of keywords and properties.
313
     *     Expected format: `[ "search query", [ "field names…" ] ]`.
314
     * @return self
315
     */
316
    public function setKeywords(array $keywords)
317
    {
318
        foreach ($keywords as $query) {
319
            $keyword    = $query[0];
320
            $properties = (isset($query[1]) ? (array)$query[1] : null);
321
            $this->addKeyword($keyword, $properties);
322
        }
323
324
        return $this;
325
    }
326
327
    /**
328
     * Add a "search" keyword filter to multiple properties.
329
     *
330
     * @param  string $keyword    A value to match among $properties.
331
     * @param  array  $properties One or more of properties to search amongst.
332
     * @return self
333
     */
334
    public function addKeyword($keyword, array $properties = null)
335
    {
336
        if ($properties === null) {
337
            $properties = [];
338
        }
339
340
        foreach ($properties as $propertyIdent) {
341
            $val = ('%'.$keyword.'%');
342
            $this->addFilter([
343
                'property' => $propertyIdent,
344
                'operator' => 'LIKE',
345
                'value'    => $val,
346
                'operand'  => 'OR'
347
            ]);
348
        }
349
350
        return $this;
351
    }
352
353
    /**
354
     * Alias of {@see SourceInterface::filters()}
355
     *
356
     * @return FilterInterface[]
357
     */
358
    public function filters()
359
    {
360
        return $this->source()->filters();
361
    }
362
363
    /**
364
     * Alias of {@see SourceInterface::setFilters()}
365
     *
366
     * @param  array $filters An array of filters.
367
     * @return self
368
     */
369
    public function setFilters(array $filters)
370
    {
371
        $this->source()->setFilters($filters);
372
        return $this;
373
    }
374
375
    /**
376
     * Alias of {@see SourceInterface::addFilters()}
377
     *
378
     * @param  array $filters An array of filters.
379
     * @return self
380
     */
381
    public function addFilters(array $filters)
382
    {
383
        foreach ($filters as $f) {
384
            $this->addFilter($f);
385
        }
386
        return $this;
387
    }
388
389
    /**
390
     * Alias of {@see SourceInterface::addFilter()}
391
     *
392
     * @param  mixed $param   The property to filter by,
393
     *     a {@see FilterInterface} object,
394
     *     or a filter array structure.
395
     * @param  mixed $value   Optional value for the property to compare against.
396
     *     Only used if the first argument is a string.
397
     * @param  array $options Optional extra settings to apply on the filter.
398
     * @return self
399
     */
400
    public function addFilter($param, $value = null, array $options = null)
401
    {
402
        $this->source()->addFilter($param, $value, $options);
0 ignored issues
show
Unused Code introduced by
The call to Charcoal\Source\FilterCo...nInterface::addFilter() has too many arguments starting with $value. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

402
        $this->source()->/** @scrutinizer ignore-call */ addFilter($param, $value, $options);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
403
        return $this;
404
    }
405
406
    /**
407
     * Alias of {@see SourceInterface::orders()}
408
     *
409
     * @return OrderInterface[]
410
     */
411
    public function orders()
412
    {
413
        return $this->source()->orders();
414
    }
415
416
    /**
417
     * Alias of {@see SourceInterface::setOrders()}
418
     *
419
     * @param  array $orders An array of orders.
420
     * @return self
421
     */
422
    public function setOrders(array $orders)
423
    {
424
        $this->source()->setOrders($orders);
425
        return $this;
426
    }
427
428
    /**
429
     * Alias of {@see SourceInterface::addOrders()}
430
     *
431
     * @param  array $orders An array of orders.
432
     * @return self
433
     */
434
    public function addOrders(array $orders)
435
    {
436
        foreach ($orders as $o) {
437
            $this->addOrder($o);
438
        }
439
        return $this;
440
    }
441
442
    /**
443
     * Alias of {@see SourceInterface::addOrder()}
444
     *
445
     * @param  mixed  $param   The property to sort by,
446
     *     a {@see OrderInterface} object,
447
     *     or a order array structure.
448
     * @param  string $mode    Optional sorting mode.
449
     *     Defaults to ascending if a property is provided.
450
     * @param  array  $options Optional extra settings to apply on the order.
451
     * @return self
452
     */
453
    public function addOrder($param, $mode = 'asc', array $options = null)
454
    {
455
        $this->source()->addOrder($param, $mode, $options);
0 ignored issues
show
Unused Code introduced by
The call to Charcoal\Source\OrderCol...onInterface::addOrder() has too many arguments starting with $mode. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

455
        $this->source()->/** @scrutinizer ignore-call */ addOrder($param, $mode, $options);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
456
        return $this;
457
    }
458
459
    /**
460
     * Alias of {@see SourceInterface::pagination()}
461
     *
462
     * @return PaginationInterface
463
     */
464
    public function pagination()
465
    {
466
        return $this->source()->pagination();
467
    }
468
469
    /**
470
     * Alias of {@see SourceInterface::setPagination()}
471
     *
472
     * @param  mixed $param An associative array of pagination settings.
473
     * @return self
474
     */
475
    public function setPagination($param)
476
    {
477
        $this->source()->setPagination($param);
478
479
        return $this;
480
    }
481
482
    /**
483
     * Alias of {@see PaginationInterface::page()}
484
     *
485
     * @return integer
486
     */
487
    public function page()
488
    {
489
        return $this->pagination()->page();
490
    }
491
492
    /**
493
     * Alias of {@see PaginationInterface::pagination()}
494
     *
495
     * @param  integer $page A page number.
496
     * @return self
497
     */
498
    public function setPage($page)
499
    {
500
        $this->pagination()->setPage($page);
501
502
        return $this;
503
    }
504
505
    /**
506
     * Alias of {@see PaginationInterface::numPerPage()}
507
     *
508
     * @return integer
509
     */
510
    public function numPerPage()
511
    {
512
        return $this->pagination()->numPerPage();
513
    }
514
515
    /**
516
     * Alias of {@see PaginationInterface::setNumPerPage()}
517
     *
518
     * @param  integer $num The number of items to display per page.
519
     * @return self
520
     */
521
    public function setNumPerPage($num)
522
    {
523
        $this->pagination()->setNumPerPage($num);
524
525
        return $this;
526
    }
527
528
    /**
529
     * Set the callback routine applied to every object added to the collection.
530
     *
531
     * @param  callable $callback The callback routine.
532
     * @return self
533
     */
534
    public function setCallback(callable $callback)
535
    {
536
        $this->callback = $callback;
537
538
        return $this;
539
    }
540
541
    /**
542
     * Retrieve the callback routine applied to every object added to the collection.
543
     *
544
     * @return callable|null
545
     */
546
    public function callback()
547
    {
548
        return $this->callback;
549
    }
550
551
    /**
552
     * Load a collection from source.
553
     *
554
     * @param  string|null   $ident    Optional. A pre-defined list to use from the model.
555
     * @param  callable|null $callback Process each entity after applying raw data.
556
     *    Leave blank to use {@see CollectionLoader::callback()}.
557
     * @param  callable|null $before   Process each entity before applying raw data.
558
     * @throws Exception If the database connection fails.
559
     * @return ModelInterface[]|ArrayAccess
560
     */
561
    public function load($ident = null, callable $callback = null, callable $before = null)
562
    {
563
        // Unused.
564
        unset($ident);
565
566
        $query = $this->source()->sqlLoad();
0 ignored issues
show
Bug introduced by
The method sqlLoad() does not exist on Charcoal\Source\SourceInterface. It seems like you code against a sub-type of Charcoal\Source\SourceInterface such as Charcoal\Source\DatabaseSource. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

566
        $query = $this->source()->/** @scrutinizer ignore-call */ sqlLoad();
Loading history...
567
568
        return $this->loadFromQuery($query, $callback, $before);
569
    }
570
571
    /**
572
     * Get the total number of items for this collection query.
573
     *
574
     * @throws RuntimeException If the database connection fails.
575
     * @return integer
576
     */
577
    public function loadCount()
578
    {
579
        $query = $this->source()->sqlLoadCount();
0 ignored issues
show
Bug introduced by
The method sqlLoadCount() does not exist on Charcoal\Source\SourceInterface. It seems like you code against a sub-type of Charcoal\Source\SourceInterface such as Charcoal\Source\DatabaseSource. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

579
        $query = $this->source()->/** @scrutinizer ignore-call */ sqlLoadCount();
Loading history...
580
581
        $db = $this->source()->db();
0 ignored issues
show
Bug introduced by
The method db() does not exist on Charcoal\Source\SourceInterface. It seems like you code against a sub-type of Charcoal\Source\SourceInterface such as Charcoal\Source\DatabaseSource. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

581
        $db = $this->source()->/** @scrutinizer ignore-call */ db();
Loading history...
582
        if (!$db) {
583
            throw new RuntimeException(
584
                'Could not instanciate a database connection.'
585
            );
586
        }
587
        $this->logger->debug($query);
588
589
        $sth = $db->prepare($query);
590
        $sth->execute();
591
        $res = $sth->fetchColumn(0);
592
593
        return (int)$res;
594
    }
595
596
    /**
597
     * Load list from query.
598
     *
599
     * **Example — Binding values to $query**
600
     *
601
     * ```php
602
     * $this->loadFromQuery([
603
     *     'SELECT name, colour, calories FROM fruit WHERE calories < :calories AND colour = :colour',
604
     *     [
605
     *         'calories' => 150,
606
     *         'colour'   => 'red'
607
     *     ],
608
     *     [ 'calories' => PDO::PARAM_INT ]
609
     * ]);
610
     * ```
611
     *
612
     * @param  string|array  $query    The SQL query as a string or an array composed of the query,
613
     *     parameter binds, and types of parameter bindings.
614
     * @param  callable|null $callback Process each entity after applying raw data.
615
     *    Leave blank to use {@see CollectionLoader::callback()}.
616
     * @param  callable|null $before   Process each entity before applying raw data.
617
     * @throws RuntimeException If the database connection fails.
618
     * @throws InvalidArgumentException If the SQL string/set is invalid.
619
     * @return ModelInterface[]|ArrayAccess
620
     */
621
    public function loadFromQuery($query, callable $callback = null, callable $before = null)
622
    {
623
        $db = $this->source()->db();
624
625
        if (!$db) {
626
            throw new RuntimeException(
627
                'Could not instanciate a database connection.'
628
            );
629
        }
630
631
        /** @todo Filter binds */
632
        if (is_string($query)) {
633
            $this->logger->debug($query);
634
            $sth = $db->prepare($query);
635
            $sth->execute();
636
        } elseif (is_array($query)) {
637
            list($query, $binds, $types) = array_pad($query, 3, []);
638
            $sth = $this->source()->dbQuery($query, $binds, $types);
0 ignored issues
show
Bug introduced by
The method dbQuery() does not exist on Charcoal\Source\SourceInterface. It seems like you code against a sub-type of Charcoal\Source\SourceInterface such as Charcoal\Source\DatabaseSource. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

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