FlexDirectoryForm.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509
  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\FlexDirectoryFormInterface;
  17. use Grav\Framework\Flex\Interfaces\FlexFormInterface;
  18. use Grav\Framework\Form\Interfaces\FormFlashInterface;
  19. use Grav\Framework\Form\Traits\FormTrait;
  20. use Grav\Framework\Route\Route;
  21. use JsonSerializable;
  22. use RocketTheme\Toolbox\ArrayTraits\NestedArrayAccessWithGetters;
  23. use RuntimeException;
  24. use Twig\Error\LoaderError;
  25. use Twig\Error\SyntaxError;
  26. use Twig\Template;
  27. use Twig\TemplateWrapper;
  28. /**
  29. * Class FlexForm
  30. * @package Grav\Framework\Flex
  31. */
  32. class FlexDirectoryForm implements FlexDirectoryFormInterface, JsonSerializable
  33. {
  34. use NestedArrayAccessWithGetters {
  35. NestedArrayAccessWithGetters::get as private traitGet;
  36. NestedArrayAccessWithGetters::set as private traitSet;
  37. }
  38. use FormTrait {
  39. FormTrait::doSerialize as doTraitSerialize;
  40. FormTrait::doUnserialize as doTraitUnserialize;
  41. }
  42. /** @var array|null */
  43. private $form;
  44. /** @var FlexDirectory */
  45. private $directory;
  46. /** @var string */
  47. private $flexName;
  48. /**
  49. * @param array $options Options to initialize the form instance:
  50. * (string) name: Form name, allows you to use custom form.
  51. * (string) unique_id: Unique id for this form instance.
  52. * (array) form: Custom form fields.
  53. * (FlexDirectory) directory: Flex Directory, mandatory.
  54. *
  55. * @return FlexFormInterface
  56. */
  57. public static function instance(array $options = []): FlexFormInterface
  58. {
  59. if (isset($options['directory'])) {
  60. $directory = $options['directory'];
  61. if (!$directory instanceof FlexDirectory) {
  62. throw new RuntimeException(__METHOD__ . "(): 'directory' should be instance of FlexDirectory", 400);
  63. }
  64. unset($options['directory']);
  65. } else {
  66. throw new RuntimeException(__METHOD__ . "(): You need to pass option 'directory'", 400);
  67. }
  68. $name = $options['name'] ?? '';
  69. return $directory->getDirectoryForm($name, $options);
  70. }
  71. /**
  72. * FlexForm constructor.
  73. * @param string $name
  74. * @param FlexDirectory $directory
  75. * @param array|null $options
  76. */
  77. public function __construct(string $name, FlexDirectory $directory, array $options = null)
  78. {
  79. $this->name = $name;
  80. $this->setDirectory($directory);
  81. $this->setName($directory->getFlexType(), $name);
  82. $this->setId($this->getName());
  83. $uniqueId = $options['unique_id'] ?? null;
  84. if (!$uniqueId) {
  85. $uniqueId = md5($directory->getFlexType() . '-directory-' . $this->name);
  86. }
  87. $this->setUniqueId($uniqueId);
  88. $this->setFlashLookupFolder($directory->getDirectoryBlueprint()->get('form/flash_folder') ?? 'tmp://forms/[SESSIONID]');
  89. $this->form = $options['form'] ?? null;
  90. if (Utils::isPositive($this->form['disabled'] ?? false)) {
  91. $this->disable();
  92. }
  93. $this->initialize();
  94. }
  95. /**
  96. * @return $this
  97. */
  98. public function initialize()
  99. {
  100. $this->messages = [];
  101. $this->submitted = false;
  102. $this->data = new Data($this->directory->loadDirectoryConfig($this->name), $this->getBlueprint());
  103. $this->files = [];
  104. $this->unsetFlash();
  105. /** @var FlexFormFlash $flash */
  106. $flash = $this->getFlash();
  107. if ($flash->exists()) {
  108. $data = $flash->getData();
  109. $includeOriginal = (bool)($this->getBlueprint()->form()['images']['original'] ?? null);
  110. $directory = $flash->getDirectory();
  111. if (null === $directory) {
  112. throw new RuntimeException('Flash has no directory');
  113. }
  114. $this->directory = $directory;
  115. $this->data = $data ? new Data($data, $this->getBlueprint()) : null;
  116. $this->files = $flash->getFilesByFields($includeOriginal);
  117. }
  118. return $this;
  119. }
  120. /**
  121. * @param string $uniqueId
  122. * @return void
  123. */
  124. public function setUniqueId(string $uniqueId): void
  125. {
  126. if ($uniqueId !== '') {
  127. $this->uniqueid = $uniqueId;
  128. }
  129. }
  130. /**
  131. * @param string $name
  132. * @param mixed $default
  133. * @param string|null $separator
  134. * @return mixed
  135. */
  136. public function get($name, $default = null, $separator = null)
  137. {
  138. switch (strtolower($name)) {
  139. case 'id':
  140. case 'uniqueid':
  141. case 'name':
  142. case 'noncename':
  143. case 'nonceaction':
  144. case 'action':
  145. case 'data':
  146. case 'files':
  147. case 'errors';
  148. case 'fields':
  149. case 'blueprint':
  150. case 'page':
  151. $method = 'get' . $name;
  152. return $this->{$method}();
  153. }
  154. return $this->traitGet($name, $default, $separator);
  155. }
  156. /**
  157. * @param string $name
  158. * @param mixed $value
  159. * @param string|null $separator
  160. * @return $this
  161. */
  162. public function set($name, $value, $separator = null)
  163. {
  164. switch (strtolower($name)) {
  165. case 'id':
  166. case 'uniqueid':
  167. $method = 'set' . $name;
  168. return $this->{$method}();
  169. }
  170. return $this->traitSet($name, $value, $separator);
  171. }
  172. /**
  173. * @return string
  174. */
  175. public function getName(): string
  176. {
  177. return $this->flexName;
  178. }
  179. protected function setName(string $type, string $name): void
  180. {
  181. // Make sure that both type and name do not have dash (convert dashes to underscores).
  182. $type = str_replace('-', '_', $type);
  183. $name = str_replace('-', '_', $name);
  184. $this->flexName = $name ? "flex_conf-{$type}-{$name}" : "flex_conf-{$type}";
  185. }
  186. /**
  187. * @return Data|object
  188. */
  189. public function getData()
  190. {
  191. if (null === $this->data) {
  192. $this->data = new Data([], $this->getBlueprint());
  193. }
  194. return $this->data;
  195. }
  196. /**
  197. * Get a value from the form.
  198. *
  199. * Note: Used in form fields.
  200. *
  201. * @param string $name
  202. * @return mixed
  203. */
  204. public function getValue(string $name)
  205. {
  206. // Attempt to get value from the form data.
  207. $value = $this->data ? $this->data[$name] : null;
  208. // Return the form data or fall back to the object property.
  209. return $value ?? null;
  210. }
  211. /**
  212. * @param string $name
  213. * @return array|mixed|null
  214. */
  215. public function getDefaultValue(string $name)
  216. {
  217. return $this->getBlueprint()->getDefaultValue($name);
  218. }
  219. /**
  220. * @return array
  221. */
  222. public function getDefaultValues(): array
  223. {
  224. return $this->getBlueprint()->getDefaults();
  225. }
  226. /**
  227. * @return string
  228. */
  229. public function getFlexType(): string
  230. {
  231. return $this->directory->getFlexType();
  232. }
  233. /**
  234. * Get form flash object.
  235. *
  236. * @return FormFlashInterface|FlexFormFlash
  237. */
  238. public function getFlash()
  239. {
  240. if (null === $this->flash) {
  241. $grav = Grav::instance();
  242. $config = [
  243. 'session_id' => $this->getSessionId(),
  244. 'unique_id' => $this->getUniqueId(),
  245. 'form_name' => $this->getName(),
  246. 'folder' => $this->getFlashFolder(),
  247. 'id' => $this->getFlashId(),
  248. 'directory' => $this->getDirectory()
  249. ];
  250. $this->flash = new FlexFormFlash($config);
  251. $this->flash
  252. ->setUrl($grav['uri']->url)
  253. ->setUser($grav['user'] ?? null);
  254. }
  255. return $this->flash;
  256. }
  257. /**
  258. * @return FlexDirectory
  259. */
  260. public function getDirectory(): FlexDirectory
  261. {
  262. return $this->directory;
  263. }
  264. /**
  265. * @return Blueprint
  266. */
  267. public function getBlueprint(): Blueprint
  268. {
  269. if (null === $this->blueprint) {
  270. try {
  271. $blueprint = $this->getDirectory()->getDirectoryBlueprint();
  272. if ($this->form) {
  273. // We have field overrides available.
  274. $blueprint->extend(['form' => $this->form], true);
  275. $blueprint->init();
  276. }
  277. } catch (RuntimeException $e) {
  278. if (!isset($this->form['fields'])) {
  279. throw $e;
  280. }
  281. // Blueprint is not defined, but we have custom form fields available.
  282. $blueprint = new Blueprint(null, ['form' => $this->form]);
  283. $blueprint->load();
  284. $blueprint->setScope('directory');
  285. $blueprint->init();
  286. }
  287. $this->blueprint = $blueprint;
  288. }
  289. return $this->blueprint;
  290. }
  291. /**
  292. * @return Route|null
  293. */
  294. public function getFileUploadAjaxRoute(): ?Route
  295. {
  296. return null;
  297. }
  298. /**
  299. * @param string|null $field
  300. * @param string|null $filename
  301. * @return Route|null
  302. */
  303. public function getFileDeleteAjaxRoute($field = null, $filename = null): ?Route
  304. {
  305. return null;
  306. }
  307. /**
  308. * @param array $params
  309. * @param string|null $extension
  310. * @return string
  311. */
  312. public function getMediaTaskRoute(array $params = [], string $extension = null): string
  313. {
  314. return '';
  315. }
  316. /**
  317. * @param string $name
  318. * @return mixed|null
  319. */
  320. #[\ReturnTypeWillChange]
  321. public function __get($name)
  322. {
  323. $method = "get{$name}";
  324. if (method_exists($this, $method)) {
  325. return $this->{$method}();
  326. }
  327. $form = $this->getBlueprint()->form();
  328. return $form[$name] ?? null;
  329. }
  330. /**
  331. * @param string $name
  332. * @param mixed $value
  333. * @return void
  334. */
  335. #[\ReturnTypeWillChange]
  336. public function __set($name, $value)
  337. {
  338. $method = "set{$name}";
  339. if (method_exists($this, $method)) {
  340. $this->{$method}($value);
  341. }
  342. }
  343. /**
  344. * @param string $name
  345. * @return bool
  346. */
  347. #[\ReturnTypeWillChange]
  348. public function __isset($name)
  349. {
  350. $method = "get{$name}";
  351. if (method_exists($this, $method)) {
  352. return true;
  353. }
  354. $form = $this->getBlueprint()->form();
  355. return isset($form[$name]);
  356. }
  357. /**
  358. * @param string $name
  359. * @return void
  360. */
  361. #[\ReturnTypeWillChange]
  362. public function __unset($name)
  363. {
  364. }
  365. /**
  366. * @return array|bool
  367. */
  368. protected function getUnserializeAllowedClasses()
  369. {
  370. return [FlexObject::class];
  371. }
  372. /**
  373. * Note: this method clones the object.
  374. *
  375. * @param FlexDirectory $directory
  376. * @return $this
  377. */
  378. protected function setDirectory(FlexDirectory $directory): self
  379. {
  380. $this->directory = $directory;
  381. return $this;
  382. }
  383. /**
  384. * @param string $layout
  385. * @return Template|TemplateWrapper
  386. * @throws LoaderError
  387. * @throws SyntaxError
  388. */
  389. protected function getTemplate($layout)
  390. {
  391. $grav = Grav::instance();
  392. /** @var Twig $twig */
  393. $twig = $grav['twig'];
  394. return $twig->twig()->resolveTemplate(
  395. [
  396. "flex-objects/layouts/{$this->getFlexType()}/form/{$layout}.html.twig",
  397. "flex-objects/layouts/_default/form/{$layout}.html.twig",
  398. "forms/{$layout}/form.html.twig",
  399. 'forms/default/form.html.twig'
  400. ]
  401. );
  402. }
  403. /**
  404. * @param array $data
  405. * @param array $files
  406. * @return void
  407. * @throws Exception
  408. */
  409. protected function doSubmit(array $data, array $files)
  410. {
  411. $this->directory->saveDirectoryConfig($this->name, $data);
  412. $this->reset();
  413. }
  414. /**
  415. * @return array
  416. */
  417. protected function doSerialize(): array
  418. {
  419. return $this->doTraitSerialize() + [
  420. 'form' => $this->form,
  421. 'directory' => $this->directory,
  422. 'flexName' => $this->flexName
  423. ];
  424. }
  425. /**
  426. * @param array $data
  427. * @return void
  428. */
  429. protected function doUnserialize(array $data): void
  430. {
  431. $this->doTraitUnserialize($data);
  432. $this->form = $data['form'];
  433. $this->directory = $data['directory'];
  434. $this->flexName = $data['flexName'];
  435. }
  436. /**
  437. * Filter validated data.
  438. *
  439. * @param ArrayAccess|Data|null $data
  440. * @phpstan-param ArrayAccess<string,mixed>|Data|null $data
  441. */
  442. protected function filterData($data = null): void
  443. {
  444. if ($data instanceof Data) {
  445. $data->filter(false, true);
  446. }
  447. }
  448. }