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: public function entity($path = null)
328: {
329: if ($path === null) {
330: return $this->_context['entity'];
331: }
332:
333: $oneElement = count($path) === 1;
334: if ($oneElement && $this->_isCollection) {
335: return false;
336: }
337: $entity = $this->_context['entity'];
338: if ($oneElement) {
339: return $entity;
340: }
341:
342: if ($path[0] === $this->_rootName) {
343: $path = array_slice($path, 1);
344: }
345:
346: $len = count($path);
347: $last = $len - 1;
348: for ($i = 0; $i < $len; $i++) {
349: $prop = $path[$i];
350: $next = $this->_getProp($entity, $prop);
351: $isLast = ($i === $last);
352:
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: protected function _getProp($target, $field)
383: {
384: if (is_array($target) && isset($target[$field])) {
385: return $target[$field];
386: }
387: if ($target instanceof EntityInterface) {
388: return $target->get($field);
389: }
390: if ($target instanceof Traversable) {
391: foreach ($target as $i => $val) {
392: if ($i == $field) {
393: return $val;
394: }
395: }
396:
397: return false;
398: }
399: }
400:
401: 402: 403: 404: 405: 406:
407: public function isRequired($field)
408: {
409: $parts = explode('.', $field);
410: $entity = $this->entity($parts);
411:
412: $isNew = true;
413: if ($entity instanceof EntityInterface) {
414: $isNew = $entity->isNew();
415: }
416:
417: $validator = $this->_getValidator($parts);
418: $fieldName = array_pop($parts);
419: if (!$validator->hasField($fieldName)) {
420: return false;
421: }
422: if ($this->type($field) !== 'boolean') {
423: return $validator->isEmptyAllowed($fieldName, $isNew) === false;
424: }
425:
426: return false;
427: }
428:
429: 430: 431:
432: public function getRequiredMessage($field)
433: {
434: $parts = explode('.', $field);
435:
436: $validator = $this->_getValidator($parts);
437: $fieldName = array_pop($parts);
438: if (!$validator->hasField($fieldName)) {
439: return null;
440: }
441:
442: $ruleset = $validator->field($fieldName);
443:
444: $requiredMessage = $validator->getRequiredMessage($fieldName);
445: $emptyMessage = $validator->getNotEmptyMessage($fieldName);
446:
447: if ($ruleset->isPresenceRequired() && $requiredMessage) {
448: return $requiredMessage;
449: }
450: if (!$ruleset->isEmptyAllowed() && $emptyMessage) {
451: return $emptyMessage;
452: }
453:
454: return null;
455: }
456:
457: 458: 459: 460: 461: 462:
463: public function getMaxLength($field)
464: {
465: $parts = explode('.', $field);
466: $validator = $this->_getValidator($parts);
467: $fieldName = array_pop($parts);
468: if (!$validator->hasField($fieldName)) {
469: return null;
470: }
471: foreach ($validator->field($fieldName)->rules() as $rule) {
472: if ($rule->get('rule') === 'maxLength') {
473: return $rule->get('pass')[0];
474: }
475: }
476:
477: return null;
478: }
479:
480: 481: 482: 483: 484: 485: 486:
487: public function fieldNames()
488: {
489: $table = $this->_getTable('0');
490:
491: return $table->getSchema()->columns();
492: }
493:
494: 495: 496: 497: 498: 499: 500:
501: protected function _getValidator($parts)
502: {
503: $keyParts = array_filter(array_slice($parts, 0, -1), function ($part) {
504: return !is_numeric($part);
505: });
506: $key = implode('.', $keyParts);
507: $entity = $this->entity($parts) ?: null;
508:
509: if (isset($this->_validator[$key])) {
510: $this->_validator[$key]->setProvider('entity', $entity);
511:
512: return $this->_validator[$key];
513: }
514:
515: $table = $this->_getTable($parts);
516: $alias = $table->getAlias();
517:
518: $method = 'default';
519: if (is_string($this->_context['validator'])) {
520: $method = $this->_context['validator'];
521: } elseif (isset($this->_context['validator'][$alias])) {
522: $method = $this->_context['validator'][$alias];
523: }
524:
525: $validator = $table->getValidator($method);
526: $validator->setProvider('entity', $entity);
527:
528: return $this->_validator[$key] = $validator;
529: }
530:
531: 532: 533: 534: 535: 536: 537: 538:
539: protected function _getTable($parts, $fallback = true)
540: {
541: if (!is_array($parts) || count($parts) === 1) {
542: return $this->_tables[$this->_rootName];
543: }
544:
545: $normalized = array_slice(array_filter($parts, function ($part) {
546: return !is_numeric($part);
547: }), 0, -1);
548:
549: $path = implode('.', $normalized);
550: if (isset($this->_tables[$path])) {
551: return $this->_tables[$path];
552: }
553:
554: if (current($normalized) === $this->_rootName) {
555: $normalized = array_slice($normalized, 1);
556: }
557:
558: $table = $this->_tables[$this->_rootName];
559: $assoc = null;
560: foreach ($normalized as $part) {
561: if ($part === '_joinData') {
562: if ($assoc) {
563: $table = $assoc->junction();
564: $assoc = null;
565: continue;
566: }
567: } else {
568: $assoc = $table->associations()->getByProperty($part);
569: }
570:
571: if (!$assoc && $fallback) {
572: break;
573: }
574: if (!$assoc && !$fallback) {
575: return false;
576: }
577:
578: $table = $assoc->getTarget();
579: }
580:
581: return $this->_tables[$path] = $table;
582: }
583:
584: 585: 586: 587: 588: 589: 590:
591: public function type($field)
592: {
593: $parts = explode('.', $field);
594: $table = $this->_getTable($parts);
595:
596: return $table->getSchema()->baseColumnType(array_pop($parts));
597: }
598:
599: 600: 601: 602: 603: 604:
605: public function attributes($field)
606: {
607: $parts = explode('.', $field);
608: $table = $this->_getTable($parts);
609: $column = (array)$table->getSchema()->getColumn(array_pop($parts));
610: $whitelist = ['length' => null, 'precision' => null];
611:
612: return array_intersect_key($column, $whitelist);
613: }
614:
615: 616: 617: 618: 619: 620:
621: public function hasError($field)
622: {
623: return $this->error($field) !== [];
624: }
625:
626: 627: 628: 629: 630: 631:
632: public function error($field)
633: {
634: $parts = explode('.', $field);
635: $entity = $this->entity($parts);
636:
637: if ($entity instanceof EntityInterface) {
638: return $entity->getError(array_pop($parts));
639: }
640:
641: return [];
642: }
643: }
644: