FlexForm.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610
  1. <?php
  2. /**
  3. * @package Grav\Framework\Flex
  4. *
  5. * @copyright Copyright (c) 2015 - 2022 Trilby Media, LLC. All rights reserved.
  6. * @license MIT License; see LICENSE file for details.
  7. */
  8. namespace Grav\Framework\Flex;
  9. use ArrayAccess;
  10. use Exception;
  11. use Grav\Common\Data\Blueprint;
  12. use Grav\Common\Data\Data;
  13. use Grav\Common\Grav;
  14. use Grav\Common\Twig\Twig;
  15. use Grav\Common\Utils;
  16. use Grav\Framework\Flex\Interfaces\FlexFormInterface;
  17. use Grav\Framework\Flex\Interfaces\FlexObjectFormInterface;
  18. use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
  19. use Grav\Framework\Form\Interfaces\FormFlashInterface;
  20. use Grav\Framework\Form\Traits\FormTrait;
  21. use Grav\Framework\Route\Route;
  22. use JsonSerializable;
  23. use RocketTheme\Toolbox\ArrayTraits\NestedArrayAccessWithGetters;
  24. use RuntimeException;
  25. use Twig\Error\LoaderError;
  26. use Twig\Error\SyntaxError;
  27. use Twig\Template;
  28. use Twig\TemplateWrapper;
  29. /**
  30. * Class FlexForm
  31. * @package Grav\Framework\Flex
  32. */
  33. class FlexForm implements FlexObjectFormInterface, JsonSerializable
  34. {
  35. use NestedArrayAccessWithGetters {
  36. NestedArrayAccessWithGetters::get as private traitGet;
  37. NestedArrayAccessWithGetters::set as private traitSet;
  38. }
  39. use FormTrait {
  40. FormTrait::doSerialize as doTraitSerialize;
  41. FormTrait::doUnserialize as doTraitUnserialize;
  42. }
  43. /** @var array */
  44. private $items = [];
  45. /** @var array|null */
  46. private $form;
  47. /** @var FlexObjectInterface */
  48. private $object;
  49. /** @var string */
  50. private $flexName;
  51. /** @var callable|null */
  52. private $submitMethod;
  53. /**
  54. * @param array $options Options to initialize the form instance:
  55. * (string) name: Form name, allows you to use custom form.
  56. * (string) unique_id: Unique id for this form instance.
  57. * (array) form: Custom form fields.
  58. * (FlexObjectInterface) object: Object instance.
  59. * (string) key: Object key, used only if object instance isn't given.
  60. * (FlexDirectory) directory: Flex Directory, mandatory if object isn't given.
  61. *
  62. * @return FlexFormInterface
  63. */
  64. public static function instance(array $options = [])
  65. {
  66. if (isset($options['object'])) {
  67. $object = $options['object'];
  68. if (!$object instanceof FlexObjectInterface) {
  69. throw new RuntimeException(__METHOD__ . "(): 'object' should be instance of FlexObjectInterface", 400);
  70. }
  71. } elseif (isset($options['directory'])) {
  72. $directory = $options['directory'];
  73. if (!$directory instanceof FlexDirectory) {
  74. throw new RuntimeException(__METHOD__ . "(): 'directory' should be instance of FlexDirectory", 400);
  75. }
  76. $key = $options['key'] ?? '';
  77. $object = $directory->getObject($key) ?? $directory->createObject([], $key);
  78. } else {
  79. throw new RuntimeException(__METHOD__ . "(): You need to pass option 'directory' or 'object'", 400);
  80. }
  81. $name = $options['name'] ?? '';
  82. // There is no reason to pass object and directory.
  83. unset($options['object'], $options['directory']);
  84. return $object->getForm($name, $options);
  85. }
  86. /**
  87. * FlexForm constructor.
  88. * @param string $name
  89. * @param FlexObjectInterface $object
  90. * @param array|null $options
  91. */
  92. public function __construct(string $name, FlexObjectInterface $object, array $options = null)
  93. {
  94. $this->name = $name;
  95. $this->setObject($object);
  96. if (isset($options['form']['name'])) {
  97. // Use custom form name.
  98. $this->flexName = $options['form']['name'];
  99. } else {
  100. // Use standard form name.
  101. $this->setName($object->getFlexType(), $name);
  102. }
  103. $this->setId($this->getName());
  104. $uniqueId = $options['unique_id'] ?? null;
  105. if (!$uniqueId) {
  106. if ($object->exists()) {
  107. $uniqueId = $object->getStorageKey();
  108. } elseif ($object->hasKey()) {
  109. $uniqueId = "{$object->getKey()}:new";
  110. } else {
  111. $uniqueId = "{$object->getFlexType()}:new";
  112. }
  113. $uniqueId = md5($uniqueId);
  114. }
  115. $this->setUniqueId($uniqueId);
  116. $directory = $object->getFlexDirectory();
  117. $this->setFlashLookupFolder($options['flash_folder'] ?? $directory->getBlueprint()->get('form/flash_folder') ?? 'tmp://forms/[SESSIONID]');
  118. $this->form = $options['form'] ?? null;
  119. if (Utils::isPositive($this->items['disabled'] ?? $this->form['disabled'] ?? false)) {
  120. $this->disable();
  121. }
  122. if (!empty($options['reset'])) {
  123. $this->getFlash()->delete();
  124. }
  125. $this->initialize();
  126. }
  127. /**
  128. * @return $this
  129. */
  130. public function initialize()
  131. {
  132. $this->messages = [];
  133. $this->submitted = false;
  134. $this->data = null;
  135. $this->files = [];
  136. $this->unsetFlash();
  137. /** @var FlexFormFlash $flash */
  138. $flash = $this->getFlash();
  139. if ($flash->exists()) {
  140. $data = $flash->getData();
  141. if (null !== $data) {
  142. $data = new Data($data, $this->getBlueprint());
  143. $data->setKeepEmptyValues(true);
  144. $data->setMissingValuesAsNull(true);
  145. }
  146. $object = $flash->getObject();
  147. if (null === $object) {
  148. throw new RuntimeException('Flash has no object');
  149. }
  150. $this->object = $object;
  151. $this->data = $data;
  152. $includeOriginal = (bool)($this->getBlueprint()->form()['images']['original'] ?? null);
  153. $this->files = $flash->getFilesByFields($includeOriginal);
  154. }
  155. return $this;
  156. }
  157. /**
  158. * @param string $uniqueId
  159. * @return void
  160. */
  161. public function setUniqueId(string $uniqueId): void
  162. {
  163. if ($uniqueId !== '') {
  164. $this->uniqueid = $uniqueId;
  165. }
  166. }
  167. /**
  168. * @param string $name
  169. * @param mixed $default
  170. * @param string|null $separator
  171. * @return mixed
  172. */
  173. public function get($name, $default = null, $separator = null)
  174. {
  175. switch (strtolower($name)) {
  176. case 'id':
  177. case 'uniqueid':
  178. case 'name':
  179. case 'noncename':
  180. case 'nonceaction':
  181. case 'action':
  182. case 'data':
  183. case 'files':
  184. case 'errors';
  185. case 'fields':
  186. case 'blueprint':
  187. case 'page':
  188. $method = 'get' . $name;
  189. return $this->{$method}();
  190. }
  191. return $this->traitGet($name, $default, $separator);
  192. }
  193. /**
  194. * @param string $name
  195. * @param mixed $value
  196. * @param string|null $separator
  197. * @return FlexForm
  198. */
  199. public function set($name, $value, $separator = null)
  200. {
  201. switch (strtolower($name)) {
  202. case 'id':
  203. case 'uniqueid':
  204. $method = 'set' . $name;
  205. return $this->{$method}();
  206. }
  207. return $this->traitSet($name, $value, $separator);
  208. }
  209. /**
  210. * @return string
  211. */
  212. public function getName(): string
  213. {
  214. return $this->flexName;
  215. }
  216. /**
  217. * @param callable|null $submitMethod
  218. */
  219. public function setSubmitMethod(?callable $submitMethod): void
  220. {
  221. $this->submitMethod = $submitMethod;
  222. }
  223. /**
  224. * @param string $type
  225. * @param string $name
  226. */
  227. protected function setName(string $type, string $name): void
  228. {
  229. // Make sure that both type and name do not have dash (convert dashes to underscores).
  230. $type = str_replace('-', '_', $type);
  231. $name = str_replace('-', '_', $name);
  232. $this->flexName = $name ? "flex-{$type}-{$name}" : "flex-{$type}";
  233. }
  234. /**
  235. * @return Data|FlexObjectInterface|object
  236. */
  237. public function getData()
  238. {
  239. return $this->data ?? $this->getObject();
  240. }
  241. /**
  242. * Get a value from the form.
  243. *
  244. * Note: Used in form fields.
  245. *
  246. * @param string $name
  247. * @return mixed
  248. */
  249. public function getValue(string $name)
  250. {
  251. // Attempt to get value from the form data.
  252. $value = $this->data ? $this->data[$name] : null;
  253. // Return the form data or fall back to the object property.
  254. return $value ?? $this->getObject()->getFormValue($name);
  255. }
  256. /**
  257. * @param string $name
  258. * @return array|mixed|null
  259. */
  260. public function getDefaultValue(string $name)
  261. {
  262. return $this->object->getDefaultValue($name);
  263. }
  264. /**
  265. * @return array
  266. */
  267. public function getDefaultValues(): array
  268. {
  269. return $this->object->getDefaultValues();
  270. }
  271. /**
  272. * @return string
  273. */
  274. public function getFlexType(): string
  275. {
  276. return $this->object->getFlexType();
  277. }
  278. /**
  279. * Get form flash object.
  280. *
  281. * @return FormFlashInterface|FlexFormFlash
  282. */
  283. public function getFlash()
  284. {
  285. if (null === $this->flash) {
  286. $grav = Grav::instance();
  287. $config = [
  288. 'session_id' => $this->getSessionId(),
  289. 'unique_id' => $this->getUniqueId(),
  290. 'form_name' => $this->getName(),
  291. 'folder' => $this->getFlashFolder(),
  292. 'id' => $this->getFlashId(),
  293. 'object' => $this->getObject()
  294. ];
  295. $this->flash = new FlexFormFlash($config);
  296. $this->flash
  297. ->setUrl($grav['uri']->url)
  298. ->setUser($grav['user'] ?? null);
  299. }
  300. return $this->flash;
  301. }
  302. /**
  303. * @return FlexObjectInterface
  304. */
  305. public function getObject(): FlexObjectInterface
  306. {
  307. return $this->object;
  308. }
  309. /**
  310. * @return FlexObjectInterface
  311. */
  312. public function updateObject(): FlexObjectInterface
  313. {
  314. $data = $this->data instanceof Data ? $this->data->toArray() : [];
  315. $files = $this->files;
  316. return $this->getObject()->update($data, $files);
  317. }
  318. /**
  319. * @return Blueprint
  320. */
  321. public function getBlueprint(): Blueprint
  322. {
  323. if (null === $this->blueprint) {
  324. try {
  325. $blueprint = $this->getObject()->getBlueprint($this->name);
  326. if ($this->form) {
  327. // We have field overrides available.
  328. $blueprint->extend(['form' => $this->form], true);
  329. $blueprint->init();
  330. }
  331. } catch (RuntimeException $e) {
  332. if (!isset($this->form['fields'])) {
  333. throw $e;
  334. }
  335. // Blueprint is not defined, but we have custom form fields available.
  336. $blueprint = new Blueprint(null, ['form' => $this->form]);
  337. $blueprint->load();
  338. $blueprint->setScope('object');
  339. $blueprint->init();
  340. }
  341. $this->blueprint = $blueprint;
  342. }
  343. return $this->blueprint;
  344. }
  345. /**
  346. * @return Route|null
  347. */
  348. public function getFileUploadAjaxRoute(): ?Route
  349. {
  350. $object = $this->getObject();
  351. if (!method_exists($object, 'route')) {
  352. /** @var Route $route */
  353. $route = Grav::instance()['route'];
  354. return $route->withExtension('json')->withGravParam('task', 'media.upload');
  355. }
  356. return $object->route('/edit.json/task:media.upload');
  357. }
  358. /**
  359. * @param string|null $field
  360. * @param string|null $filename
  361. * @return Route|null
  362. */
  363. public function getFileDeleteAjaxRoute($field = null, $filename = null): ?Route
  364. {
  365. $object = $this->getObject();
  366. if (!method_exists($object, 'route')) {
  367. /** @var Route $route */
  368. $route = Grav::instance()['route'];
  369. return $route->withExtension('json')->withGravParam('task', 'media.delete');
  370. }
  371. return $object->route('/edit.json/task:media.delete');
  372. }
  373. /**
  374. * @param array $params
  375. * @param string|null $extension
  376. * @return string
  377. */
  378. public function getMediaTaskRoute(array $params = [], string $extension = null): string
  379. {
  380. $grav = Grav::instance();
  381. /** @var Flex $flex */
  382. $flex = $grav['flex_objects'];
  383. if (method_exists($flex, 'adminRoute')) {
  384. return $flex->adminRoute($this->getObject(), $params, $extension ?? 'json');
  385. }
  386. return '';
  387. }
  388. /**
  389. * @param string $name
  390. * @return mixed|null
  391. */
  392. #[\ReturnTypeWillChange]
  393. public function __get($name)
  394. {
  395. $method = "get{$name}";
  396. if (method_exists($this, $method)) {
  397. return $this->{$method}();
  398. }
  399. $form = $this->getBlueprint()->form();
  400. return $form[$name] ?? null;
  401. }
  402. /**
  403. * @param string $name
  404. * @param mixed $value
  405. * @return void
  406. */
  407. #[\ReturnTypeWillChange]
  408. public function __set($name, $value)
  409. {
  410. $method = "set{$name}";
  411. if (method_exists($this, $method)) {
  412. $this->{$method}($value);
  413. }
  414. }
  415. /**
  416. * @param string $name
  417. * @return bool
  418. */
  419. #[\ReturnTypeWillChange]
  420. public function __isset($name)
  421. {
  422. $method = "get{$name}";
  423. if (method_exists($this, $method)) {
  424. return true;
  425. }
  426. $form = $this->getBlueprint()->form();
  427. return isset($form[$name]);
  428. }
  429. /**
  430. * @param string $name
  431. * @return void
  432. */
  433. #[\ReturnTypeWillChange]
  434. public function __unset($name)
  435. {
  436. }
  437. /**
  438. * @return array|bool
  439. */
  440. protected function getUnserializeAllowedClasses()
  441. {
  442. return [FlexObject::class];
  443. }
  444. /**
  445. * Note: this method clones the object.
  446. *
  447. * @param FlexObjectInterface $object
  448. * @return $this
  449. */
  450. protected function setObject(FlexObjectInterface $object): self
  451. {
  452. $this->object = clone $object;
  453. return $this;
  454. }
  455. /**
  456. * @param string $layout
  457. * @return Template|TemplateWrapper
  458. * @throws LoaderError
  459. * @throws SyntaxError
  460. */
  461. protected function getTemplate($layout)
  462. {
  463. $grav = Grav::instance();
  464. /** @var Twig $twig */
  465. $twig = $grav['twig'];
  466. return $twig->twig()->resolveTemplate(
  467. [
  468. "flex-objects/layouts/{$this->getFlexType()}/form/{$layout}.html.twig",
  469. "flex-objects/layouts/_default/form/{$layout}.html.twig",
  470. "forms/{$layout}/form.html.twig",
  471. 'forms/default/form.html.twig'
  472. ]
  473. );
  474. }
  475. /**
  476. * @param array $data
  477. * @param array $files
  478. * @return void
  479. * @throws Exception
  480. */
  481. protected function doSubmit(array $data, array $files)
  482. {
  483. /** @var FlexObject $object */
  484. $object = clone $this->getObject();
  485. $method = $this->submitMethod;
  486. if ($method) {
  487. $method($data, $files, $object);
  488. } else {
  489. $object->update($data, $files);
  490. $object->save();
  491. }
  492. $this->setObject($object);
  493. $this->reset();
  494. }
  495. /**
  496. * @return array
  497. */
  498. protected function doSerialize(): array
  499. {
  500. return $this->doTraitSerialize() + [
  501. 'items' => $this->items,
  502. 'form' => $this->form,
  503. 'object' => $this->object,
  504. 'flexName' => $this->flexName,
  505. 'submitMethod' => $this->submitMethod,
  506. ];
  507. }
  508. /**
  509. * @param array $data
  510. * @return void
  511. */
  512. protected function doUnserialize(array $data): void
  513. {
  514. $this->doTraitUnserialize($data);
  515. $this->items = $data['items'] ?? null;
  516. $this->form = $data['form'] ?? null;
  517. $this->object = $data['object'] ?? null;
  518. $this->flexName = $data['flexName'] ?? null;
  519. $this->submitMethod = $data['submitMethod'] ?? null;
  520. }
  521. /**
  522. * Filter validated data.
  523. *
  524. * @param ArrayAccess|Data|null $data
  525. * @return void
  526. * @phpstan-param ArrayAccess<string,mixed>|Data|null $data
  527. */
  528. protected function filterData($data = null): void
  529. {
  530. if ($data instanceof Data) {
  531. $data->filter(true, true);
  532. }
  533. }
  534. }