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\Model;
  4: 
  5: use \Closure;
  6: use \Traversable;
  7: use \ArrayIterator;
  8: use \CachingIterator;
  9: use \LogicException;
 10: use \InvalidArgumentException;
 11: 
 12: // Local namespace dependencies
 13: use \Charcoal\Model\CollectionInterface;
 14: use \Charcoal\Model\ModelInterface;
 15: 
 16: /**
 17:  * A Model Collection
 18:  *
 19:  * For iterating instances of {@see ModelInterface}.
 20:  *
 21:  * Used by {@see \Charcoal\Loader\CollectionLoader} for storing results.
 22:  *
 23:  * The collection stores models by {@see \Charcoal\Source\StorableInterface their primary key}.
 24:  * If two objects share the same storable ID but hold disparate data, they are considered
 25:  * to be alike. Adding an object that shares the same ID as an object previously stored in
 26:  * the collection will replace the latter.
 27:  */
 28: class Collection implements CollectionInterface
 29: {
 30:     /**
 31:      * The objects contained in the collection.
 32:      *
 33:      * Stored as a dictionary indexed by each object's primary key.
 34:      * Ensures that each object gets loaded only once by keeping
 35:      * every loaded object in an associative array.
 36:      *
 37:      * @var object[]
 38:      */
 39:     protected $objects = [];
 40: 
 41:     /**
 42:      * Create a new collection.
 43:      *
 44:      * @param  array|Traversable|null $objs Array of objects to pre-populate this collection.
 45:      * @return void
 46:      */
 47:     public function __construct($objs = null)
 48:     {
 49:         if ($objs) {
 50:             $this->merge($objs);
 51:         }
 52:     }
 53: 
 54:     /**
 55:      * Retrieve the first object in the collection.
 56:      *
 57:      * @return object|null Returns the first object, or NULL if the collection is empty.
 58:      */
 59:     public function first()
 60:     {
 61:         if (empty($this->objects)) {
 62:             return null;
 63:         }
 64: 
 65:         return reset($this->objects);
 66:     }
 67: 
 68:     /**
 69:      * Retrieve the last object in the collection.
 70:      *
 71:      * @return object|null Returns the last object, or NULL if the collection is empty.
 72:      */
 73:     public function last()
 74:     {
 75:         if (empty($this->objects)) {
 76:             return null;
 77:         }
 78: 
 79:         return end($this->objects);
 80:     }
 81: 
 82:     // Satisfies CollectionInterface
 83:     // =============================================================================================
 84: 
 85:     /**
 86:      * Merge the collection with the given objects.
 87:      *
 88:      * @param  array|Traversable $objs Array of objects to append to this collection.
 89:      * @throws InvalidArgumentException If the given array contains an unacceptable value.
 90:      * @return self
 91:      */
 92:     public function merge($objs)
 93:     {
 94:         $objs = $this->asArray($objs);
 95: 
 96:         foreach ($objs as $obj) {
 97:             if (!$this->isAcceptable($obj)) {
 98:                 throw new InvalidArgumentException(
 99:                     sprintf(
100:                         'Must be an array of models, contains %s',
101:                         (is_object($obj) ? get_class($obj) : gettype($obj))
102:                     )
103:                 );
104:             }
105: 
106:             $key = $this->modelKey($obj);
107:             $this->objects[$key] = $obj;
108:         }
109: 
110:         return $this;
111:     }
112: 
113:     /**
114:      * Add an object to the collection.
115:      *
116:      * @param  object $obj An acceptable object.
117:      * @throws InvalidArgumentException If the given object is not acceptable.
118:      * @return self
119:      */
120:     public function add($obj)
121:     {
122:         if (!$this->isAcceptable($obj)) {
123:             throw new InvalidArgumentException(
124:                 sprintf(
125:                     'Must be a model, received %s',
126:                     (is_object($obj) ? get_class($obj) : gettype($obj))
127:                 )
128:             );
129:         }
130: 
131:         $key = $this->modelKey($obj);
132:         $this->objects[$key] = $obj;
133: 
134:         return $this;
135:     }
136: 
137:     /**
138:      * Retrieve the object by primary key.
139:      *
140:      * @param  mixed $key The primary key.
141:      * @return object|null Returns the requested object or NULL if not in the collection.
142:      */
143:     public function get($key)
144:     {
145:         if ($this->isAcceptable($key)) {
146:             $key = $this->modelKey($key);
147:         }
148: 
149:         if ($this->has($key)) {
150:             return $this->objects[$key];
151:         }
152: 
153:         return null;
154:     }
155: 
156:     /**
157:      * Determine if an object exists in the collection by key.
158:      *
159:      * @param  mixed $key The primary key to lookup.
160:      * @return boolean
161:      */
162:     public function has($key)
163:     {
164:         if ($this->isAcceptable($key)) {
165:             $key = $this->modelKey($key);
166:         }
167: 
168:         return array_key_exists($key, $this->objects);
169:     }
170: 
171:     /**
172:      * Remove object from collection by primary key.
173:      *
174:      * @param  mixed $key The object primary key to remove.
175:      * @throws InvalidArgumentException If the given key is not acceptable.
176:      * @return self
177:      */
178:     public function remove($key)
179:     {
180:         if ($this->isAcceptable($key)) {
181:             $key = $this->modelKey($key);
182:         }
183: 
184:         unset($this->objects[$key]);
185: 
186:         return $this;
187:     }
188: 
189:     /**
190:      * Remove all objects from collection.
191:      *
192:      * @return self
193:      */
194:     public function clear()
195:     {
196:         $this->objects = [];
197: 
198:         return $this;
199:     }
200: 
201:     /**
202:      * Retrieve all objects in collection indexed by primary keys.
203:      *
204:      * @return object[] An associative array of objects.
205:      */
206:     public function all()
207:     {
208:         return $this->objects;
209:     }
210: 
211:     /**
212:      * Retrieve all objects in the collection indexed numerically.
213:      *
214:      * @return object[] A sequential array of objects.
215:      */
216:     public function values()
217:     {
218:         return array_values($this->objects);
219:     }
220: 
221:     /**
222:      * Retrieve the primary keys of the objects in the collection.
223:      *
224:      * @return array A sequential array of keys.
225:      */
226:     public function keys()
227:     {
228:         return array_keys($this->objects);
229:     }
230: 
231:     // Satisfies ArrayAccess
232:     // =============================================================================================
233: 
234:     /**
235:      * Alias of {@see CollectionInterface::has()}.
236:      *
237:      * @see    \ArrayAccess
238:      * @param  mixed $offset The object primary key or array offset.
239:      * @return boolean
240:      */
241:     public function offsetExists($offset)
242:     {
243:         if (is_int($offset)) {
244:             $offset  = $this->resolveOffset($offset);
245:             $objects = array_values($this->objects);
246: 
247:             return array_key_exists($offset, $objects);
248:         }
249: 
250:         return $this->has($offset);
251:     }
252: 
253:     /**
254:      * Alias of {@see CollectionInterface::get()}.
255:      *
256:      * @see    \ArrayAccess
257:      * @param  mixed $offset The object primary key or array offset.
258:      * @return mixed Returns the requested object or NULL if not in the collection.
259:      */
260:     public function offsetGet($offset)
261:     {
262:         if (is_int($offset)) {
263:             $offset  = $this->resolveOffset($offset);
264:             $objects = array_values($this->objects);
265:             if (isset($objects[$offset])) {
266:                 return $objects[$offset];
267:             }
268:         }
269: 
270:         return $this->get($offset);
271:     }
272: 
273:     /**
274:      * Alias of {@see CollectionInterface::set()}.
275:      *
276:      * @see    \ArrayAccess
277:      * @param  mixed $offset The object primary key or array offset.
278:      * @param  mixed $value  The object.
279:      * @throws LogicException Attempts to assign an offset.
280:      * @return void
281:      */
282:     public function offsetSet($offset, $value)
283:     {
284:         if ($offset === null) {
285:             $this->add($value);
286:         } else {
287:             throw new LogicException(
288:                 sprintf('Offsets are not accepted on the model collection, received %s.', $offset)
289:             );
290:         }
291:     }
292: 
293:     /**
294:      * Alias of {@see CollectionInterface::remove()}.
295:      *
296:      * @see    \ArrayAccess
297:      * @param  mixed $offset The object primary key or array offset.
298:      * @return void
299:      */
300:     public function offsetUnset($offset)
301:     {
302:         if (is_int($offset)) {
303:             $offset = $this->resolveOffset($offset);
304:             $keys   = array_keys($this->objects);
305:             if (isset($keys[$offset])) {
306:                 $offset = $keys[$offset];
307:             }
308:         }
309: 
310:         $this->remove($offset);
311:     }
312: 
313:     /**
314:      * Parse the array offset.
315:      *
316:      * If offset is non-negative, the sequence will start at that offset in the collection.
317:      * If offset is negative, the sequence will start that far from the end of the collection.
318:      *
319:      * @param  integer $offset The array offset.
320:      * @return integer Returns the resolved array offset.
321:      */
322:     protected function resolveOffset($offset)
323:     {
324:         if (is_int($offset)) {
325:             if ($offset < 0) {
326:                 $offset = ($this->count() - abs($offset));
327:             }
328:         }
329: 
330:         return $offset;
331:     }
332: 
333:     // Satisfies Countable
334:     // =============================================================================================
335: 
336:     /**
337:      * Get number of objects in collection
338:      *
339:      * @see    \Countable
340:      * @return integer
341:      */
342:     public function count()
343:     {
344:         return count($this->objects);
345:     }
346: 
347:     // Satisfies IteratorAggregate
348:     // =============================================================================================
349: 
350:     /**
351:      * Retrieve an external iterator.
352:      *
353:      * @see    \IteratorAggregate
354:      * @return \ArrayIterator
355:      */
356:     public function getIterator()
357:     {
358:         return new ArrayIterator($this->objects);
359:     }
360: 
361:     /**
362:      * Retrieve a cached iterator.
363:      *
364:      * @param  integer $flags Bitmask of flags.
365:      * @return \CachingIterator
366:      */
367:     public function getCachingIterator($flags = CachingIterator::CALL_TOSTRING)
368:     {
369:         return new CachingIterator($this->getIterator(), $flags);
370:     }
371: 
372:     // Satisfies backwards-compatibility
373:     // =============================================================================================
374: 
375:     /**
376:      * Retrieve the array offset from the given key.
377:      *
378:      * @deprecated
379:      * @param  mixed $key The primary key to retrieve the offset from.
380:      * @return integer Returns an array offset.
381:      */
382:     public function pos($key)
383:     {
384:         trigger_error('Collection::pos() is deprecated', E_USER_DEPRECATED);
385: 
386:         return array_search($key, array_keys($this->objects));
387:     }
388: 
389:     /**
390:      * Alias of {@see self::values()}
391:      *
392:      * @deprecated
393:      * @todo   Trigger deprecation error.
394:      * @return object[]
395:      */
396:     public function objects()
397:     {
398:         return $this->values();
399:     }
400: 
401:     /**
402:      * Alias of {@see self::all()}.
403:      *
404:      * @deprecated
405:      * @todo   Trigger deprecation error.
406:      * @return object[]
407:      */
408:     public function map()
409:     {
410:         return $this->all();
411:     }
412: 
413:     // =============================================================================================
414: 
415:     /**
416:      * Determine if the given value is acceptable for the collection.
417:      *
418:      * Note: Practical for specialized collections extending the base collection.
419:      *
420:      * @param  mixed $value The value being vetted.
421:      * @return boolean
422:      */
423:     public function isAcceptable($value)
424:     {
425:         return ($value instanceof ModelInterface);
426:     }
427: 
428:     /**
429:      * Convert a given object into a model identifier.
430:      *
431:      * Note: Practical for specialized collections extending the base collection.
432:      *
433:      * @param  object $obj An acceptable object.
434:      * @throws InvalidArgumentException If the given object is not acceptable.
435:      * @return boolean
436:      */
437:     protected function modelKey($obj)
438:     {
439:         if (!$this->isAcceptable($obj)) {
440:             throw new InvalidArgumentException(
441:                 sprintf(
442:                     'Must be a model, received %s',
443:                     (is_object($obj) ? get_class($obj) : gettype($obj))
444:                 )
445:             );
446:         }
447: 
448:         return $obj->id();
449:     }
450: 
451:     /**
452:      * Determine if the collection is empty or not.
453:      *
454:      * @return boolean
455:      */
456:     public function isEmpty()
457:     {
458:         return empty($this->objects);
459:     }
460: 
461:     /**
462:      * Get a base collection instance from this collection.
463:      *
464:      * Note: Practical for extended classes.
465:      *
466:      * @return Collection
467:      */
468:     public function toBase()
469:     {
470:         return new self($this);
471:     }
472: 
473:     /**
474:      * Parse the given value into an array.
475:      *
476:      * @link http://php.net/types.array#language.types.array.casting
477:      *     If an object is converted to an array, the result is an array whose
478:      *     elements are the object's properties.
479:      * @param  mixed $value The value being converted.
480:      * @return array
481:      */
482:     protected function asArray($value)
483:     {
484:         if (is_array($value)) {
485:             return $value;
486:         } elseif ($value instanceof CollectionInterface) {
487:             return $value->all();
488:         } elseif ($value instanceof Traversable) {
489:             return iterator_to_array($value);
490:         } elseif ($value instanceof ModelInterface) {
491:             return [ $value ];
492:         }
493: 
494:         return (array)$value;
495:     }
496: }
497: 
API documentation generated by ApiGen