EntityContext.php 15 KB

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