EntityContext.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513
  1. <?php
  2. /**
  3. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  4. * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  5. *
  6. * Licensed under The MIT License
  7. * For full copyright and license information, please see the LICENSE.txt
  8. * Redistributions of files must retain the above copyright notice.
  9. *
  10. * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  11. * @link http://cakephp.org CakePHP(tm) Project
  12. * @since 3.0.0
  13. * @license http://www.opensource.org/licenses/mit-license.php MIT License
  14. */
  15. namespace Cake\View\Form;
  16. use Cake\Collection\Collection;
  17. use Cake\Datasource\EntityInterface;
  18. use Cake\Network\Request;
  19. use Cake\ORM\TableRegistry;
  20. use Cake\Utility\Inflector;
  21. use RuntimeException;
  22. use Traversable;
  23. /**
  24. * Provides a form context around a single entity and its relations.
  25. * It also can be used as context around an array or iterator of entities.
  26. *
  27. * This class lets FormHelper interface with entities or collections
  28. * of entities.
  29. *
  30. * Important Keys:
  31. *
  32. * - `entity` The entity this context is operating on.
  33. * - `table` Either the ORM\Table instance to fetch schema/validators
  34. * from, an array of table instances in the case of a form spanning
  35. * multiple entities, or the name(s) of the table.
  36. * If this is null the table name(s) will be determined using naming
  37. * conventions.
  38. * - `validator` Either the Validation\Validator to use, or the name of the
  39. * validation method to call on the table object. For example 'default'.
  40. * Defaults to 'default'. Can be an array of table alias=>validators when
  41. * dealing with associated forms.
  42. */
  43. class EntityContext implements ContextInterface
  44. {
  45. /**
  46. * The request object.
  47. *
  48. * @var \Cake\Network\Request
  49. */
  50. protected $_request;
  51. /**
  52. * Context data for this object.
  53. *
  54. * @var array
  55. */
  56. protected $_context;
  57. /**
  58. * The name of the top level entity/table object.
  59. *
  60. * @var string
  61. */
  62. protected $_rootName;
  63. /**
  64. * Boolean to track whether or not the entity is a
  65. * collection.
  66. *
  67. * @var bool
  68. */
  69. protected $_isCollection = false;
  70. /**
  71. * A dictionary of tables
  72. *
  73. * @var array
  74. */
  75. protected $_tables = [];
  76. /**
  77. * Constructor.
  78. *
  79. * @param \Cake\Network\Request $request The request object.
  80. * @param array $context Context info.
  81. */
  82. public function __construct(Request $request, array $context)
  83. {
  84. $this->_request = $request;
  85. $context += [
  86. 'entity' => null,
  87. 'table' => null,
  88. 'validator' => [],
  89. ];
  90. $this->_context = $context;
  91. $this->_prepare();
  92. }
  93. /**
  94. * Prepare some additional data from the context.
  95. *
  96. * If the table option was provided to the constructor and it
  97. * was a string, ORM\TableRegistry will be used to get the correct table instance.
  98. *
  99. * If an object is provided as the table option, it will be used as is.
  100. *
  101. * If no table option is provided, the table name will be derived based on
  102. * naming conventions. This inference will work with a number of common objects
  103. * like arrays, Collection objects and ResultSets.
  104. *
  105. * @return void
  106. * @throws \RuntimeException When a table object cannot be located/inferred.
  107. */
  108. protected function _prepare()
  109. {
  110. $table = $this->_context['table'];
  111. $entity = $this->_context['entity'];
  112. if (empty($table)) {
  113. if (is_array($entity) || $entity instanceof Traversable) {
  114. foreach ($entity as $e) {
  115. $entity = $e;
  116. break;
  117. }
  118. }
  119. $isEntity = $entity instanceof EntityInterface;
  120. if ($isEntity) {
  121. $table = $entity->source();
  122. }
  123. if (!$table && $isEntity && get_class($entity) !== 'Cake\ORM\Entity') {
  124. list(, $entityClass) = namespaceSplit(get_class($entity));
  125. $table = Inflector::pluralize($entityClass);
  126. }
  127. }
  128. if (is_string($table)) {
  129. $table = TableRegistry::get($table);
  130. }
  131. if (!is_object($table)) {
  132. throw new RuntimeException(
  133. 'Unable to find table class for current entity'
  134. );
  135. }
  136. $this->_isCollection = (
  137. is_array($entity) ||
  138. $entity instanceof Traversable
  139. );
  140. $alias = $this->_rootName = $table->alias();
  141. $this->_tables[$alias] = $table;
  142. }
  143. /**
  144. * Get the primary key data for the context.
  145. *
  146. * Gets the primary key columns from the root entity's schema.
  147. *
  148. * @return bool
  149. */
  150. public function primaryKey()
  151. {
  152. return (array)$this->_tables[$this->_rootName]->primaryKey();
  153. }
  154. /**
  155. * {@inheritDoc}
  156. */
  157. public function isPrimaryKey($field)
  158. {
  159. $parts = explode('.', $field);
  160. $table = $this->_getTable($parts);
  161. $primaryKey = (array)$table->primaryKey();
  162. return in_array(array_pop($parts), $primaryKey);
  163. }
  164. /**
  165. * Check whether or not this form is a create or update.
  166. *
  167. * If the context is for a single entity, the entity's isNew() method will
  168. * be used. If isNew() returns null, a create operation will be assumed.
  169. *
  170. * If the context is for a collection or array the first object in the
  171. * collection will be used.
  172. *
  173. * @return bool
  174. */
  175. public function isCreate()
  176. {
  177. $entity = $this->_context['entity'];
  178. if (is_array($entity) || $entity instanceof Traversable) {
  179. foreach ($entity as $e) {
  180. $entity = $e;
  181. break;
  182. }
  183. }
  184. if ($entity instanceof EntityInterface) {
  185. return $entity->isNew() !== false;
  186. }
  187. return true;
  188. }
  189. /**
  190. * Get the value for a given path.
  191. *
  192. * Traverses the entity data and finds the value for $path.
  193. *
  194. * @param string $field The dot separated path to the value.
  195. * @return mixed The value of the field or null on a miss.
  196. */
  197. public function val($field)
  198. {
  199. $val = $this->_request->data($field);
  200. if ($val !== null) {
  201. return $val;
  202. }
  203. if (empty($this->_context['entity'])) {
  204. return null;
  205. }
  206. $parts = explode('.', $field);
  207. $entity = $this->entity($parts);
  208. if (end($parts) === '_ids' && !empty($entity)) {
  209. return $this->_extractMultiple($entity, $parts);
  210. }
  211. if ($entity instanceof EntityInterface) {
  212. return $entity->get(array_pop($parts));
  213. }
  214. if (is_array($entity)) {
  215. $key = array_pop($parts);
  216. return isset($entity[$key]) ? $entity[$key] : null;
  217. }
  218. return null;
  219. }
  220. /**
  221. * Helper method used to extract all the primary key values out of an array, The
  222. * primary key column is guessed out of the provided $path array
  223. *
  224. * @param array|\Traversable $values The list from which to extract primary keys from
  225. * @param array $path Each one of the parts in a path for a field name
  226. * @return array
  227. */
  228. protected function _extractMultiple($values, $path)
  229. {
  230. if (!(is_array($values) || $values instanceof Traversable)) {
  231. return null;
  232. }
  233. $table = $this->_getTable($path, false);
  234. $primary = $table ? (array)$table->primaryKey() : ['id'];
  235. return (new Collection($values))->extract($primary[0])->toArray();
  236. }
  237. /**
  238. * Fetch the leaf entity for the given path.
  239. *
  240. * This method will traverse the given path and find the leaf
  241. * entity. If the path does not contain a leaf entity false
  242. * will be returned.
  243. *
  244. * @param array|null $path Each one of the parts in a path for a field name
  245. * or null to get the entity passed in contructor context.
  246. * @return \Cake\Datasource\EntityInterface|\Traversable|array|bool
  247. * @throws \RuntimeException When properties cannot be read.
  248. */
  249. public function entity($path = null)
  250. {
  251. if ($path === null) {
  252. return $this->_context['entity'];
  253. }
  254. $oneElement = count($path) === 1;
  255. if ($oneElement && $this->_isCollection) {
  256. return false;
  257. }
  258. $entity = $this->_context['entity'];
  259. if ($oneElement) {
  260. return $entity;
  261. }
  262. if ($path[0] === $this->_rootName) {
  263. $path = array_slice($path, 1);
  264. }
  265. $len = count($path);
  266. $last = $len - 1;
  267. for ($i = 0; $i < $len; $i++) {
  268. $prop = $path[$i];
  269. $next = $this->_getProp($entity, $prop);
  270. $isLast = ($i === $last);
  271. if (!$isLast && $next === null && $prop !== '_ids') {
  272. $table = $this->_getTable($path);
  273. return $table->newEntity();
  274. }
  275. $isTraversable = (
  276. is_array($next) ||
  277. $next instanceof Traversable ||
  278. $next instanceof EntityInterface
  279. );
  280. if ($isLast || !$isTraversable) {
  281. return $entity;
  282. }
  283. $entity = $next;
  284. }
  285. throw new RuntimeException(sprintf(
  286. 'Unable to fetch property "%s"',
  287. implode(".", $path)
  288. ));
  289. }
  290. /**
  291. * Read property values or traverse arrays/iterators.
  292. *
  293. * @param mixed $target The entity/array/collection to fetch $field from.
  294. * @param string $field The next field to fetch.
  295. * @return mixed
  296. */
  297. protected function _getProp($target, $field)
  298. {
  299. if (is_array($target) && isset($target[$field])) {
  300. return $target[$field];
  301. }
  302. if ($target instanceof EntityInterface) {
  303. return $target->get($field);
  304. }
  305. if ($target instanceof Traversable) {
  306. foreach ($target as $i => $val) {
  307. if ($i == $field) {
  308. return $val;
  309. }
  310. }
  311. return false;
  312. }
  313. }
  314. /**
  315. * Check if a field should be marked as required.
  316. *
  317. * @param string $field The dot separated path to the field you want to check.
  318. * @return bool
  319. */
  320. public function isRequired($field)
  321. {
  322. $parts = explode('.', $field);
  323. $entity = $this->entity($parts);
  324. $isNew = true;
  325. if ($entity instanceof EntityInterface) {
  326. $isNew = $entity->isNew();
  327. }
  328. $validator = $this->_getValidator($parts);
  329. $fieldName = array_pop($parts);
  330. if (!$validator->hasField($fieldName)) {
  331. return false;
  332. }
  333. if ($this->type($field) !== 'boolean') {
  334. return $validator->isEmptyAllowed($fieldName, $isNew) === false;
  335. }
  336. return false;
  337. }
  338. /**
  339. * Get the field names from the top level entity.
  340. *
  341. * If the context is for an array of entities, the 0th index will be used.
  342. *
  343. * @return array Array of fieldnames in the table/entity.
  344. */
  345. public function fieldNames()
  346. {
  347. $table = $this->_getTable('0');
  348. return $table->schema()->columns();
  349. }
  350. /**
  351. * Get the validator associated to an entity based on naming
  352. * conventions.
  353. *
  354. * @param array $parts Each one of the parts in a path for a field name
  355. * @return \Cake\Validation\Validator
  356. */
  357. protected function _getValidator($parts)
  358. {
  359. $keyParts = array_filter(array_slice($parts, 0, -1), function ($part) {
  360. return !is_numeric($part);
  361. });
  362. $key = implode('.', $keyParts);
  363. $entity = $this->entity($parts) ?: null;
  364. if (isset($this->_validator[$key])) {
  365. $this->_validator[$key]->provider('entity', $entity);
  366. return $this->_validator[$key];
  367. }
  368. $table = $this->_getTable($parts);
  369. $alias = $table->alias();
  370. $method = 'default';
  371. if (is_string($this->_context['validator'])) {
  372. $method = $this->_context['validator'];
  373. } elseif (isset($this->_context['validator'][$alias])) {
  374. $method = $this->_context['validator'][$alias];
  375. }
  376. $validator = $table->validator($method);
  377. $validator->provider('entity', $entity);
  378. return $this->_validator[$key] = $validator;
  379. }
  380. /**
  381. * Get the table instance from a property path
  382. *
  383. * @param array $parts Each one of the parts in a path for a field name
  384. * @param bool $rootFallback Whether or not to fallback to the root entity.
  385. * @return \Cake\ORM\Table|bool Table instance or false
  386. */
  387. protected function _getTable($parts, $rootFallback = true)
  388. {
  389. if (count($parts) === 1) {
  390. return $this->_tables[$this->_rootName];
  391. }
  392. $normalized = array_slice(array_filter($parts, function ($part) {
  393. return !is_numeric($part);
  394. }), 0, -1);
  395. $path = implode('.', $normalized);
  396. if (isset($this->_tables[$path])) {
  397. return $this->_tables[$path];
  398. }
  399. if (current($normalized) === $this->_rootName) {
  400. $normalized = array_slice($normalized, 1);
  401. }
  402. $table = $this->_tables[$this->_rootName];
  403. foreach ($normalized as $part) {
  404. $assoc = $table->associations()->getByProperty($part);
  405. if (!$assoc && $rootFallback) {
  406. break;
  407. }
  408. if (!$assoc && !$rootFallback) {
  409. return false;
  410. }
  411. $table = $assoc->target();
  412. }
  413. return $this->_tables[$path] = $table;
  414. }
  415. /**
  416. * Get the abstract field type for a given field name.
  417. *
  418. * @param string $field A dot separated path to get a schema type for.
  419. * @return null|string An abstract data type or null.
  420. * @see \Cake\Database\Type
  421. */
  422. public function type($field)
  423. {
  424. $parts = explode('.', $field);
  425. $table = $this->_getTable($parts);
  426. return $table->schema()->baseColumnType(array_pop($parts));
  427. }
  428. /**
  429. * Get an associative array of other attributes for a field name.
  430. *
  431. * @param string $field A dot separated path to get additional data on.
  432. * @return array An array of data describing the additional attributes on a field.
  433. */
  434. public function attributes($field)
  435. {
  436. $parts = explode('.', $field);
  437. $table = $this->_getTable($parts);
  438. $column = (array)$table->schema()->column(array_pop($parts));
  439. $whitelist = ['length' => null, 'precision' => null];
  440. return array_intersect_key($column, $whitelist);
  441. }
  442. /**
  443. * Check whether or not a field has an error attached to it
  444. *
  445. * @param string $field A dot separated path to check errors on.
  446. * @return bool Returns true if the errors for the field are not empty.
  447. */
  448. public function hasError($field)
  449. {
  450. return $this->error($field) !== [];
  451. }
  452. /**
  453. * Get the errors for a given field
  454. *
  455. * @param string $field A dot separated path to check errors on.
  456. * @return array An array of errors.
  457. */
  458. public function error($field)
  459. {
  460. $parts = explode('.', $field);
  461. $entity = $this->entity($parts);
  462. if ($entity instanceof EntityInterface) {
  463. return $entity->errors(array_pop($parts));
  464. }
  465. return [];
  466. }
  467. }