Form.php 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. <?php
  2. declare(strict_types = 1);
  3. // {{{ License
  4. // This file is part of GNU social - https://www.gnu.org/software/social
  5. //
  6. // GNU social is free software: you can redistribute it and/or modify
  7. // it under the terms of the GNU Affero General Public License as published by
  8. // the Free Software Foundation, either version 3 of the License, or
  9. // (at your option) any later version.
  10. //
  11. // GNU social is distributed in the hope that it will be useful,
  12. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. // GNU Affero General Public License for more details.
  15. //
  16. // You should have received a copy of the GNU Affero General Public License
  17. // along with GNU social. If not, see <http://www.gnu.org/licenses/>.
  18. // }}}
  19. /**
  20. * Convert a Form from our declarative to Symfony's representation
  21. *
  22. * @package GNUsocial
  23. * @category Wrapper
  24. *
  25. * @author Hugo Sales <hugo@hsal.es>
  26. * @copyright 2020-2021 Free Software Foundation, Inc http://www.fsf.org
  27. * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
  28. */
  29. namespace App\Core;
  30. use App\Core\DB\DB;
  31. use App\Util\Exception\RedirectException;
  32. use App\Util\Exception\ServerException;
  33. use App\Util\Formatting;
  34. use Symfony\Component\Form\Extension\Core\Type\SubmitType;
  35. use Symfony\Component\Form\Form as SymfForm;
  36. use Symfony\Component\Form\FormFactoryInterface;
  37. use Symfony\Component\Form\FormInterface as SymfFormInterface;
  38. use Symfony\Component\HttpFoundation\Request;
  39. /**
  40. * This class converts our own form representation to Symfony's
  41. *
  42. * Example:
  43. * ```
  44. * $form = Form::create([
  45. * ['content', TextareaType::class, ['label' => ' ', 'data' => '', 'attr' => ['placeholder' => _m($placeholder_string[$rand_key])]]],
  46. * ['attachments', FileType::class, ['label' => ' ', 'data' => null, 'multiple' => true, 'required' => false]],
  47. * ['visibility', ChoiceType::class, ['label' => _m('Visibility:'), 'expanded' => true, 'data' => 'public', 'choices' => [_m('Public') => 'public', _m('Instance') => 'instance', _m('Private') => 'private']]],
  48. * ['to', ChoiceType::class, ['label' => _m('To:'), 'multiple' => true, 'expanded' => true, 'choices' => $to_tags]],
  49. * ['post', SubmitType::class, ['label' => _m('Post')]],
  50. * ]);
  51. * ```
  52. * turns into
  53. * ```
  54. * \Symfony\Component\Form\Form {
  55. * config: Symfony\Component\Form\FormBuilder { ... }
  56. * ...
  57. * children: Symfony\Component\Form\Util\OrderedHashMap {
  58. * elements: array:5 [
  59. * "content" => Symfony\Component\Form\Form { ... }
  60. * "attachments" => Symfony\Component\Form\Form { ... }
  61. * "visibility" => Symfony\Component\Form\Form { ... }
  62. * "to" => Symfony\Component\Form\Form { ... }
  63. * "post" => Symfony\Component\Form\SubmitButton { ... }
  64. * ]
  65. * ...
  66. * }
  67. * ...
  68. * }
  69. * ```
  70. */
  71. abstract class Form
  72. {
  73. private static ?FormFactoryInterface $form_factory;
  74. public static function setFactory($ff): void
  75. {
  76. self::$form_factory = $ff;
  77. }
  78. /**
  79. * Create a form with the given associative array $form as fields
  80. */
  81. public static function create(
  82. array $form,
  83. ?object $target = null,
  84. array $extra_data = [],
  85. string $type = 'Symfony\Component\Form\Extension\Core\Type\FormType',
  86. array $form_options = [],
  87. ): SymfFormInterface {
  88. $name = $form[array_key_last($form)][0];
  89. $fb = self::$form_factory->createNamedBuilder($name, $type, data: null, options: array_merge($form_options, ['translation_domain' => false]));
  90. foreach ($form as [$key, $class, $options]) {
  91. if ($class == SubmitType::class && \in_array($key, ['save', 'publish', 'post'])) {
  92. Log::critical($m = "It's generally a bad idea to use {$key} as a form name, because it can conflict with other forms in the same page");
  93. throw new ServerException($m);
  94. }
  95. if ($target != null && empty($options['data']) && (mb_strstr($key, 'password') == false) && $class != SubmitType::class) {
  96. if (isset($extra_data[$key])) {
  97. // @codeCoverageIgnoreStart
  98. $options['data'] = $extra_data[$key];
  99. // @codeCoverageIgnoreEnd
  100. } else {
  101. $method = 'get' . ucfirst(Formatting::snakeCaseToCamelCase($key));
  102. if (method_exists($target, $method)) {
  103. $options['data'] = $target->{$method}();
  104. }
  105. }
  106. }
  107. unset($options['hide']);
  108. if (isset($options['transformer'])) {
  109. $transformer = $options['transformer'];
  110. unset($options['transformer']);
  111. }
  112. $fb->add($key, $class, $options);
  113. if (isset($transformer)) {
  114. $fb->get($key)->addModelTransformer(new $transformer());
  115. unset($transformer);
  116. }
  117. }
  118. return $fb->getForm();
  119. }
  120. /**
  121. * Whether the given $field of $form has the `required` property
  122. * set, defaults to true
  123. */
  124. public static function isRequired(array $form, string $field): bool
  125. {
  126. return $form[$field][2]['required'] ?? true;
  127. }
  128. /**
  129. * Handle the full life cycle of a form. Creates it with @see
  130. * self::create and inserts the submitted values into the database
  131. *
  132. * @throws ServerException
  133. */
  134. public static function handle(array $form_definition, Request $request, ?object $target, array $extra_args = [], ?callable $extra_step = null, array $create_args = [], ?SymfForm $testing_only_form = null): mixed
  135. {
  136. $form = $testing_only_form ?? self::create($form_definition, $target, ...$create_args);
  137. $form->handleRequest($request);
  138. if ($request->getMethod() === 'POST' && $form->isSubmitted()) {
  139. if (!$form->isValid()) {
  140. $errors = [];
  141. foreach ($form->all() as $child) {
  142. if (!$child->isValid()) {
  143. $errors[$child->getName()] = (string) $form[$child->getName()]->getErrors();
  144. }
  145. }
  146. return $errors;
  147. } else {
  148. $data = $form->getData();
  149. if (\is_null($target)) {
  150. return $data;
  151. }
  152. unset($data['translation_domain'], $data['save']);
  153. foreach ($data as $key => $val) {
  154. $method = 'set' . ucfirst(Formatting::snakeCaseToCamelCase($key));
  155. if (method_exists($target, $method)) {
  156. if (isset($extra_args[$key])) {
  157. // @codeCoverageIgnoreStart
  158. $target->{$method}($val, $extra_args[$key]);
  159. // @codeCoverageIgnoreEnd
  160. } else {
  161. $target->{$method}($val);
  162. }
  163. }
  164. }
  165. if (isset($extra_step)) {
  166. // @codeCoverageIgnoreStart
  167. $extra_step($data, $extra_args);
  168. // @codeCoverageIgnoreEnd
  169. }
  170. DB::merge($target);
  171. DB::flush();
  172. throw new RedirectException(url: $request->getPathInfo());
  173. }
  174. }
  175. return $form;
  176. }
  177. }