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 \DateTimeInterface;
  6: use \Exception;
  7: use \InvalidArgumentException;
  8: 
  9: use \PDO;
 10: 
 11: // Dependencies from PSR-3 (Logger)
 12: use \Psr\Log\LoggerAwareInterface;
 13: use \Psr\Log\LoggerAwareTrait;
 14: use \Psr\Log\NullLogger;
 15: 
 16: use \Pimple\Container;
 17: 
 18: // Dependency from 'charcoal-config'
 19: use \Charcoal\Config\AbstractEntity;
 20: 
 21: // Dependencies from 'charcoal-view'
 22: use \Charcoal\View\GenericView;
 23: use \Charcoal\View\ViewableInterface;
 24: use \Charcoal\View\ViewableTrait;
 25: 
 26: // Dependencies from 'charcoal-property'
 27: use \Charcoal\Property\DescribablePropertyInterface;
 28: use \Charcoal\Property\DescribablePropertyTrait;
 29: 
 30: // Intra-module ('charcoal-core') dependencies
 31: use \Charcoal\Model\DescribableInterface;
 32: use \Charcoal\Model\DescribableTrait;
 33: use \Charcoal\Source\SourceFactory;
 34: use \Charcoal\Source\StorableInterface;
 35: use \Charcoal\Source\StorableTrait;
 36: use \Charcoal\Validator\ValidatableInterface;
 37: use \Charcoal\Validator\ValidatableTrait;
 38: 
 39: // Local namespace dependencies
 40: use \Charcoal\Model\ModelInterface;
 41: use \Charcoal\Model\ModelMetadata;
 42: use \Charcoal\Model\ModelValidator;
 43: 
 44: /**
 45:  * An abstract class that implements most of `ModelInterface`.
 46:  *
 47:  * In addition to `ModelInterface`, the abstract model implements the following interfaces:
 48:  *
 49:  * - `DescribableInterface`
 50:  * - `StorableInterface
 51:  * - `ValidatableInterface`
 52:  * - `ViewableInterface`.
 53:  *
 54:  * Those interfaces
 55:  * are implemented (in parts, at least) with the `DescribableTrait`, `StorableTrait`,
 56:  * `ValidatableTrait` and the `ViewableTrait`.
 57:  *
 58:  * The `JsonSerializable` interface is fully provided by the `DescribableTrait`.
 59:  */
 60: abstract class AbstractModel extends AbstractEntity implements
 61:     ModelInterface,
 62:     DescribableInterface,
 63:     DescribablePropertyInterface,
 64:     LoggerAwareInterface,
 65:     StorableInterface,
 66:     ValidatableInterface,
 67:     ViewableInterface
 68: {
 69:     use LoggerAwareTrait;
 70:     use DescribableTrait;
 71:     use DescribablePropertyTrait;
 72:     use StorableTrait;
 73:     use ValidatableTrait;
 74:     use ViewableTrait;
 75: 
 76:     const DEFAULT_SOURCE_TYPE = 'database';
 77: 
 78:     /**
 79:      * @param array $data Dependencies.
 80:      */
 81:     public function __construct(array $data = null)
 82:     {
 83:         // LoggerAwareInterface dependencies
 84:         $this->setLogger($data['logger']);
 85: 
 86:         // Optional DescribableInterface dependencies
 87:         if (isset($data['property_factory'])) {
 88:             $this->setPropertyFactory($data['property_factory']);
 89:         }
 90:         if (isset($data['metadata'])) {
 91:             $this->setMetadata($data['metadata']);
 92:         }
 93:         if (isset($data['metadata_loader'])) {
 94:             $this->setMetadataLoader($data['metadata_loader']);
 95:         }
 96: 
 97:         // Optional StorableInterface dependencies
 98:         if (isset($data['source'])) {
 99:              $this->setSource($data['source']);
100:         }
101:         if (isset($data['source_factory'])) {
102:             $this->setSourceFactory($data['source_factory']);
103:         }
104: 
105:         // Optional ViewableInterface dependencies
106:         if (isset($data['view'])) {
107:             $this->setView($data['view']);
108:         }
109: 
110:         if (isset($data['container'])) {
111:             $this->setDependencies($data['container']);
112:         }
113:     }
114: 
115:     /**
116:      * Inject dependencies from a DI Container.
117:      *
118:      * @param Container $container A dependencies container instance.
119:      * @return void
120:      */
121:     public function setDependencies(Container $container)
122:     {
123:         // This method is a stub. Reimplement in children method
124:     }
125: 
126:     /**
127:      * Set the object's ID from an associative array map (or any other Traversable).
128:      *
129:      * Useful for setting the object ID before the rest of the object's data.
130:      *
131:      * @param array|\Traversable $data The object data.
132:      * @return array|\Traversable The object data without the pre-set ID.
133:      */
134:     protected function setIdFromData($data)
135:     {
136:         $key = $this->key();
137:         if (isset($data[$key])) {
138:             $this->setId($data[$key]);
139:             unset($data[$key]);
140:         }
141: 
142:         return $data;
143:     }
144: 
145:     /**
146:      * Sets the object data, from an associative array map (or any other Traversable).
147:      *
148:      * @param array $data The entity data. Will call setters.
149:      * @return AbstractModel Chainable
150:      */
151:     public function setData(array $data)
152:     {
153:         $data = $this->setIdFromData($data);
154: 
155:         return parent::setData($data);
156:     }
157: 
158:     /**
159:      * Return the object data as an array.
160:      *
161:      * @param array $propertyFilters Optional. Property filter.
162:      * @return array
163:      */
164:     public function data(array $propertyFilters = null)
165:     {
166:         $data = [];
167:         $properties = $this->properties($propertyFilters);
168:         foreach ($properties as $propertyIdent => $property) {
169:             // Ensure objects are properly encoded.
170:             $v = $this->propertyValue($propertyIdent);
171:             $v = $this->serializedValue($v);
172:             $data[$propertyIdent] = $v;
173:         }
174:         return $data;
175:     }
176: 
177:     /**
178:      * @param mixed $val The value to serialize.
179:      * @return mixed
180:      */
181:     private function serializedValue($val)
182:     {
183:         if (is_scalar($val)) {
184:             return $val;
185:         } elseif ($val instanceof DateTimeInterface) {
186:             return $val->format('Y-m-d H:i:s');
187:         } else {
188:             return json_decode(json_encode($val), true);
189:         }
190:     }
191: 
192:     /**
193:      * Override's `\Charcoal\Config\AbstractEntity`'s `setData` method to take properties into consideration.
194:      *
195:      * Also add a special case, to merge values for l10n properties.
196:      *
197:      * @param array|\Traversable $data The data to merge.
198:      * @return EntityInterface Chainable
199:      * @see \Charcoal\Config\AbstractEntity::offsetSet()
200:      */
201:     public function mergeData($data)
202:     {
203:         $data = $this->setIdFromData($data);
204: 
205:         foreach ($data as $propIdent => $val) {
206:             if (!$this->hasProperty($propIdent)) {
207:                 $this->logger->warning(
208:                     sprintf('Cannot set property "%s" on object; not defined in metadata.', $propIdent)
209:                 );
210:                 continue;
211:             }
212:             $property = $this->p($propIdent);
213:             if ($property->l10n() && is_array($val)) {
214:                 $currentValue = json_decode(json_encode($this[$propIdent]), true);
215:                 if (is_array($currentValue)) {
216:                     $this[$propIdent] = array_merge($currentValue, $val);
217:                 } else {
218:                     $this[$propIdent] = $val;
219:                 }
220:             } else {
221:                 $this[$propIdent] = $val;
222:             }
223:         }
224: 
225:         return $this;
226:     }
227: 
228:     /**
229:      * @return array
230:      */
231:     public function defaultData()
232:     {
233:         $metadata = $this->metadata();
234:         return $metadata->defaultData();
235:     }
236: 
237:     /**
238:      * Sets the data
239:      *
240:      * This function takes a 1-dimensional array and fill the object with its value.
241:      *
242:      * @param array $flatData The data, as a flat (1-dimension) array.
243:      * @return AbstractModel Chainable
244:      */
245:     public function setFlatData(array $flatData)
246:     {
247:         $flatData = $this->setIdFromData($flatData);
248: 
249:         $data = [];
250:         $properties = $this->properties();
251:         foreach ($properties as $propertyIdent => $property) {
252:             $fields = $property->fields(null);
253: 
254:             foreach ($fields as $k => $f) {
255:                 if (is_string($k)) {
256:                     $f_id = $f->ident();
257:                     $key = str_replace($propertyIdent.'_', '', $f_id);
258:                     if (isset($flatData[$f_id])) {
259:                         $data[$propertyIdent][$key] = $flatData[$f_id];
260:                         unset($flatData[$f_id]);
261:                     }
262:                 } else {
263:                     $f_id = $f->ident();
264:                     if (isset($flatData[$f_id])) {
265:                         $data[$propertyIdent] = $flatData[$f_id];
266:                         unset($flatData[$f_id]);
267:                     }
268:                 }
269:             }
270:         }
271: 
272:         $this->setData($data);
273: 
274:         // Set remaining (non-property) data.
275:         if (!empty($flatData)) {
276:             $this->setData($flatData);
277:         }
278: 
279:         return $this;
280:     }
281: 
282:     /**
283:      * @return array
284:      * @todo Implement retrieval of flattened data
285:      */
286:     public function flatData()
287:     {
288:         return [];
289:     }
290: 
291:     /**
292:      * @param string $propertyIdent The property ident to get the value from.
293:      * @return mixed
294:      */
295:     public function propertyValue($propertyIdent)
296:     {
297:         $getter = $this->getter($propertyIdent);
298:         $func   = [ $this, $getter ];
299: 
300:         if (is_callable($func)) {
301:             return call_user_func($func);
302:         } elseif (isset($this->{$propertyIdent})) {
303:             return $this->{$propertyIdent};
304:         }
305: 
306:         return null;
307:     }
308: 
309:     /**
310:      * @param array $properties Optional array of properties to save. If null, use all object's properties.
311:      * @return boolean
312:      */
313:     public function saveProperties(array $properties = null)
314:     {
315:         if ($properties === null) {
316:             $properties = array_keys($this->metadata()->properties());
317:         }
318: 
319:         foreach ($properties as $propertyIdent) {
320:             $p = $this->p($propertyIdent);
321:             $v = $p->save($this->propertyValue($propertyIdent));
322: 
323:             if ($v === null) {
324:                 continue;
325:             }
326: 
327:             $this[$propertyIdent] = $v;
328:         }
329: 
330:         return true;
331:     }
332: 
333:     /**
334:      * Load an object from the database from its l10n key $key.
335:      * Also retrieve and return the actual language that matched.
336:      *
337:      * @param string $key   Key pointing a column's l10n base ident.
338:      * @param mixed  $value Value to search in all languages.
339:      * @param array  $langs List of languages (code, ex: "en") to check into.
340:      * @throws Exception If the PDO query fails.
341:      * @return string The matching language.
342:      */
343:     public function loadFromL10n($key, $value, array $langs)
344:     {
345:         $switch = [];
346:         $where = [];
347:         foreach ($langs as $lang) {
348:             $switch[] = 'when `'.$key.'_'.$lang.'` = :ident then \''.$lang.'\'';
349:             $where[] = '`'.$key.'_'.$lang.'` = :ident';
350:         }
351: 
352:         $q = '
353:             SELECT
354:                 *,
355:                 (case
356:                     '.implode("\n", $switch).'
357:                 end) as _lang
358:             FROM
359:                `'.$this->source()->table().'`
360:             WHERE
361:                 ('.implode(' OR ', $where).')
362:             LIMIT
363:                1';
364: 
365:         $binds = [
366:             'ident' => $value
367:         ];
368: 
369:         $sth = $this->source()->dbQuery($q, $binds);
370:         if ($sth === false) {
371:             throw new Exception('Error');
372:         }
373: 
374:         $data = $sth->fetch(PDO::FETCH_ASSOC);
375:         $lang = $data['_lang'];
376:         unset($data['_lang']);
377: 
378:         if ($data) {
379:             $this->setFlatData($data);
380:         }
381: 
382:         return $lang;
383:     }
384: 
385:     /**
386:      * Save the object's current state to storage.
387:      *
388:      * Overrides default StorableTrait save() method to also save properties.
389:      *
390:      * @see    Charcoal\Source\StorableTrait::save() For the "create" event.
391:      * @return boolean
392:      * @todo   Enable model validation.
393:      */
394:     public function save()
395:     {
396:         $pre = $this->preSave();
397:         if ($pre === false) {
398:             return false;
399:         }
400: 
401:         // Disabled: Invalid models can not be saved.
402:         if (!!false) {
403:             $valid = $this->validate();
404:             if ($valid === false) {
405:                 return false;
406:             }
407:         }
408: 
409:         $ret = $this->source()->saveItem($this);
410:         if ($ret === false) {
411:             return false;
412:         } else {
413:             $this->setId($ret);
414:         }
415:         $this->postSave();
416:         return $ret;
417:     }
418: 
419:     /**
420:      * StorableTrait > preSave(). Save hook called before saving the model.
421:      *
422:      * @return boolean
423:      */
424:     protected function preSave()
425:     {
426:         return $this->saveProperties();
427:     }
428: 
429:     /**
430:      * StorableTrait > preUpdate(). Update hook called before updating the model.
431:      *
432:      * @param string[] $properties Optional. The properties to update.
433:      * @return boolean
434:      */
435:     protected function preUpdate(array $properties = null)
436:     {
437:         // $properties is unused for now
438:         unset($properties);
439:         return $this->saveProperties();
440:     }
441: 
442:     /**
443:      * DescribableTrait > createMetadata().
444:      *
445:      * @return MetadataInterface
446:      */
447:     protected function createMetadata()
448:     {
449:         return new ModelMetadata();
450:     }
451: 
452:     /**
453:      * StorableInterface > createSource()
454:      *
455:      * @throws Exception If the metadata source can not be found.
456:      * @return SourceInterface
457:      */
458:     protected function createSource()
459:     {
460:         $metadata = $this->metadata();
461:         $defaultSource = $metadata->defaultSource();
462:         $sourceConfig = $metadata->source($defaultSource);
463: 
464:         if (!$sourceConfig) {
465:             throw new Exception(
466:                 sprintf('Can not create %s source: invalid metadata.', get_class($this))
467:             );
468:         }
469: 
470:         $sourceType = isset($sourceConfig['type']) ? $sourceConfig['type'] : self::DEFAULT_SOURCE_TYPE;
471:         $sourceFactory = $this->sourceFactory();
472:         $source = $sourceFactory->create($sourceType);
473:         $source->setModel($this);
474: 
475:         $source->setData($sourceConfig);
476: 
477:         return $source;
478:     }
479: 
480:     /**
481:      * ValidatableInterface > create_validator().
482:      *
483:      * @param array $data Optional.
484:      * @return ValidatorInterface
485:      */
486:     protected function createValidator(array $data = null)
487:     {
488:         $validator = new ModelValidator($this);
489:         if ($data !== null) {
490:             $validator->setData($data);
491:         }
492:         return $validator;
493:     }
494: 
495:     /**
496:      * @param array $data Optional. View data.
497:      * @return ViewInterface
498:      */
499:     public function createView(array $data = null)
500:     {
501:         $this->logger->warning('Obsolete method createView called.');
502:         $view = new GenericView([
503:             'logger'=>$this->logger
504:         ]);
505:         if ($data !== null) {
506:             $view->setData($data);
507:         }
508:         return $view;
509:     }
510: 
511:     /**
512:      * Convert the current class name in "type-ident" format.
513:      *
514:      * @return string
515:      */
516:     public function objType()
517:     {
518:         $ident = preg_replace('/([a-z])([A-Z])/', '$1-$2', get_class($this));
519:         $objType = strtolower(str_replace('\\', '/', $ident));
520:         return $objType;
521:     }
522: }
523: 
API documentation generated by ApiGen