EntityContext.php 12 KB

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