PageRenderTime 1012ms CodeModel.GetById 13ms RepoModel.GetById 0ms app.codeStats 0ms

/plugins/Field/src/Controller/FieldUIControllerTrait.php

http://github.com/QuickAppsCMS/QuickApps-CMS
PHP | 616 lines | 332 code | 61 blank | 223 comment | 44 complexity | 6d0edf28c67089c61c12d8ac6354954f MD5 | raw file
Possible License(s): LGPL-2.1, MPL-2.0-no-copyleft-exception, GPL-3.0
  1. <?php
  2. /**
  3. * Licensed under The GPL-3.0 License
  4. * For full copyright and license information, please see the LICENSE.txt
  5. * Redistributions of files must retain the above copyright notice.
  6. *
  7. * @since 2.0.0
  8. * @author Christopher Castro <chris@quickapps.es>
  9. * @link http://www.quickappscms.org
  10. * @license http://opensource.org/licenses/gpl-3.0.html GPL-3.0 License
  11. */
  12. namespace Field\Controller;
  13. use Cake\Controller\Controller;
  14. use Cake\Core\Plugin;
  15. use Cake\Event\Event;
  16. use Cake\Network\Exception\ForbiddenException;
  17. use Cake\Network\Exception\NotFoundException;
  18. use Cake\ORM\Entity;
  19. use Cake\ORM\Exception\RecordNotFoundException;
  20. use Cake\ORM\TableRegistry;
  21. use Cake\Utility\Inflector;
  22. use CMS\Event\EventDispatcherTrait;
  23. use CMS\View\ViewModeAwareTrait;
  24. /**
  25. * Field UI Trait.
  26. *
  27. * Other plugins may `extends` Field plugin by using this trait in their controllers.
  28. *
  29. * With this trait, Field plugin provides an user friendly UI for manage entity's
  30. * custom fields. It provides a field-manager user interface (UI) by attaching a
  31. * series of actions over a `clean` controller.
  32. *
  33. * # Usage:
  34. *
  35. * Beside adding `use FieldUIControllerTrait;` to your controller you MUST also
  36. * indicate the name of the table being managed using the **$_manageTable
  37. * property**, you must set this property to any valid table alias within your
  38. * system (dot notation is also allowed). For example:
  39. *
  40. * ```php
  41. * uses Field\Controller\FieldUIControllerTrait;
  42. *
  43. * class MyCleanController extends <Plugin>AppController {
  44. * use FieldUIControllerTrait;
  45. * protected $_manageTable = 'Content.Contents';
  46. * }
  47. * ```
  48. *
  49. * Optionally you can indicate a bundle within your table to manage by declaring the
  50. * **$_bundle property**:
  51. *
  52. * ```php
  53. * uses Field\Controller\FieldUIControllerTrait;
  54. *
  55. * class MyCleanController extends <Plugin>AppController {
  56. * use FieldUIControllerTrait;
  57. * protected $_manageTable = 'Content.Contents';
  58. * protected $_bundle = 'articles';
  59. * }
  60. * ```
  61. *
  62. * # Requirements
  63. *
  64. * - This trait should only be used over a clean controller.
  65. * - You must define `$_manageTable` property in your controller.
  66. * - Your Controller must be a backend-controller (under `Controller\Admin` namespace).
  67. */
  68. trait FieldUIControllerTrait
  69. {
  70. use EventDispatcherTrait;
  71. use ViewModeAwareTrait;
  72. /**
  73. * Instance of the table being managed.
  74. *
  75. * @var \Cake\ORM\Table
  76. */
  77. protected $_table = null;
  78. /**
  79. * Table alias name.
  80. *
  81. * @var string
  82. */
  83. protected $_tableAlias = null;
  84. /**
  85. * Validation rules.
  86. *
  87. * @param \Cake\Event\Event $event The event instance.
  88. * @return void
  89. * @throws \Cake\Network\Exception\ForbiddenException When
  90. * - $_manageTable is not defined.
  91. * - trait is used in non-controller classes.
  92. * - the controller is not a backend controller.
  93. */
  94. public function beforeFilter(Event $event)
  95. {
  96. $requestParams = $event->subject()->request->params;
  97. if (empty($this->_manageTable) ||
  98. !($this->_table = TableRegistry::get($this->_manageTable))
  99. ) {
  100. throw new ForbiddenException(__d('field', 'FieldUIControllerTrait: The property $_manageTable was not found or is empty.'));
  101. } elseif (!($this instanceof Controller)) {
  102. throw new ForbiddenException(__d('field', 'FieldUIControllerTrait: This trait must be used on instances of Cake\Controller\Controller.'));
  103. } elseif (!isset($requestParams['prefix']) || strtolower($requestParams['prefix']) !== 'admin') {
  104. throw new ForbiddenException(__d('field', 'FieldUIControllerTrait: This trait must be used on backend-controllers only.'));
  105. }
  106. $this->_tableAlias = Inflector::underscore($this->_table->alias());
  107. }
  108. /**
  109. * Fallback for template location when extending Field UI API.
  110. *
  111. * If controller tries to render an unexisting template under its Template
  112. * directory, then we try to find that view under `Field/Template/FieldUI`
  113. * directory.
  114. *
  115. * ### Example:
  116. *
  117. * Suppose you are using this trait to manage fields attached to `Persons`
  118. * entities. You would probably have a `Person` plugin and a `clean` controller
  119. * as follow:
  120. *
  121. * ```
  122. * // http://example.com/admin/person/fields_manager
  123. * Person\Controller\FieldsManagerController::index()
  124. * ```
  125. *
  126. * The above controller action will try to render
  127. * `/plugins/Person/Template/FieldsManager/index.ctp`. But if does not exists
  128. * then `<QuickAppsCorePath>/plugins/Field/Template/FieldUI/index.ctp`
  129. * will be used instead.
  130. *
  131. * Of course you may create your own template and skip this fallback
  132. * functionality.
  133. *
  134. * @param \Cake\Event\Event $event the event instance.
  135. * @return void
  136. */
  137. public function beforeRender(Event $event)
  138. {
  139. $plugin = Inflector::camelize($event->subject()->request->params['plugin']);
  140. $controller = Inflector::camelize($event->subject()->request->params['controller']);
  141. $action = Inflector::underscore($event->subject()->request->params['action']);
  142. $templatePath = Plugin::classPath($plugin) . "Template/{$controller}/{$action}.ctp";
  143. if (!is_readable($templatePath)) {
  144. $alternativeTemplatePath = Plugin::classPath('Field') . 'Template/FieldUI';
  145. if (is_readable("{$alternativeTemplatePath}/{$action}.ctp")) {
  146. $this->plugin = 'Field';
  147. $this->viewBuilder()->templatePath('FieldUI');
  148. }
  149. }
  150. parent::beforeRender($event);
  151. }
  152. /**
  153. * Field UI main action.
  154. *
  155. * Shows all the fields attached to the Table being managed.
  156. *
  157. * @return void
  158. */
  159. public function index()
  160. {
  161. $this->loadModel('Field.FieldInstances');
  162. $instances = $this->_getInstances();
  163. if (count($instances) == 0) {
  164. $this->Flash->warning(__d('field', 'There are no field attached yet.'));
  165. }
  166. $this->title(__d('field', 'Fields List'));
  167. $this->set('instances', $instances);
  168. }
  169. /**
  170. * Handles a single field instance configuration parameters.
  171. *
  172. * In FormHelper, all fields prefixed with `_` will be considered as columns
  173. * values of the instance being edited. Any other input element will be
  174. * considered as part of the `settings` column.
  175. *
  176. * For example: `_label`, `_required` and `description` maps to `label`,
  177. * `required` and `description`. And `some_input`, `another_input` maps to
  178. * `settings.some_input`, `settings.another_input`
  179. *
  180. * @param int $id The field instance ID to manage
  181. * @return void
  182. * @throws \Cake\ORM\Exception\RecordNotFoundException When no field instance
  183. * was found
  184. */
  185. public function configure($id)
  186. {
  187. $instance = $this->_getOrThrow($id, ['locked' => false]);
  188. $arrayContext = [
  189. 'schema' => [],
  190. 'defaults' => [],
  191. 'errors' => [],
  192. ];
  193. if ($this->request->data()) {
  194. $instance->accessible('*', true);
  195. $instance->accessible(['id', 'eav_attribute', 'handler', 'ordering'], false);
  196. foreach ($this->request->data as $k => $v) {
  197. if (str_starts_with($k, '_')) {
  198. $instance->set(str_replace_once('_', '', $k), $v);
  199. unset($this->request->data[$k]);
  200. }
  201. }
  202. $validator = $this->FieldInstances->validator('settings');
  203. $instance->validateSettings($this->request->data(), $validator);
  204. $errors = $validator->errors($this->request->data(), false);
  205. if (empty($errors)) {
  206. $instance->set('settings', $this->request->data());
  207. $save = $this->FieldInstances->save($instance);
  208. if ($save) {
  209. $this->Flash->success(__d('field', 'Field information was saved.'));
  210. $this->redirect($this->referer());
  211. } else {
  212. $this->Flash->danger(__d('field', 'Your information could not be saved.'));
  213. }
  214. } else {
  215. $this->Flash->danger(__d('field', 'Field settings could not be saved.'));
  216. foreach ($errors as $field => $message) {
  217. $arrayContext['errors'][$field] = $message;
  218. }
  219. }
  220. } else {
  221. $arrayContext['defaults'] = (array)$instance->settings;
  222. $this->request->data = $arrayContext['defaults'];
  223. }
  224. $this->title(__d('field', 'Configure Field'));
  225. $this->set(compact('arrayContext', 'instance'));
  226. }
  227. /**
  228. * Attach action.
  229. *
  230. * Attaches a new Field to the table being managed.
  231. *
  232. * @return void
  233. */
  234. public function attach()
  235. {
  236. $this->loadModel('Field.FieldInstances');
  237. if ($this->request->data()) {
  238. $handler = $this->request->data('handler');
  239. $info = fieldsInfo($handler);
  240. $type = !empty($info['type']) ? $info['type'] : null;
  241. $data = $this->request->data();
  242. $data['eav_attribute'] = array_merge([
  243. 'table_alias' => $this->_tableAlias,
  244. 'bundle' => $this->_getBundle(),
  245. 'type' => $type,
  246. ], (array)$this->request->data('eav_attribute'));
  247. $fieldInstance = $this->FieldInstances->newEntity($data, ['associated' => ['EavAttribute']]);
  248. $this->_validateSlug($fieldInstance);
  249. $instanceErrors = $fieldInstance->errors();
  250. $attributeErrors = $fieldInstance->get('eav_attribute')->errors();
  251. $success = empty($instanceErrors) && empty($attributeErrors);
  252. if ($success) {
  253. $success = $this->FieldInstances->save($fieldInstance, ['associated' => ['EavAttribute']]);
  254. if ($success) {
  255. $this->Flash->success(__d('field', 'Field attached!'));
  256. $this->redirect($this->referer());
  257. }
  258. }
  259. if (!$success) {
  260. $this->Flash->danger(__d('field', 'Field could not be attached'));
  261. }
  262. } else {
  263. $fieldInstance = $this->FieldInstances->newEntity();
  264. }
  265. $fieldsInfoCollection = fieldsInfo();
  266. $fieldsList = $fieldsInfoCollection->combine('handler', 'name')->toArray(); // for form select
  267. $fieldsInfo = $fieldsInfoCollection->toArray(); // for help-blocks
  268. $this->title(__d('field', 'Attach New Field'));
  269. $this->set('fieldsList', $fieldsList);
  270. $this->set('fieldsInfo', $fieldsInfo);
  271. $this->set('fieldInstance', $fieldInstance);
  272. }
  273. /**
  274. * Detach action.
  275. *
  276. * Detaches a Field from table being managed.
  277. *
  278. * @param int $id ID of the instance to detach
  279. * @return void
  280. */
  281. public function detach($id)
  282. {
  283. $instance = $this->_getOrThrow($id, ['locked' => false]);
  284. $this->loadModel('Field.FieldInstances');
  285. if ($this->FieldInstances->delete($instance)) {
  286. $this->Flash->success(__d('field', 'Field detached successfully!'));
  287. } else {
  288. $this->Flash->danger(__d('field', 'Field could not be detached'));
  289. }
  290. $this->title(__d('field', 'Detach Field'));
  291. $this->redirect($this->referer());
  292. }
  293. /**
  294. * View modes.
  295. *
  296. * Shows the list of fields for corresponding view mode.
  297. *
  298. * @param string $viewMode View mode slug. e.g. `rss` or `default`
  299. * @return void
  300. * @throws \Cake\Network\Exception\NotFoundException When given view mode
  301. * does not exists
  302. */
  303. public function viewModeList($viewMode)
  304. {
  305. $this->_validateViewMode($viewMode);
  306. $this->loadModel('Field.FieldInstances');
  307. $instances = $this->_getInstances();
  308. if (count($instances) === 0) {
  309. $this->Flash->warning(__d('field', 'There are no field attached yet.'));
  310. } else {
  311. $instances = $instances->sortBy(function ($fieldInstance) use ($viewMode) {
  312. if (isset($fieldInstance->view_modes[$viewMode]['ordering'])) {
  313. return $fieldInstance->view_modes[$viewMode]['ordering'];
  314. }
  315. return 0;
  316. }, SORT_ASC);
  317. }
  318. $this->title(__d('field', 'View Modes'));
  319. $this->set('instances', $instances);
  320. $this->set('viewMode', $viewMode);
  321. $this->set('viewModeInfo', $this->viewModes($viewMode));
  322. }
  323. /**
  324. * Handles field instance rendering settings for a particular view mode.
  325. *
  326. * @param string $viewMode View mode slug
  327. * @param int $id The field instance ID to manage
  328. * @return void
  329. * @throws \Cake\ORM\Exception\RecordNotFoundException When no field
  330. * instance was found
  331. * @throws \Cake\Network\Exception\NotFoundException When given view
  332. * mode does not exists
  333. */
  334. public function viewModeEdit($viewMode, $id)
  335. {
  336. $this->_validateViewMode($viewMode);
  337. $instance = $this->_getOrThrow($id);
  338. $arrayContext = [
  339. 'schema' => [
  340. 'label_visibility' => ['type' => 'string'],
  341. 'shortcodes' => ['type' => 'boolean'],
  342. 'hidden' => ['type' => 'boolean'],
  343. ],
  344. 'defaults' => [
  345. 'label_visibility' => 'hidden',
  346. 'shortcodes' => false,
  347. 'hidden' => false,
  348. ],
  349. 'errors' => []
  350. ];
  351. $viewModeInfo = $this->viewModes($viewMode);
  352. if ($this->request->data()) {
  353. $validator = $this->FieldInstances->validator('viewMode');
  354. $instance->validateViewModeSettings($this->request->data(), $validator, $viewMode);
  355. $errors = $validator->errors($this->request->data(), false);
  356. if (empty($errors)) {
  357. $instance->accessible('*', true);
  358. $viewModes = $instance->get('view_modes');
  359. $viewModes[$viewMode] = array_merge($viewModes[$viewMode], $this->request->data());
  360. $instance->set('view_modes', $viewModes);
  361. if ($this->FieldInstances->save($instance)) {
  362. $this->Flash->success(__d('field', 'Field information was saved.'));
  363. $this->redirect($this->referer());
  364. } else {
  365. $this->Flash->danger(__d('field', 'Your information could not be saved.'));
  366. }
  367. } else {
  368. $this->Flash->danger(__d('field', 'View mode settings could not be saved.'));
  369. foreach ($errors as $field => $message) {
  370. $arrayContext['errors'][$field] = $message;
  371. }
  372. }
  373. } else {
  374. $arrayContext['defaults'] = (array)$instance->view_modes[$viewMode];
  375. $this->request->data = $arrayContext['defaults'];
  376. }
  377. $this->title(__d('field', 'Configure View Mode'));
  378. $instance->accessible('settings', true);
  379. $this->set(compact('arrayContext', 'viewMode', 'viewModeInfo', 'instance'));
  380. }
  381. /**
  382. * Moves a field up or down within a view mode.
  383. *
  384. * The ordering indicates the position they are displayed when entities are
  385. * rendered in a specific view mode.
  386. *
  387. * @param string $viewMode View mode slug
  388. * @param int $id Field instance id
  389. * @param string $direction Direction, 'up' or 'down'
  390. * @return void Redirects to previous page
  391. * @throws \Cake\ORM\Exception\RecordNotFoundException When no field
  392. * instance was found
  393. * @throws \Cake\Network\Exception\NotFoundException When given view mode
  394. * does not exists
  395. */
  396. public function viewModeMove($viewMode, $id, $direction)
  397. {
  398. $this->_validateViewMode($viewMode);
  399. $instance = $this->_getOrThrow($id);
  400. $unordered = [];
  401. $position = false;
  402. $k = 0;
  403. $list = $this->_getInstances()
  404. ->sortBy(function ($fieldInstance) use ($viewMode) {
  405. if (isset($fieldInstance->view_modes[$viewMode]['ordering'])) {
  406. return $fieldInstance->view_modes[$viewMode]['ordering'];
  407. }
  408. return 0;
  409. }, SORT_ASC);
  410. foreach ($list as $field) {
  411. if ($field->id === $instance->id) {
  412. $position = $k;
  413. }
  414. $unordered[] = $field;
  415. $k++;
  416. }
  417. if ($position !== false) {
  418. $ordered = array_move($unordered, $position, $direction);
  419. $before = md5(serialize($unordered));
  420. $after = md5(serialize($ordered));
  421. if ($before != $after) {
  422. foreach ($ordered as $k => $field) {
  423. $viewModes = $field->view_modes;
  424. $viewModes[$viewMode]['ordering'] = $k;
  425. $field->set('view_modes', $viewModes);
  426. $this->FieldInstances->save($field);
  427. }
  428. }
  429. }
  430. $this->title(__d('field', 'Change Field Order'));
  431. $this->redirect($this->referer());
  432. }
  433. /**
  434. * Moves a field up or down.
  435. *
  436. * The ordering indicates the position they are displayed on entity's
  437. * editing form.
  438. *
  439. * @param int $id Field instance id
  440. * @param string $direction Direction, 'up' or 'down'
  441. * @return void Redirects to previous page
  442. */
  443. public function move($id, $direction)
  444. {
  445. $instance = $this->_getOrThrow($id);
  446. $unordered = [];
  447. $direction = !in_array($direction, ['up', 'down']) ? 'up' : $direction;
  448. $position = false;
  449. $list = $this->_getInstances();
  450. foreach ($list as $k => $field) {
  451. if ($field->id === $instance->id) {
  452. $position = $k;
  453. }
  454. $unordered[] = $field;
  455. }
  456. if ($position !== false) {
  457. $ordered = array_move($unordered, $position, $direction);
  458. $before = md5(serialize($unordered));
  459. $after = md5(serialize($ordered));
  460. if ($before != $after) {
  461. foreach ($ordered as $k => $field) {
  462. $field->set('ordering', $k);
  463. $this->FieldInstances->save($field);
  464. }
  465. }
  466. }
  467. $this->title(__d('field', 'Reorder Field'));
  468. $this->redirect($this->referer());
  469. }
  470. /**
  471. * Returns all field instances attached to the table being managed.
  472. *
  473. * @return \Cake\Datasource\ResultSetInterface
  474. */
  475. protected function _getInstances()
  476. {
  477. $conditions = ['EavAttribute.table_alias' => $this->_tableAlias];
  478. if (!empty($this->_bundle)) {
  479. $conditions['EavAttribute.bundle'] = $this->_bundle;
  480. }
  481. $this->loadModel('Field.FieldInstances');
  482. return $this->FieldInstances
  483. ->find()
  484. ->contain(['EavAttribute'])
  485. ->where($conditions)
  486. ->order(['FieldInstances.ordering' => 'ASC'])
  487. ->all();
  488. }
  489. /**
  490. * Checks that the given instance's slug do not collide with table's real column
  491. * names.
  492. *
  493. * If collision occurs, an error message will be registered on the given entity.
  494. *
  495. * @param \Field\Model\Entity\FieldInstance $instance Instance to validate
  496. * @return void
  497. */
  498. protected function _validateSlug($instance)
  499. {
  500. $slug = $instance->get('eav_attribute')->get('name');
  501. $columns = $this->_table->schema()->columns();
  502. if (in_array($slug, $columns)) {
  503. $instance->get('eav_attribute')->errors('name', __d('field', 'The name "{0}" cannot be used as it collides with table column names.', $slug));
  504. }
  505. }
  506. /**
  507. * Gets bundle name.
  508. *
  509. * @return string|null
  510. */
  511. protected function _getBundle()
  512. {
  513. if (!empty($this->_bundle)) {
  514. return $this->_bundle;
  515. }
  516. return null;
  517. }
  518. /**
  519. * Gets the given field instance by ID or throw if not exists.
  520. *
  521. * @param int $id Field instance ID
  522. * @param array $conditions Additional conditions for the WHERE query
  523. * @return \Field\Model\Entity\FieldInstance The instance
  524. * @throws \Cake\ORM\Exception\RecordNotFoundException When instance
  525. * was not found
  526. */
  527. protected function _getOrThrow($id, $conditions = [])
  528. {
  529. $this->loadModel('Field.FieldInstances');
  530. $conditions = array_merge(['id' => $id], $conditions);
  531. $instance = $this->FieldInstances
  532. ->find()
  533. ->where($conditions)
  534. ->limit(1)
  535. ->first();
  536. if (!$instance) {
  537. throw new RecordNotFoundException(__d('field', 'The requested field does not exists.'));
  538. }
  539. return $instance;
  540. }
  541. /**
  542. * Throws if the given view modes does not exists.
  543. *
  544. * @param string $viewMode The view mode to validate
  545. * @return void
  546. * @throws \Cake\Network\Exception\NotFoundException When given view mode
  547. * does not exists
  548. */
  549. protected function _validateViewMode($viewMode)
  550. {
  551. if (!in_array($viewMode, $this->viewModes())) {
  552. throw new NotFoundException(__d('field', 'The requested view mode does not exists.'));
  553. }
  554. }
  555. }