Overview

Namespaces

  • Charcoal
    • Loader
    • Model
      • Service
      • ServiceProvider
    • Source
      • Database
    • Validator

Classes

  • Charcoal\Loader\CollectionLoader
  • Charcoal\Loader\FileLoader
  • Charcoal\Model\AbstractMetadata
  • Charcoal\Model\AbstractModel
  • Charcoal\Model\Collection
  • Charcoal\Model\Model
  • Charcoal\Model\ModelMetadata
  • Charcoal\Model\ModelValidator
  • Charcoal\Model\Service\MetadataLoader
  • Charcoal\Model\Service\ModelBuilder
  • Charcoal\Model\Service\ModelLoader
  • Charcoal\Model\Service\ModelLoaderBuilder
  • Charcoal\Model\ServiceProvider\ModelServiceProvider
  • Charcoal\Source\AbstractSource
  • Charcoal\Source\Database\DatabaseFilter
  • Charcoal\Source\Database\DatabaseOrder
  • Charcoal\Source\Database\DatabasePagination
  • Charcoal\Source\DatabaseSource
  • Charcoal\Source\DatabaseSourceConfig
  • Charcoal\Source\Filter
  • Charcoal\Source\Order
  • Charcoal\Source\Pagination
  • Charcoal\Source\SourceConfig
  • Charcoal\Validator\AbstractValidator
  • Charcoal\Validator\ValidatorResult

Interfaces

  • Charcoal\Model\CollectionInterface
  • Charcoal\Model\DescribableInterface
  • Charcoal\Model\MetadataInterface
  • Charcoal\Model\ModelInterface
  • Charcoal\Source\DatabaseSourceInterface
  • Charcoal\Source\FilterInterface
  • Charcoal\Source\OrderInterface
  • Charcoal\Source\PaginationInterface
  • Charcoal\Source\SourceInterface
  • Charcoal\Source\StorableInterface
  • Charcoal\Validator\ValidatableInterface
  • Charcoal\Validator\ValidatorInterface

Traits

  • Charcoal\Model\DescribableTrait
  • Charcoal\Source\StorableTrait
  • Charcoal\Validator\ValidatableTrait
  • Overview
  • Namespace
  • Class
  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    $callback Optional. Apply a callback to every entity of the collection.
523:      *    Leave blank to use {@see CollectionLoader::callback()}.
524:      * @throws Exception If the database connection fails.
525:      * @return array|ArrayAccess
526:      */
527:     public function load($ident = null, callable $callback = null)
528:     {
529:         // Unused.
530:         unset($ident);
531: 
532:         $query = $this->source()->sqlLoad();
533: 
534:         return $this->loadFromQuery($query, $callback);
535:     }
536: 
537:     /**
538:      * Get the total number of items for this collection query.
539:      *
540:      * @throws RuntimeException If the database connection fails.
541:      * @return integer
542:      */
543:     public function loadCount()
544:     {
545:         $query = $this->source()->sqlLoadCount();
546: 
547:         $db = $this->source()->db();
548:         if (!$db) {
549:             throw new RuntimeException(
550:                 'Could not instanciate a database connection.'
551:             );
552:         }
553:         $this->logger->debug($query);
554: 
555:         $sth = $db->prepare($query);
556:         $sth->execute();
557:         $res = $sth->fetchColumn(0);
558: 
559:         return (int)$res;
560:     }
561: 
562:     /**
563:      * Load list from query.
564:      *
565:      * **Example — Binding values to $query**
566:      *
567:      * ```php
568:      * $this->loadFromQuery([
569:      *     'SELECT name, colour, calories FROM fruit WHERE calories < :calories AND colour = :colour',
570:      *     [
571:      *         'calories' => 150,
572:      *         'colour'   => 'red'
573:      *     ],
574:      *     [ 'calories' => PDO::PARAM_INT ]
575:      * ]);
576:      * ```
577:      *
578:      * @param  string|array $query    The SQL query as a string or an array composed of the query,
579:      *     parameter binds, and types of parameter bindings.
580:      * @param  callable     $callback Optional. Apply a callback to every entity of the collection.
581:      *    Leave blank to use {@see CollectionLoader::callback()}.
582:      * @throws RuntimeException If the database connection fails.
583:      * @throws InvalidArgumentException If the SQL string/set is invalid.
584:      * @return array|ArrayAccess
585:      */
586:     public function loadFromQuery($query, callable $callback = null)
587:     {
588:         $db = $this->source()->db();
589: 
590:         if (!$db) {
591:             throw new RuntimeException(
592:                 'Could not instanciate a database connection.'
593:             );
594:         }
595: 
596:         /** @todo Filter binds */
597:         if (is_string($query)) {
598:             $this->logger->debug($query);
599:             $sth = $db->prepare($query);
600:             $sth->execute();
601:         } elseif (is_array($query)) {
602:             list($query, $binds, $types) = array_pad($query, 3, []);
603:             $sth = $this->source()->dbQuery($query, $binds, $types);
604:         } else {
605:             throw new InvalidArgumentException(sprintf(
606:                 'The SQL query must be a string or an array: '.
607:                 '[ string $query, array $binds, array $dataTypes ]; '.
608:                 'received %s',
609:                 is_object($query) ? get_class($query) : $query
610:             ));
611:         }
612: 
613:         $sth->setFetchMode(PDO::FETCH_ASSOC);
614: 
615:         return $this->processCollection($sth, $callback);
616:     }
617: 
618:     /**
619:      * Process the collection of raw data.
620:      *
621:      * @param  array|Traversable $results  The raw result set.
622:      * @param  callable          $callback Optional. Apply a callback to every entity of the collection.
623:      *    Leave blank to use {@see CollectionLoader::callback()}.
624:      * @throws InvalidArgumentException If the SQL string/set is invalid.
625:      * @return array|ArrayAccess
626:      */
627:     protected function processCollection($results, callable $callback = null)
628:     {
629:         if ($callback === null) {
630:             $callback = $this->callback();
631:         }
632: 
633:         $modelObjType = $this->model()->objType();
634:         $collection   = $this->createCollection();
635:         foreach ($results as $objData) {
636:             if ($this->dynamicTypeField && isset($objData[$this->dynamicTypeField])) {
637:                 $objType = $objData[$this->dynamicTypeField];
638:             } else {
639:                 $objType = $modelObjType;
640:             }
641: 
642: 
643:             $obj = $this->factory()->create($objType);
644:             $obj->setFlatData($objData);
645: 
646:             if (isset($callback)) {
647:                 call_user_func_array($callback, [ &$obj ]);
648:             }
649: 
650:             if ($obj instanceof ModelInterface) {
651:                 $collection[] = $obj;
652:             }
653:         }
654: 
655:         return $collection;
656:     }
657: 
658:     /**
659:      * Create a collection class or array.
660:      *
661:      * @throws RuntimeException If the collection class is invalid.
662:      * @return array|ArrayAccess
663:      */
664:     public function createCollection()
665:     {
666:         $collectClass = $this->collectionClass();
667:         if ($collectClass === 'array') {
668:             return [];
669:         }
670: 
671:         if (!class_exists($collectClass)) {
672:             throw new RuntimeException(sprintf(
673:                 'Collection class [%s] does not exist.',
674:                 $collectClass
675:             ));
676:         }
677: 
678:         if (!is_subclass_of($collectClass, ArrayAccess::class)) {
679:             throw new RuntimeException(sprintf(
680:                 'Collection class [%s] must implement ArrayAccess.',
681:                 $collectClass
682:             ));
683:         }
684: 
685:         $collection = new $collectClass;
686: 
687:         return $collection;
688:     }
689: 
690:     /**
691:      * Set the class name of the collection.
692:      *
693:      * @param  string $className The class name of the collection.
694:      * @throws InvalidArgumentException If the class name is not a string.
695:      * @return AbstractPropertyDisplay Chainable
696:      */
697:     public function setCollectionClass($className)
698:     {
699:         if (!is_string($className)) {
700:             throw new InvalidArgumentException(
701:                 'Collection class name must be a string.'
702:             );
703:         }
704: 
705:         $this->collectionClass = $className;
706: 
707:         return $this;
708:     }
709: 
710:     /**
711:      * Retrieve the class name of the collection.
712:      *
713:      * @return string
714:      */
715:     public function collectionClass()
716:     {
717:         return $this->collectionClass;
718:     }
719: 
720:     /**
721:      * Allow an object to define how the key getter are called.
722:      *
723:      * @param string $key The key to get the getter from.
724:      * @return string The getter method name, for a given key.
725:      */
726:     protected function getter($key)
727:     {
728:         $getter = $key;
729:         return $this->camelize($getter);
730:     }
731: 
732:     /**
733:      * Allow an object to define how the key setter are called.
734:      *
735:      * @param string $key The key to get the setter from.
736:      * @return string The setter method name, for a given key.
737:      */
738:     protected function setter($key)
739:     {
740:         $setter = 'set_'.$key;
741:         return $this->camelize($setter);
742:     }
743: 
744:     /**
745:      * Transform a snake_case string to camelCase.
746:      *
747:      * @param string $str The snake_case string to camelize.
748:      * @return string The camelcase'd string.
749:      */
750:     protected function camelize($str)
751:     {
752:         return lcfirst(implode('', array_map('ucfirst', explode('_', $str))));
753:     }
754: }
755: 
API documentation generated by ApiGen