Completed
Push — master ( f22323...146580 )
by Chauncey
02:50
created

CollectionLoader::modelClass()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 1
b 0
f 0
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\FilterCollectionInterface;
23
use Charcoal\Source\OrderCollectionInterface;
24
use Charcoal\Source\FilterInterface;
25
use Charcoal\Source\OrderInterface;
26
use Charcoal\Source\PaginationInterface;
27
use Charcoal\Source\SourceInterface;
28
29
/**
30
 * Object Collection Loader
31
 */
32
class CollectionLoader implements
33
    FilterCollectionInterface,
34
    OrderCollectionInterface,
35
    LoggerAwareInterface
36
{
37
    use LoggerAwareTrait;
38
39
    /**
40
     * The source to load objects from.
41
     *
42
     * @var SourceInterface
43
     */
44
    private $source;
45
46
    /**
47
     * The model to load the collection from.
48
     *
49
     * @var ModelInterface
50
     */
51
    private $model;
52
53
    /**
54
     * Store the factory instance for the current class.
55
     *
56
     * @var FactoryInterface
57
     */
58
    private $factory;
59
60
    /**
61
     * The callback routine applied to every object added to the collection.
62
     *
63
     * @var callable|null
64
     */
65
    private $callback;
66
67
    /**
68
     * The field which defines the data's model.
69
     *
70
     * @var string|null
71
     */
72
    private $dynamicTypeField;
73
74
    /**
75
     * The class name of the collection to use.
76
     *
77
     * Must be a fully-qualified PHP namespace and an implementation of {@see ArrayAccess}.
78
     *
79
     * @var string
80
     */
81
    private $collectionClass = Collection::class;
82
83
    /**
84
     * Return a new CollectionLoader object.
85
     *
86
     * @param array $data The loader's dependencies.
87
     */
88
    public function __construct(array $data)
89
    {
90
        if (!isset($data['logger'])) {
91
            $data['logger'] = new NullLogger();
92
        }
93
94
        $this->setLogger($data['logger']);
95
96
        if (isset($data['collection'])) {
97
            $this->setCollectionClass($data['collection']);
98
        }
99
100
        if (isset($data['factory'])) {
101
            $this->setFactory($data['factory']);
102
        }
103
104
        if (isset($data['model'])) {
105
            $this->setModel($data['model']);
106
        }
107
    }
108
109
    /**
110
     * Set an object model factory.
111
     *
112
     * @param  FactoryInterface $factory The model factory, to create objects.
113
     * @return self
114
     */
115
    public function setFactory(FactoryInterface $factory)
116
    {
117
        $this->factory = $factory;
118
119
        return $this;
120
    }
121
122
    /**
123
     * Retrieve the object model factory.
124
     *
125
     * @throws RuntimeException If the model factory was not previously set.
126
     * @return FactoryInterface
127
     */
128
    protected function factory()
129
    {
130
        if ($this->factory === null) {
131
            throw new RuntimeException(
132
                sprintf('Model Factory is not defined for "%s"', get_class($this))
133
            );
134
        }
135
136
        return $this->factory;
137
    }
138
139
    /**
140
     * Create a new model.
141
     *
142
     * @return ModelInterface
143
     */
144
    public function createModel()
145
    {
146
        $obj = $this->factory()->create($this->modelClass());
147
        return $obj;
148
    }
149
150
    /**
151
     * Create a new model from a dataset.
152
     *
153
     * @param  array $data The model data.
154
     * @return ModelInterface
155
     */
156
    protected function createModelFromData(array $data)
157
    {
158
        $obj = $this->factory()->create($this->dynamicModelClass($data));
159
        return $obj;
160
    }
161
162
    /**
163
     * Set the loader settings.
164
     *
165
     * @param  array $data Data to assign to the loader.
166
     * @return self
167
     */
168
    public function setData(array $data)
169
    {
170
        foreach ($data as $key => $val) {
171
            $setter = $this->setter($key);
172
            if (is_callable([ $this, $setter ])) {
173
                $this->{$setter}($val);
174
            } else {
175
                $this->{$key} = $val;
176
            }
177
        }
178
179
        return $this;
180
    }
181
182
    /**
183
     * Retrieve the source to load objects from.
184
     *
185
     * @throws RuntimeException If no source has been defined.
186
     * @return SourceInterface
187
     */
188
    public function source()
189
    {
190
        if ($this->source === null) {
191
            throw new RuntimeException('No source set.');
192
        }
193
194
        return $this->source;
195
    }
196
197
    /**
198
     * Set the source to load objects from.
199
     *
200
     * @param  SourceInterface $source A data source.
201
     * @return self
202
     */
203
    public function setSource(SourceInterface $source)
204
    {
205
        $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

205
        $source->/** @scrutinizer ignore-call */ 
206
                 reset();
Loading history...
206
207
        $this->source = $source;
208
209
        return $this;
210
    }
211
212
    /**
213
     * Reset everything but the model.
214
     *
215
     * @return self
216
     */
217
    public function reset()
218
    {
219
        if ($this->source) {
220
            $this->source()->reset();
221
        }
222
223
        $this->callback = null;
224
        $this->dynamicTypeField = null;
225
226
        return $this;
227
    }
228
229
    /**
230
     * Retrieve the object model.
231
     *
232
     * @throws RuntimeException If no model has been defined.
233
     * @return ModelInterface
234
     */
235
    public function model()
236
    {
237
        if ($this->model === null) {
238
            throw new RuntimeException('The collection loader must have a model.');
239
        }
240
241
        return $this->model;
242
    }
243
244
    /**
245
     * Determine if the loader has an object model.
246
     *
247
     * @return boolean
248
     */
249
    public function hasModel()
250
    {
251
        return !!$this->model;
252
    }
253
254
    /**
255
     * Set the model to use for the loaded objects.
256
     *
257
     * @param  string|ModelInterface $model An object model.
258
     * @throws InvalidArgumentException If the given argument is not a model.
259
     * @return self
260
     */
261
    public function setModel($model)
262
    {
263
        if (is_string($model)) {
264
            $model = $this->factory()->get($model);
265
        }
266
267
        if (!$model instanceof ModelInterface) {
268
            throw new InvalidArgumentException(
269
                sprintf(
270
                    'The model must be an instance of "%s"',
271
                    ModelInterface::class
272
                )
273
            );
274
        }
275
276
        $this->model = $model;
277
278
        $this->setSource($model->source());
279
280
        return $this;
281
    }
282
283
    /**
284
     * Retrieve the model class.
285
     *
286
     * @return string
287
     */
288
    public function modelClass()
289
    {
290
        return get_class($this->model());
291
    }
292
293
    /**
294
     * Retrieve the model class.
295
     *
296
     * @param  array $data The model data.
297
     * @return string
298
     */
299
    protected function dynamicModelClass(array $data)
300
    {
301
        $field = $this->dynamicTypeField();
302
        if ($field && isset($data[$field])) {
303
            return $data[$field];
304
        }
305
306
        return $this->modelClass();
307
    }
308
309
    /**
310
     * @return string|null
311
     */
312
    public function dynamicTypeField()
313
    {
314
        return $this->dynamicTypeField;
315
    }
316
317
    /**
318
     * Determine if the model has a dynamic object type.
319
     *
320
     * @return boolean
321
     */
322
    public function hasDynamicTypeField()
323
    {
324
        return !!$this->dynamicTypeField;
325
    }
326
327
    /**
328
     * @param  string $field The field to use for dynamic object type.
329
     * @throws InvalidArgumentException If the field is not a string.
330
     * @return self
331
     */
332
    public function setDynamicTypeField($field)
333
    {
334
        if (!is_string($field)) {
0 ignored issues
show
introduced by
The condition is_string($field) is always true.
Loading history...
335
            throw new InvalidArgumentException(
336
                'Dynamic type field must be a string'
337
            );
338
        }
339
340
        $this->dynamicTypeField = $field;
341
342
        return $this;
343
    }
344
345
    /**
346
     * Alias of {@see SourceInterface::properties()}
347
     *
348
     * @return array
349
     */
350
    public function properties()
351
    {
352
        return $this->source()->properties();
353
    }
354
355
    /**
356
     * Alias of {@see SourceInterface::setProperties()}
357
     *
358
     * @param  array $properties An array of property identifiers.
359
     * @return self
360
     */
361
    public function setProperties(array $properties)
362
    {
363
        $this->source()->setProperties($properties);
364
365
        return $this;
366
    }
367
368
    /**
369
     * Alias of {@see SourceInterface::addProperty()}
370
     *
371
     * @param  string $property A property identifier.
372
     * @return self
373
     */
374
    public function addProperty($property)
375
    {
376
        $this->source()->addProperty($property);
377
378
        return $this;
379
    }
380
381
    /**
382
     * Set "search" keywords to filter multiple properties.
383
     *
384
     * @param  array $keywords An array of keywords and properties.
385
     *     Expected format: `[ "search query", [ "field names…" ] ]`.
386
     * @return self
387
     */
388
    public function setKeywords(array $keywords)
389
    {
390
        foreach ($keywords as $query) {
391
            $keyword    = $query[0];
392
            $properties = (isset($query[1]) ? (array)$query[1] : null);
393
            $this->addKeyword($keyword, $properties);
394
        }
395
396
        return $this;
397
    }
398
399
    /**
400
     * Add a "search" keyword filter to multiple properties.
401
     *
402
     * @param  string $keyword    A value to match among $properties.
403
     * @param  array  $properties One or more of properties to search amongst.
404
     * @return self
405
     */
406
    public function addKeyword($keyword, array $properties = null)
407
    {
408
        if ($properties === null) {
409
            $properties = [];
410
        }
411
412
        foreach ($properties as $propertyIdent) {
413
            $val = ('%'.$keyword.'%');
414
            $this->addFilter([
415
                'property' => $propertyIdent,
416
                'operator' => 'LIKE',
417
                'value'    => $val,
418
                'operand'  => 'OR'
419
            ]);
420
        }
421
422
        return $this;
423
    }
424
425
    /**
426
     * Alias of {@see SourceInterface::filters()}
427
     *
428
     * @return FilterInterface[]
429
     */
430
    public function filters()
431
    {
432
        return $this->source()->filters();
433
    }
434
435
    /**
436
     * Alias of {@see SourceInterface::hasFilters()}
437
     *
438
     * @return boolean
439
     */
440
    public function hasFilters()
441
    {
442
        return $this->source()->hasFilters();
443
    }
444
445
    /**
446
     * Alias of {@see SourceInterface::setFilters()}
447
     *
448
     * @param  array $filters An array of filters.
449
     * @return self
450
     */
451
    public function setFilters(array $filters)
452
    {
453
        $this->source()->setFilters($filters);
454
        return $this;
455
    }
456
457
    /**
458
     * Alias of {@see SourceInterface::addFilters()}
459
     *
460
     * @param  array $filters An array of filters.
461
     * @return self
462
     */
463
    public function addFilters(array $filters)
464
    {
465
        foreach ($filters as $f) {
466
            $this->addFilter($f);
467
        }
468
        return $this;
469
    }
470
471
    /**
472
     * Alias of {@see SourceInterface::addFilter()}
473
     *
474
     * @param  mixed $param   The property to filter by,
475
     *     a {@see FilterInterface} object,
476
     *     or a filter array structure.
477
     * @param  mixed $value   Optional value for the property to compare against.
478
     *     Only used if the first argument is a string.
479
     * @param  array $options Optional extra settings to apply on the filter.
480
     * @return self
481
     */
482
    public function addFilter($param, $value = null, array $options = null)
483
    {
484
        $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

484
        $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...
485
        return $this;
486
    }
487
488
    /**
489
     * Alias of {@see SourceInterface::orders()}
490
     *
491
     * @return OrderInterface[]
492
     */
493
    public function orders()
494
    {
495
        return $this->source()->orders();
496
    }
497
498
    /**
499
     * Alias of {@see SourceInterface::hasOrders()}
500
     *
501
     * @return boolean
502
     */
503
    public function hasOrders()
504
    {
505
        return $this->source()->hasOrders();
506
    }
507
508
    /**
509
     * Alias of {@see SourceInterface::setOrders()}
510
     *
511
     * @param  array $orders An array of orders.
512
     * @return self
513
     */
514
    public function setOrders(array $orders)
515
    {
516
        $this->source()->setOrders($orders);
517
        return $this;
518
    }
519
520
    /**
521
     * Alias of {@see SourceInterface::addOrders()}
522
     *
523
     * @param  array $orders An array of orders.
524
     * @return self
525
     */
526
    public function addOrders(array $orders)
527
    {
528
        foreach ($orders as $o) {
529
            $this->addOrder($o);
530
        }
531
        return $this;
532
    }
533
534
    /**
535
     * Alias of {@see SourceInterface::addOrder()}
536
     *
537
     * @param  mixed  $param   The property to sort by,
538
     *     a {@see OrderInterface} object,
539
     *     or a order array structure.
540
     * @param  string $mode    Optional sorting mode.
541
     *     Defaults to ascending if a property is provided.
542
     * @param  array  $options Optional extra settings to apply on the order.
543
     * @return self
544
     */
545
    public function addOrder($param, $mode = 'asc', array $options = null)
546
    {
547
        $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

547
        $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...
548
        return $this;
549
    }
550
551
    /**
552
     * Alias of {@see SourceInterface::pagination()}
553
     *
554
     * @return PaginationInterface
555
     */
556
    public function pagination()
557
    {
558
        return $this->source()->pagination();
559
    }
560
561
    /**
562
     * Alias of {@see SourceInterface::setPagination()}
563
     *
564
     * @param  mixed $param An associative array of pagination settings.
565
     * @return self
566
     */
567
    public function setPagination($param)
568
    {
569
        $this->source()->setPagination($param);
570
571
        return $this;
572
    }
573
574
    /**
575
     * Alias of {@see PaginationInterface::page()}
576
     *
577
     * @return integer
578
     */
579
    public function page()
580
    {
581
        return $this->pagination()->page();
582
    }
583
584
    /**
585
     * Alias of {@see PaginationInterface::pagination()}
586
     *
587
     * @param  integer $page A page number.
588
     * @return self
589
     */
590
    public function setPage($page)
591
    {
592
        $this->pagination()->setPage($page);
593
594
        return $this;
595
    }
596
597
    /**
598
     * Alias of {@see PaginationInterface::numPerPage()}
599
     *
600
     * @return integer
601
     */
602
    public function numPerPage()
603
    {
604
        return $this->pagination()->numPerPage();
605
    }
606
607
    /**
608
     * Alias of {@see PaginationInterface::setNumPerPage()}
609
     *
610
     * @param  integer $num The number of items to display per page.
611
     * @return self
612
     */
613
    public function setNumPerPage($num)
614
    {
615
        $this->pagination()->setNumPerPage($num);
616
617
        return $this;
618
    }
619
620
    /**
621
     * Set the callback routine applied to every object added to the collection.
622
     *
623
     * @param  callable $callback The callback routine.
624
     * @return self
625
     */
626
    public function setCallback(callable $callback)
627
    {
628
        $this->callback = $callback;
629
630
        return $this;
631
    }
632
633
    /**
634
     * Retrieve the callback routine applied to every object added to the collection.
635
     *
636
     * @return callable|null
637
     */
638
    public function callback()
639
    {
640
        return $this->callback;
641
    }
642
643
    /**
644
     * Load a collection from source.
645
     *
646
     * @param  string|null   $ident    Optional. A pre-defined list to use from the model.
647
     * @param  callable|null $callback Process each entity after applying raw data.
648
     *    Leave blank to use {@see CollectionLoader::callback()}.
649
     * @param  callable|null $before   Process each entity before applying raw data.
650
     * @throws Exception If the database connection fails.
651
     * @return ModelInterface[]|ArrayAccess
652
     */
653
    public function load($ident = null, callable $callback = null, callable $before = null)
654
    {
655
        // Unused.
656
        unset($ident);
657
658
        $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

658
        $query = $this->source()->/** @scrutinizer ignore-call */ sqlLoad();
Loading history...
659
660
        return $this->loadFromQuery($query, $callback, $before);
661
    }
662
663
    /**
664
     * Get the total number of items for this collection query.
665
     *
666
     * @throws RuntimeException If the database connection fails.
667
     * @return integer
668
     */
669
    public function loadCount()
670
    {
671
        $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

671
        $query = $this->source()->/** @scrutinizer ignore-call */ sqlLoadCount();
Loading history...
672
673
        $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

673
        $db = $this->source()->/** @scrutinizer ignore-call */ db();
Loading history...
674
        if (!$db) {
675
            throw new RuntimeException(
676
                'Could not instanciate a database connection.'
677
            );
678
        }
679
        $this->logger->debug($query);
680
681
        $sth = $db->prepare($query);
682
        $sth->execute();
683
        $res = $sth->fetchColumn(0);
684
685
        return (int)$res;
686
    }
687
688
    /**
689
     * Load list from query.
690
     *
691
     * **Example — Binding values to $query**
692
     *
693
     * ```php
694
     * $this->loadFromQuery([
695
     *     'SELECT name, colour, calories FROM fruit WHERE calories < :calories AND colour = :colour',
696
     *     [
697
     *         'calories' => 150,
698
     *         'colour'   => 'red'
699
     *     ],
700
     *     [ 'calories' => PDO::PARAM_INT ]
701
     * ]);
702
     * ```
703
     *
704
     * @param  string|array  $query    The SQL query as a string or an array composed of the query,
705
     *     parameter binds, and types of parameter bindings.
706
     * @param  callable|null $callback Process each entity after applying raw data.
707
     *    Leave blank to use {@see CollectionLoader::callback()}.
708
     * @param  callable|null $before   Process each entity before applying raw data.
709
     * @throws RuntimeException If the database connection fails.
710
     * @throws InvalidArgumentException If the SQL string/set is invalid.
711
     * @return ModelInterface[]|ArrayAccess
712
     */
713
    public function loadFromQuery($query, callable $callback = null, callable $before = null)
714
    {
715
        $db = $this->source()->db();
716
717
        if (!$db) {
718
            throw new RuntimeException(
719
                'Could not instanciate a database connection.'
720
            );
721
        }
722
723
        /** @todo Filter binds */
724
        if (is_string($query)) {
725
            $this->logger->debug($query);
726
            $sth = $db->prepare($query);
727
            $sth->execute();
728
        } elseif (is_array($query)) {
0 ignored issues
show
introduced by
The condition is_array($query) is always true.
Loading history...
729
            list($query, $binds, $types) = array_pad($query, 3, []);
730
            $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

730
            $sth = $this->source()->/** @scrutinizer ignore-call */ dbQuery($query, $binds, $types);
Loading history...
731
        } else {
732
            throw new InvalidArgumentException(sprintf(
733
                'The SQL query must be a string or an array: '.
734
                '[ string $query, array $binds, array $dataTypes ]; '.
735
                'received %s',
736
                is_object($query) ? get_class($query) : $query
737
            ));
738
        }
739
740
        $sth->setFetchMode(PDO::FETCH_ASSOC);
741
742
        if ($callback === null) {
743
            $callback = $this->callback();
744
        }
745
746
        return $this->processCollection($sth, $before, $callback);
747
    }
748
749
    /**
750
     * Process the collection of raw data.
751
     *
752
     * @param  mixed[]|Traversable $results The raw result set.
753
     * @param  callable|null       $before  Process each entity before applying raw data.
754
     * @param  callable|null       $after   Process each entity after applying raw data.
755
     * @return ModelInterface[]|ArrayAccess
756
     */
757
    protected function processCollection($results, callable $before = null, callable $after = null)
758
    {
759
        $collection = $this->createCollection();
760
        foreach ($results as $objData) {
761
            $obj = $this->processModel($objData, $before, $after);
762
763
            if ($obj instanceof ModelInterface) {
764
                $collection[] = $obj;
765
            }
766
        }
767
768
        return $collection;
769
    }
770
771
    /**
772
     * Process the raw data for one model.
773
     *
774
     * @param  mixed         $objData The raw dataset.
775
     * @param  callable|null $before  Process each entity before applying raw data.
776
     * @param  callable|null $after   Process each entity after applying raw data.
777
     * @return ModelInterface|ArrayAccess|null
778
     */
779
    protected function processModel($objData, callable $before = null, callable $after = null)
780
    {
781
        $obj = $this->createModelFromData($objData);
782
783
        if ($before !== null) {
784
            call_user_func_array($before, [ &$obj ]);
785
        }
786
787
        $obj->setFlatData($objData);
788
789
        if ($after !== null) {
790
            call_user_func_array($after, [ &$obj ]);
791
        }
792
793
        return $obj;
794
    }
795
796
    /**
797
     * Create a collection class or array.
798
     *
799
     * @throws RuntimeException If the collection class is invalid.
800
     * @return array|ArrayAccess
801
     */
802
    public function createCollection()
803
    {
804
        $collectClass = $this->collectionClass();
805
        if ($collectClass === 'array') {
806
            return [];
807
        }
808
809
        if (!class_exists($collectClass)) {
810
            throw new RuntimeException(sprintf(
811
                'Collection class [%s] does not exist.',
812
                $collectClass
813
            ));
814
        }
815
816
        if (!is_subclass_of($collectClass, ArrayAccess::class)) {
817
            throw new RuntimeException(sprintf(
818
                'Collection class [%s] must implement ArrayAccess.',
819
                $collectClass
820
            ));
821
        }
822
823
        $collection = new $collectClass;
824
825
        return $collection;
826
    }
827
828
    /**
829
     * Set the class name of the collection.
830
     *
831
     * @param  string $className The class name of the collection.
832
     * @throws InvalidArgumentException If the class name is not a string.
833
     * @return self
834
     */
835
    public function setCollectionClass($className)
836
    {
837
        if (!is_string($className)) {
0 ignored issues
show
introduced by
The condition is_string($className) is always true.
Loading history...
838
            throw new InvalidArgumentException(
839
                'Collection class name must be a string.'
840
            );
841
        }
842
843
        $this->collectionClass = $className;
844
845
        return $this;
846
    }
847
848
    /**
849
     * Retrieve the class name of the collection.
850
     *
851
     * @return string
852
     */
853
    public function collectionClass()
854
    {
855
        return $this->collectionClass;
856
    }
857
858
    /**
859
     * Allow an object to define how the key getter are called.
860
     *
861
     * @param  string $key The key to get the getter from.
862
     * @return string The getter method name, for a given key.
863
     */
864
    protected function getter($key)
865
    {
866
        $getter = $key;
867
        return $this->camelize($getter);
868
    }
869
870
    /**
871
     * Allow an object to define how the key setter are called.
872
     *
873
     * @param  string $key The key to get the setter from.
874
     * @return string The setter method name, for a given key.
875
     */
876
    protected function setter($key)
877
    {
878
        $setter = 'set_'.$key;
879
        return $this->camelize($setter);
880
    }
881
882
    /**
883
     * Transform a snake_case string to camelCase.
884
     *
885
     * @param  string $str The snake_case string to camelize.
886
     * @return string The camelcase'd string.
887
     */
888
    protected function camelize($str)
889
    {
890
        return lcfirst(implode('', array_map('ucfirst', explode('_', $str))));
891
    }
892
}
893