FormFactory.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\Form;
  11. use Symfony\Component\Form\Exception\FormException;
  12. use Symfony\Component\Form\Exception\UnexpectedTypeException;
  13. use Symfony\Component\Form\Exception\TypeDefinitionException;
  14. use Symfony\Component\Form\Exception\CreationException;
  15. class FormFactory implements FormFactoryInterface
  16. {
  17. private static $requiredOptions = array(
  18. 'data',
  19. 'required',
  20. 'max_length',
  21. );
  22. /**
  23. * Extensions
  24. * @var array An array of FormExtensionInterface
  25. */
  26. private $extensions = array();
  27. /**
  28. * All known types (cache)
  29. * @var array An array of FormTypeInterface
  30. */
  31. private $types = array();
  32. /**
  33. * The guesser chain
  34. * @var FormTypeGuesserChain
  35. */
  36. private $guesser;
  37. /**
  38. * Constructor.
  39. *
  40. * @param array $extensions An array of FormExtensionInterface
  41. *
  42. * @throws UnexpectedTypeException if any extension does not implement FormExtensionInterface
  43. */
  44. public function __construct(array $extensions)
  45. {
  46. foreach ($extensions as $extension) {
  47. if (!$extension instanceof FormExtensionInterface) {
  48. throw new UnexpectedTypeException($extension, 'Symfony\Component\Form\FormExtensionInterface');
  49. }
  50. }
  51. $this->extensions = $extensions;
  52. }
  53. /**
  54. * Returns whether the given type is supported.
  55. *
  56. * @param string $name The name of the type
  57. *
  58. * @return Boolean Whether the type is supported
  59. */
  60. public function hasType($name)
  61. {
  62. if (isset($this->types[$name])) {
  63. return true;
  64. }
  65. try {
  66. $this->loadType($name);
  67. } catch (FormException $e) {
  68. return false;
  69. }
  70. return true;
  71. }
  72. /**
  73. * Add a type.
  74. *
  75. * @param FormTypeInterface $type The type
  76. */
  77. public function addType(FormTypeInterface $type)
  78. {
  79. $this->loadTypeExtensions($type);
  80. $this->types[$type->getName()] = $type;
  81. }
  82. /**
  83. * Returns a type by name.
  84. *
  85. * This methods registers the type extensions from the form extensions.
  86. *
  87. * @param string|FormTypeInterface $name The name of the type or a type instance
  88. *
  89. * @return FormTypeInterface The type
  90. *
  91. * @throws FormException if the type can not be retrieved from any extension
  92. */
  93. public function getType($name)
  94. {
  95. if (!is_string($name)) {
  96. throw new UnexpectedTypeException($name, 'string');
  97. }
  98. if (!isset($this->types[$name])) {
  99. $this->loadType($name);
  100. }
  101. return $this->types[$name];
  102. }
  103. /**
  104. * Returns a form.
  105. *
  106. * @see createBuilder()
  107. *
  108. * @param string|FormTypeInterface $type The type of the form
  109. * @param mixed $data The initial data
  110. * @param array $options The options
  111. *
  112. * @return Form The form named after the type
  113. *
  114. * @throws FormException if any given option is not applicable to the given type
  115. */
  116. public function create($type, $data = null, array $options = array())
  117. {
  118. return $this->createBuilder($type, $data, $options)->getForm();
  119. }
  120. /**
  121. * Returns a form.
  122. *
  123. * @see createNamedBuilder()
  124. *
  125. * @param string|FormTypeInterface $type The type of the form
  126. * @param string $name The name of the form
  127. * @param mixed $data The initial data
  128. * @param array $options The options
  129. *
  130. * @return Form The form
  131. *
  132. * @throws FormException if any given option is not applicable to the given type
  133. */
  134. public function createNamed($type, $name, $data = null, array $options = array())
  135. {
  136. return $this->createNamedBuilder($type, $name, $data, $options)->getForm();
  137. }
  138. /**
  139. * Returns a form for a property of a class.
  140. *
  141. * @see createBuilderForProperty()
  142. *
  143. * @param string $class The fully qualified class name
  144. * @param string $property The name of the property to guess for
  145. * @param mixed $data The initial data
  146. * @param array $options The options for the builder
  147. *
  148. * @return Form The form named after the property
  149. *
  150. * @throws FormException if any given option is not applicable to the form type
  151. */
  152. public function createForProperty($class, $property, $data = null, array $options = array())
  153. {
  154. return $this->createBuilderForProperty($class, $property, $data, $options)->getForm();
  155. }
  156. /**
  157. * Returns a form builder
  158. *
  159. * @param string|FormTypeInterface $type The type of the form
  160. * @param string $name The name of the form
  161. * @param mixed $data The initial data
  162. * @param array $options The options
  163. *
  164. * @return FormBuilder The form builder
  165. *
  166. * @throws FormException if any given option is not applicable to the given type
  167. */
  168. public function createBuilder($type, $data = null, array $options = array())
  169. {
  170. $name = is_object($type) ? $type->getName() : $type;
  171. return $this->createNamedBuilder($type, $name, $data, $options);
  172. }
  173. /**
  174. * Returns a form builder.
  175. *
  176. * @param string|FormTypeInterface $type The type of the form
  177. * @param string $name The name of the form
  178. * @param mixed $data The initial data
  179. * @param array $options The options
  180. *
  181. * @return FormBuilder The form builder
  182. *
  183. * @throws FormException if any given option is not applicable to the given type
  184. */
  185. public function createNamedBuilder($type, $name, $data = null, array $options = array())
  186. {
  187. $builder = null;
  188. $types = array();
  189. $knownOptions = array();
  190. $passedOptions = array_keys($options);
  191. $optionValues = array();
  192. if (!array_key_exists('data', $options)) {
  193. $options['data'] = $data;
  194. }
  195. while (null !== $type) {
  196. if ($type instanceof FormTypeInterface) {
  197. $this->addType($type);
  198. } else {
  199. $type = $this->getType($type);
  200. }
  201. $defaultOptions = $type->getDefaultOptions($options);
  202. $optionValues = array_merge_recursive($optionValues, $type->getAllowedOptionValues($options));
  203. foreach ($type->getExtensions() as $typeExtension) {
  204. $defaultOptions = array_replace($defaultOptions, $typeExtension->getDefaultOptions($options));
  205. $optionValues = array_merge_recursive($optionValues, $typeExtension->getAllowedOptionValues($options));
  206. }
  207. $options = array_replace($defaultOptions, $options);
  208. $knownOptions = array_merge($knownOptions, array_keys($defaultOptions));
  209. array_unshift($types, $type);
  210. $type = $type->getParent($options);
  211. }
  212. $type = end($types);
  213. $diff = array_diff(self::$requiredOptions, $knownOptions);
  214. if (count($diff) > 0) {
  215. throw new TypeDefinitionException(sprintf('Type "%s" should support the option(s) "%s"', $type->getName(), implode('", "', $diff)));
  216. }
  217. $diff = array_diff($passedOptions, $knownOptions);
  218. if (count($diff) > 1) {
  219. throw new CreationException(sprintf('The options "%s" do not exist', implode('", "', $diff)));
  220. }
  221. if (count($diff) > 0) {
  222. throw new CreationException(sprintf('The option "%s" does not exist', current($diff)));
  223. }
  224. foreach ($optionValues as $option => $allowedValues) {
  225. if (!in_array($options[$option], $allowedValues, true)) {
  226. throw new CreationException(sprintf('The option "%s" has the value "%s", but is expected to be one of "%s"', $option, $options[$option], implode('", "', $allowedValues)));
  227. }
  228. }
  229. for ($i = 0, $l = count($types); $i < $l && !$builder; ++$i) {
  230. $builder = $types[$i]->createBuilder($name, $this, $options);
  231. }
  232. if (!$builder) {
  233. throw new TypeDefinitionException(sprintf('Type "%s" or any of its parents should return a FormBuilder instance from createBuilder()', $type->getName()));
  234. }
  235. $builder->setTypes($types);
  236. foreach ($types as $type) {
  237. $type->buildForm($builder, $options);
  238. foreach ($type->getExtensions() as $typeExtension) {
  239. $typeExtension->buildForm($builder, $options);
  240. }
  241. }
  242. return $builder;
  243. }
  244. /**
  245. * Returns a form builder for a property of a class.
  246. *
  247. * If any of the 'max_length', 'required' and type options can be guessed,
  248. * and are not provided in the options argument, the guessed value is used.
  249. *
  250. * @param string $class The fully qualified class name
  251. * @param string $property The name of the property to guess for
  252. * @param mixed $data The initial data
  253. * @param array $options The options for the builder
  254. *
  255. * @return FormBuilder The form builder named after the property
  256. *
  257. * @throws FormException if any given option is not applicable to the form type
  258. */
  259. public function createBuilderForProperty($class, $property, $data = null, array $options = array())
  260. {
  261. if (!$this->guesser) {
  262. $this->loadGuesser();
  263. }
  264. $typeGuess = $this->guesser->guessType($class, $property);
  265. $maxLengthGuess = $this->guesser->guessMaxLength($class, $property);
  266. $minLengthGuess = $this->guesser->guessMinLength($class, $property);
  267. $requiredGuess = $this->guesser->guessRequired($class, $property);
  268. $type = $typeGuess ? $typeGuess->getType() : 'text';
  269. if ($maxLengthGuess) {
  270. $options = array_merge(array('max_length' => $maxLengthGuess->getValue()), $options);
  271. }
  272. if ($minLengthGuess) {
  273. if ($maxLengthGuess) {
  274. $options = array_merge(array('pattern' => '.{'.$minLengthGuess->getValue().','.$maxLengthGuess->getValue().'}'), $options);
  275. } else {
  276. $options = array_merge(array('pattern' => '.{'.$minLengthGuess->getValue().',}'), $options);
  277. }
  278. }
  279. if ($requiredGuess) {
  280. $options = array_merge(array('required' => $requiredGuess->getValue()), $options);
  281. }
  282. // user options may override guessed options
  283. if ($typeGuess) {
  284. $options = array_merge($typeGuess->getOptions(), $options);
  285. }
  286. return $this->createNamedBuilder($type, $property, $data, $options);
  287. }
  288. /**
  289. * Initializes the guesser chain.
  290. */
  291. private function loadGuesser()
  292. {
  293. $guessers = array();
  294. foreach ($this->extensions as $extension) {
  295. $guesser = $extension->getTypeGuesser();
  296. if ($guesser) {
  297. $guessers[] = $guesser;
  298. }
  299. }
  300. $this->guesser = new FormTypeGuesserChain($guessers);
  301. }
  302. /**
  303. * Loads a type.
  304. *
  305. * @param string $name The type name
  306. *
  307. * @throws FormException if the type is not provided by any registered extension
  308. */
  309. private function loadType($name)
  310. {
  311. $type = null;
  312. foreach ($this->extensions as $extension) {
  313. if ($extension->hasType($name)) {
  314. $type = $extension->getType($name);
  315. break;
  316. }
  317. }
  318. if (!$type) {
  319. throw new FormException(sprintf('Could not load type "%s"', $name));
  320. }
  321. $this->loadTypeExtensions($type);
  322. $this->types[$name] = $type;
  323. }
  324. /**
  325. * Loads the extensions for a given type.
  326. *
  327. * @param FormTypeInterface $type The type
  328. */
  329. private function loadTypeExtensions(FormTypeInterface $type)
  330. {
  331. $typeExtensions = array();
  332. foreach ($this->extensions as $extension) {
  333. $typeExtensions = array_merge(
  334. $typeExtensions,
  335. $extension->getTypeExtensions($type->getName())
  336. );
  337. }
  338. $type->setExtensions($typeExtensions);
  339. }
  340. }