1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14:
15: namespace Cake\View\Form;
16:
17: use ArrayAccess;
18: use Cake\Collection\Collection;
19: use Cake\Datasource\EntityInterface;
20: use Cake\Datasource\RepositoryInterface;
21: use Cake\Http\ServerRequest;
22: use Cake\ORM\Locator\LocatorAwareTrait;
23: use Cake\Utility\Inflector;
24: use RuntimeException;
25: use Traversable;
26:
27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46:
47: class EntityContext implements ContextInterface
48: {
49: use LocatorAwareTrait;
50:
51: 52: 53: 54: 55:
56: protected $_request;
57:
58: 59: 60: 61: 62:
63: protected $_context;
64:
65: 66: 67: 68: 69:
70: protected $_rootName;
71:
72: 73: 74: 75: 76: 77:
78: protected $_isCollection = false;
79:
80: 81: 82: 83: 84:
85: protected $_tables = [];
86:
87: 88: 89: 90: 91:
92: protected $_validator = [];
93:
94: 95: 96: 97: 98: 99:
100: public function __construct(ServerRequest $request, array $context)
101: {
102: $this->_request = $request;
103: $context += [
104: 'entity' => null,
105: 'table' => null,
106: 'validator' => [],
107: ];
108: $this->_context = $context;
109: $this->_prepare();
110: }
111:
112: 113: 114: 115: 116: 117: 118: 119: 120: 121: 122: 123: 124: 125: 126:
127: protected function _prepare()
128: {
129: $table = $this->_context['table'];
130: $entity = $this->_context['entity'];
131: if (empty($table)) {
132: if (is_array($entity) || $entity instanceof Traversable) {
133: foreach ($entity as $e) {
134: $entity = $e;
135: break;
136: }
137: }
138: $isEntity = $entity instanceof EntityInterface;
139:
140: if ($isEntity) {
141: $table = $entity->getSource();
142: }
143: if (!$table && $isEntity && get_class($entity) !== 'Cake\ORM\Entity') {
144: list(, $entityClass) = namespaceSplit(get_class($entity));
145: $table = Inflector::pluralize($entityClass);
146: }
147: }
148: if (is_string($table)) {
149: $table = $this->getTableLocator()->get($table);
150: }
151:
152: if (!($table instanceof RepositoryInterface)) {
153: throw new RuntimeException(
154: 'Unable to find table class for current entity'
155: );
156: }
157: $this->_isCollection = (
158: is_array($entity) ||
159: $entity instanceof Traversable
160: );
161:
162: $alias = $this->_rootName = $table->getAlias();
163: $this->_tables[$alias] = $table;
164: }
165:
166: 167: 168: 169: 170: 171: 172:
173: public function primaryKey()
174: {
175: return (array)$this->_tables[$this->_rootName]->getPrimaryKey();
176: }
177:
178: 179: 180:
181: public function isPrimaryKey($field)
182: {
183: $parts = explode('.', $field);
184: $table = $this->_getTable($parts);
185: $primaryKey = (array)$table->getPrimaryKey();
186:
187: return in_array(array_pop($parts), $primaryKey);
188: }
189:
190: 191: 192: 193: 194: 195: 196: 197: 198: 199: 200:
201: public function isCreate()
202: {
203: $entity = $this->_context['entity'];
204: if (is_array($entity) || $entity instanceof Traversable) {
205: foreach ($entity as $e) {
206: $entity = $e;
207: break;
208: }
209: }
210: if ($entity instanceof EntityInterface) {
211: return $entity->isNew() !== false;
212: }
213:
214: return true;
215: }
216:
217: 218: 219: 220: 221: 222: 223: 224: 225: 226: 227: 228: 229:
230: public function val($field, $options = [])
231: {
232: $options += [
233: 'default' => null,
234: 'schemaDefault' => true
235: ];
236:
237: $val = $this->_request->getData($field);
238: if ($val !== null) {
239: return $val;
240: }
241: if (empty($this->_context['entity'])) {
242: return $options['default'];
243: }
244: $parts = explode('.', $field);
245: $entity = $this->entity($parts);
246:
247: if (end($parts) === '_ids' && !empty($entity)) {
248: return $this->_extractMultiple($entity, $parts);
249: }
250:
251: if ($entity instanceof EntityInterface) {
252: $part = end($parts);
253: $val = $entity->get($part);
254: if ($val !== null) {
255: return $val;
256: }
257: if ($options['default'] !== null
258: || !$options['schemaDefault']
259: || !$entity->isNew()
260: ) {
261: return $options['default'];
262: }
263:
264: return $this->_schemaDefault($parts);
265: }
266: if (is_array($entity) || $entity instanceof ArrayAccess) {
267: $key = array_pop($parts);
268:
269: return isset($entity[$key]) ? $entity[$key] : $options['default'];
270: }
271:
272: return null;
273: }
274:
275: 276: 277: 278: 279: 280:
281: protected function _schemaDefault($parts)
282: {
283: $table = $this->_getTable($parts);
284: if ($table === false) {
285: return null;
286: }
287: $field = end($parts);
288: $defaults = $table->getSchema()->defaultValues();
289: if (!array_key_exists($field, $defaults)) {
290: return null;
291: }
292:
293: return $defaults[$field];
294: }
295:
296: 297: 298: 299: 300: 301: 302: 303:
304: protected function _extractMultiple($values, $path)
305: {
306: if (!(is_array($values) || $values instanceof Traversable)) {
307: return null;
308: }
309: $table = $this->_getTable($path, false);
310: $primary = $table ? (array)$table->getPrimaryKey() : ['id'];
311:
312: return (new Collection($values))->extract($primary[0])->toArray();
313: }
314:
315: 316: 317: 318: 319: 320: 321: 322: 323: 324: 325: 326: 327:
328: public function entity($path = null)
329: {
330: if ($path === null) {
331: return $this->_context['entity'];
332: }
333:
334: $oneElement = count($path) === 1;
335: if ($oneElement && $this->_isCollection) {
336: return false;
337: }
338: $entity = $this->_context['entity'];
339: if ($oneElement) {
340: return $entity;
341: }
342:
343: if ($path[0] === $this->_rootName) {
344: $path = array_slice($path, 1);
345: }
346:
347: $len = count($path);
348: $last = $len - 1;
349: for ($i = 0; $i < $len; $i++) {
350: $prop = $path[$i];
351: $next = $this->_getProp($entity, $prop);
352: $isLast = ($i === $last);
353: if (!$isLast && $next === null && $prop !== '_ids') {
354: $table = $this->_getTable($path);
355:
356: return $table->newEntity();
357: }
358:
359: $isTraversable = (
360: is_array($next) ||
361: $next instanceof Traversable ||
362: $next instanceof EntityInterface
363: );
364: if ($isLast || !$isTraversable) {
365: return $entity;
366: }
367: $entity = $next;
368: }
369: throw new RuntimeException(sprintf(
370: 'Unable to fetch property "%s"',
371: implode('.', $path)
372: ));
373: }
374:
375: 376: 377: 378: 379: 380: 381: 382: 383: 384: 385: 386:
387: protected function leafEntity($path = null)
388: {
389: if ($path === null) {
390: return $this->_context['entity'];
391: }
392:
393: $oneElement = count($path) === 1;
394: if ($oneElement && $this->_isCollection) {
395: throw new RuntimeException(sprintf(
396: 'Unable to fetch property "%s"',
397: implode('.', $path)
398: ));
399: }
400: $entity = $this->_context['entity'];
401: if ($oneElement) {
402: return [$entity, $path];
403: }
404:
405: if ($path[0] === $this->_rootName) {
406: $path = array_slice($path, 1);
407: }
408:
409: $len = count($path);
410: $last = $len - 1;
411: $leafEntity = $entity;
412: for ($i = 0; $i < $len; $i++) {
413: $prop = $path[$i];
414: $next = $this->_getProp($entity, $prop);
415:
416:
417: if (is_array($entity) && !($next instanceof EntityInterface || $next instanceof Traversable)) {
418: return [$leafEntity, array_slice($path, $i - 1)];
419: }
420:
421: if ($next instanceof EntityInterface) {
422: $leafEntity = $next;
423: }
424:
425:
426:
427: $isTraversable = (
428: is_array($next) ||
429: $next instanceof Traversable ||
430: $next instanceof EntityInterface
431: );
432: if (!$isTraversable) {
433: return [$leafEntity, array_slice($path, $i)];
434: }
435: $entity = $next;
436: }
437: throw new RuntimeException(sprintf(
438: 'Unable to fetch property "%s"',
439: implode('.', $path)
440: ));
441: }
442:
443: 444: 445: 446: 447: 448: 449:
450: protected function _getProp($target, $field)
451: {
452: if (is_array($target) && isset($target[$field])) {
453: return $target[$field];
454: }
455: if ($target instanceof EntityInterface) {
456: return $target->get($field);
457: }
458: if ($target instanceof Traversable) {
459: foreach ($target as $i => $val) {
460: if ($i == $field) {
461: return $val;
462: }
463: }
464:
465: return false;
466: }
467: }
468:
469: 470: 471: 472: 473: 474:
475: public function isRequired($field)
476: {
477: $parts = explode('.', $field);
478: $entity = $this->entity($parts);
479:
480: $isNew = true;
481: if ($entity instanceof EntityInterface) {
482: $isNew = $entity->isNew();
483: }
484:
485: $validator = $this->_getValidator($parts);
486: $fieldName = array_pop($parts);
487: if (!$validator->hasField($fieldName)) {
488: return false;
489: }
490: if ($this->type($field) !== 'boolean') {
491: return $validator->isEmptyAllowed($fieldName, $isNew) === false;
492: }
493:
494: return false;
495: }
496:
497: 498: 499:
500: public function getRequiredMessage($field)
501: {
502: $parts = explode('.', $field);
503:
504: $validator = $this->_getValidator($parts);
505: $fieldName = array_pop($parts);
506: if (!$validator->hasField($fieldName)) {
507: return null;
508: }
509:
510: $ruleset = $validator->field($fieldName);
511:
512: $requiredMessage = $validator->getRequiredMessage($fieldName);
513: $emptyMessage = $validator->getNotEmptyMessage($fieldName);
514:
515: if ($ruleset->isPresenceRequired() && $requiredMessage) {
516: return $requiredMessage;
517: }
518: if (!$ruleset->isEmptyAllowed() && $emptyMessage) {
519: return $emptyMessage;
520: }
521:
522: return null;
523: }
524:
525: 526: 527: 528: 529: 530:
531: public function getMaxLength($field)
532: {
533: $parts = explode('.', $field);
534: $validator = $this->_getValidator($parts);
535: $fieldName = array_pop($parts);
536: if (!$validator->hasField($fieldName)) {
537: return null;
538: }
539: foreach ($validator->field($fieldName)->rules() as $rule) {
540: if ($rule->get('rule') === 'maxLength') {
541: return $rule->get('pass')[0];
542: }
543: }
544:
545: return null;
546: }
547:
548: 549: 550: 551: 552: 553: 554:
555: public function fieldNames()
556: {
557: $table = $this->_getTable('0');
558:
559: return $table->getSchema()->columns();
560: }
561:
562: 563: 564: 565: 566: 567: 568:
569: protected function _getValidator($parts)
570: {
571: $keyParts = array_filter(array_slice($parts, 0, -1), function ($part) {
572: return !is_numeric($part);
573: });
574: $key = implode('.', $keyParts);
575: $entity = $this->entity($parts) ?: null;
576:
577: if (isset($this->_validator[$key])) {
578: $this->_validator[$key]->setProvider('entity', $entity);
579:
580: return $this->_validator[$key];
581: }
582:
583: $table = $this->_getTable($parts);
584: $alias = $table->getAlias();
585:
586: $method = 'default';
587: if (is_string($this->_context['validator'])) {
588: $method = $this->_context['validator'];
589: } elseif (isset($this->_context['validator'][$alias])) {
590: $method = $this->_context['validator'][$alias];
591: }
592:
593: $validator = $table->getValidator($method);
594: $validator->setProvider('entity', $entity);
595:
596: return $this->_validator[$key] = $validator;
597: }
598:
599: 600: 601: 602: 603: 604: 605: 606:
607: protected function _getTable($parts, $fallback = true)
608: {
609: if (!is_array($parts) || count($parts) === 1) {
610: return $this->_tables[$this->_rootName];
611: }
612:
613: $normalized = array_slice(array_filter($parts, function ($part) {
614: return !is_numeric($part);
615: }), 0, -1);
616:
617: $path = implode('.', $normalized);
618: if (isset($this->_tables[$path])) {
619: return $this->_tables[$path];
620: }
621:
622: if (current($normalized) === $this->_rootName) {
623: $normalized = array_slice($normalized, 1);
624: }
625:
626: $table = $this->_tables[$this->_rootName];
627: $assoc = null;
628: foreach ($normalized as $part) {
629: if ($part === '_joinData') {
630: if ($assoc) {
631: $table = $assoc->junction();
632: $assoc = null;
633: continue;
634: }
635: } else {
636: $assoc = $table->associations()->getByProperty($part);
637: }
638:
639: if (!$assoc && $fallback) {
640: break;
641: }
642: if (!$assoc && !$fallback) {
643: return false;
644: }
645:
646: $table = $assoc->getTarget();
647: }
648:
649: return $this->_tables[$path] = $table;
650: }
651:
652: 653: 654: 655: 656: 657: 658:
659: public function type($field)
660: {
661: $parts = explode('.', $field);
662: $table = $this->_getTable($parts);
663:
664: return $table->getSchema()->baseColumnType(array_pop($parts));
665: }
666:
667: 668: 669: 670: 671: 672:
673: public function attributes($field)
674: {
675: $parts = explode('.', $field);
676: $table = $this->_getTable($parts);
677: $column = (array)$table->getSchema()->getColumn(array_pop($parts));
678: $whitelist = ['length' => null, 'precision' => null];
679:
680: return array_intersect_key($column, $whitelist);
681: }
682:
683: 684: 685: 686: 687: 688:
689: public function hasError($field)
690: {
691: return $this->error($field) !== [];
692: }
693:
694: 695: 696: 697: 698: 699:
700: public function error($field)
701: {
702: $parts = explode('.', $field);
703: try {
704: list($entity, $remainingParts) = $this->leafEntity($parts);
705: } catch (RuntimeException $e) {
706: return [];
707: }
708: if (count($remainingParts) === 0) {
709: return $entity->getErrors();
710: }
711:
712: if ($entity instanceof EntityInterface) {
713: $error = $entity->getError(implode('.', $remainingParts));
714: if ($error) {
715: return $error;
716: }
717:
718: return $entity->getError(array_pop($parts));
719: }
720:
721: return [];
722: }
723: }
724: