1: <?php
2:
3: namespace Charcoal\Model;
4:
5: use \DateTimeInterface;
6: use \Exception;
7: use \InvalidArgumentException;
8:
9: use \PDO;
10:
11:
12: use \Psr\Log\LoggerAwareInterface;
13: use \Psr\Log\LoggerAwareTrait;
14: use \Psr\Log\NullLogger;
15:
16: use \Pimple\Container;
17:
18:
19: use \Charcoal\Config\AbstractEntity;
20:
21:
22: use \Charcoal\View\GenericView;
23: use \Charcoal\View\ViewableInterface;
24: use \Charcoal\View\ViewableTrait;
25:
26:
27: use \Charcoal\Property\DescribablePropertyInterface;
28: use \Charcoal\Property\DescribablePropertyTrait;
29:
30:
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:
40: use \Charcoal\Model\ModelInterface;
41: use \Charcoal\Model\ModelMetadata;
42: use \Charcoal\Model\ModelValidator;
43:
44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 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: 80:
81: public function __construct(array $data = null)
82: {
83:
84: $this->setLogger($data['logger']);
85:
86:
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:
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:
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: 117: 118: 119: 120:
121: public function setDependencies(Container $container)
122: {
123:
124: }
125:
126: 127: 128: 129: 130: 131: 132: 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: 147: 148: 149: 150:
151: public function setData(array $data)
152: {
153: $data = $this->setIdFromData($data);
154:
155: return parent::setData($data);
156: }
157:
158: 159: 160: 161: 162: 163:
164: public function data(array $propertyFilters = null)
165: {
166: $data = [];
167: $properties = $this->properties($propertyFilters);
168: foreach ($properties as $propertyIdent => $property) {
169:
170: $v = $this->propertyValue($propertyIdent);
171: $v = $this->serializedValue($v);
172: $data[$propertyIdent] = $v;
173: }
174: return $data;
175: }
176:
177: 178: 179: 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: 194: 195: 196: 197: 198: 199: 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: 230:
231: public function defaultData()
232: {
233: $metadata = $this->metadata();
234: return $metadata->defaultData();
235: }
236:
237: 238: 239: 240: 241: 242: 243: 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:
275: if (!empty($flatData)) {
276: $this->setData($flatData);
277: }
278:
279: return $this;
280: }
281:
282: 283: 284: 285:
286: public function flatData()
287: {
288: return [];
289: }
290:
291: 292: 293: 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: 311: 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: 335: 336: 337: 338: 339: 340: 341: 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: 387: 388: 389: 390: 391: 392: 393:
394: public function save()
395: {
396: $pre = $this->preSave();
397: if ($pre === false) {
398: return false;
399: }
400:
401:
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: 421: 422: 423:
424: protected function preSave()
425: {
426: return $this->saveProperties();
427: }
428:
429: 430: 431: 432: 433: 434:
435: protected function preUpdate(array $properties = null)
436: {
437:
438: unset($properties);
439: return $this->saveProperties();
440: }
441:
442: 443: 444: 445: 446:
447: protected function createMetadata()
448: {
449: return new ModelMetadata();
450: }
451:
452: 453: 454: 455: 456: 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: 482: 483: 484: 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: 497: 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: 513: 514: 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: