PageRenderTime 58ms CodeModel.GetById 29ms RepoModel.GetById 1ms app.codeStats 0ms

/src/Symfony/Component/DomCrawler/Form.php

https://bitbucket.org/gencer/symfony
PHP | 476 lines | 219 code | 52 blank | 205 comment | 48 complexity | c3a898716af763e623038d4e0d90d5cd MD5 | raw file
  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\DomCrawler;
  11. use Symfony\Component\DomCrawler\Field\FormField;
  12. /**
  13. * Form represents an HTML form.
  14. *
  15. * @author Fabien Potencier <fabien@symfony.com>
  16. *
  17. * @api
  18. */
  19. class Form extends Link implements \ArrayAccess
  20. {
  21. /**
  22. * @var \DOMElement
  23. */
  24. private $button;
  25. /**
  26. * @var FormFieldRegistry
  27. */
  28. private $fields;
  29. /**
  30. * Constructor.
  31. *
  32. * @param \DOMElement $node A \DOMElement instance
  33. * @param string $currentUri The URI of the page where the form is embedded
  34. * @param string $method The method to use for the link (if null, it defaults to the method defined by the form)
  35. *
  36. * @throws \LogicException if the node is not a button inside a form tag
  37. *
  38. * @api
  39. */
  40. public function __construct(\DOMElement $node, $currentUri, $method = null)
  41. {
  42. parent::__construct($node, $currentUri, $method);
  43. $this->initialize();
  44. }
  45. /**
  46. * Gets the form node associated with this form.
  47. *
  48. * @return \DOMElement A \DOMElement instance
  49. */
  50. public function getFormNode()
  51. {
  52. return $this->node;
  53. }
  54. /**
  55. * Sets the value of the fields.
  56. *
  57. * @param array $values An array of field values
  58. *
  59. * @return Form
  60. *
  61. * @api
  62. */
  63. public function setValues(array $values)
  64. {
  65. foreach ($values as $name => $value) {
  66. $this->fields->set($name, $value);
  67. }
  68. return $this;
  69. }
  70. /**
  71. * Gets the field values.
  72. *
  73. * The returned array does not include file fields (@see getFiles).
  74. *
  75. * @return array An array of field values.
  76. *
  77. * @api
  78. */
  79. public function getValues()
  80. {
  81. $values = array();
  82. foreach ($this->fields->all() as $name => $field) {
  83. if ($field->isDisabled()) {
  84. continue;
  85. }
  86. if (!$field instanceof Field\FileFormField && $field->hasValue()) {
  87. $values[$name] = $field->getValue();
  88. }
  89. }
  90. return $values;
  91. }
  92. /**
  93. * Gets the file field values.
  94. *
  95. * @return array An array of file field values.
  96. *
  97. * @api
  98. */
  99. public function getFiles()
  100. {
  101. if (!in_array($this->getMethod(), array('POST', 'PUT', 'DELETE', 'PATCH'))) {
  102. return array();
  103. }
  104. $files = array();
  105. foreach ($this->fields->all() as $name => $field) {
  106. if ($field->isDisabled()) {
  107. continue;
  108. }
  109. if ($field instanceof Field\FileFormField) {
  110. $files[$name] = $field->getValue();
  111. }
  112. }
  113. return $files;
  114. }
  115. /**
  116. * Gets the field values as PHP.
  117. *
  118. * This method converts fields with the array notation
  119. * (like foo[bar] to arrays) like PHP does.
  120. *
  121. * @return array An array of field values.
  122. *
  123. * @api
  124. */
  125. public function getPhpValues()
  126. {
  127. $values = array();
  128. foreach ($this->getValues() as $name => $value) {
  129. $qs = http_build_query(array($name => $value), '', '&');
  130. if (!empty($qs)) {
  131. parse_str($qs, $expandedValue);
  132. $varName = substr($name, 0, strlen(key($expandedValue)));
  133. $values = array_replace_recursive($values, array($varName => current($expandedValue)));
  134. }
  135. }
  136. return $values;
  137. }
  138. /**
  139. * Gets the file field values as PHP.
  140. *
  141. * This method converts fields with the array notation
  142. * (like foo[bar] to arrays) like PHP does.
  143. *
  144. * @return array An array of field values.
  145. *
  146. * @api
  147. */
  148. public function getPhpFiles()
  149. {
  150. $values = array();
  151. foreach ($this->getFiles() as $name => $value) {
  152. $qs = http_build_query(array($name => $value), '', '&');
  153. if (!empty($qs)) {
  154. parse_str($qs, $expandedValue);
  155. $varName = substr($name, 0, strlen(key($expandedValue)));
  156. $values = array_replace_recursive($values, array($varName => current($expandedValue)));
  157. }
  158. }
  159. return $values;
  160. }
  161. /**
  162. * Gets the URI of the form.
  163. *
  164. * The returned URI is not the same as the form "action" attribute.
  165. * This method merges the value if the method is GET to mimics
  166. * browser behavior.
  167. *
  168. * @return string The URI
  169. *
  170. * @api
  171. */
  172. public function getUri()
  173. {
  174. $uri = parent::getUri();
  175. if (!in_array($this->getMethod(), array('POST', 'PUT', 'DELETE', 'PATCH')) && $queryString = http_build_query($this->getValues(), null, '&')) {
  176. $sep = false === strpos($uri, '?') ? '?' : '&';
  177. $uri .= $sep.$queryString;
  178. }
  179. return $uri;
  180. }
  181. protected function getRawUri()
  182. {
  183. return $this->node->getAttribute('action');
  184. }
  185. /**
  186. * Gets the form method.
  187. *
  188. * If no method is defined in the form, GET is returned.
  189. *
  190. * @return string The method
  191. *
  192. * @api
  193. */
  194. public function getMethod()
  195. {
  196. if (null !== $this->method) {
  197. return $this->method;
  198. }
  199. return $this->node->getAttribute('method') ? strtoupper($this->node->getAttribute('method')) : 'GET';
  200. }
  201. /**
  202. * Returns true if the named field exists.
  203. *
  204. * @param string $name The field name
  205. *
  206. * @return bool true if the field exists, false otherwise
  207. *
  208. * @api
  209. */
  210. public function has($name)
  211. {
  212. return $this->fields->has($name);
  213. }
  214. /**
  215. * Removes a field from the form.
  216. *
  217. * @param string $name The field name
  218. *
  219. * @throws \InvalidArgumentException when the name is malformed
  220. *
  221. * @api
  222. */
  223. public function remove($name)
  224. {
  225. $this->fields->remove($name);
  226. }
  227. /**
  228. * Gets a named field.
  229. *
  230. * @param string $name The field name
  231. *
  232. * @return FormField The field instance
  233. *
  234. * @throws \InvalidArgumentException When field is not present in this form
  235. *
  236. * @api
  237. */
  238. public function get($name)
  239. {
  240. return $this->fields->get($name);
  241. }
  242. /**
  243. * Sets a named field.
  244. *
  245. * @param FormField $field The field
  246. *
  247. * @api
  248. */
  249. public function set(FormField $field)
  250. {
  251. $this->fields->add($field);
  252. }
  253. /**
  254. * Gets all fields.
  255. *
  256. * @return FormField[] An array of fields
  257. *
  258. * @api
  259. */
  260. public function all()
  261. {
  262. return $this->fields->all();
  263. }
  264. /**
  265. * Returns true if the named field exists.
  266. *
  267. * @param string $name The field name
  268. *
  269. * @return bool true if the field exists, false otherwise
  270. */
  271. public function offsetExists($name)
  272. {
  273. return $this->has($name);
  274. }
  275. /**
  276. * Gets the value of a field.
  277. *
  278. * @param string $name The field name
  279. *
  280. * @return FormField The associated Field instance
  281. *
  282. * @throws \InvalidArgumentException if the field does not exist
  283. */
  284. public function offsetGet($name)
  285. {
  286. return $this->fields->get($name);
  287. }
  288. /**
  289. * Sets the value of a field.
  290. *
  291. * @param string $name The field name
  292. * @param string|array $value The value of the field
  293. *
  294. * @throws \InvalidArgumentException if the field does not exist
  295. */
  296. public function offsetSet($name, $value)
  297. {
  298. $this->fields->set($name, $value);
  299. }
  300. /**
  301. * Removes a field from the form.
  302. *
  303. * @param string $name The field name
  304. */
  305. public function offsetUnset($name)
  306. {
  307. $this->fields->remove($name);
  308. }
  309. /**
  310. * Disables validation
  311. *
  312. * @return self
  313. */
  314. public function disableValidation()
  315. {
  316. foreach ($this->fields->all() as $field) {
  317. if ($field instanceof Field\ChoiceFormField) {
  318. $field->disableValidation();
  319. }
  320. }
  321. return $this;
  322. }
  323. /**
  324. * Sets the node for the form.
  325. *
  326. * Expects a 'submit' button \DOMElement and finds the corresponding form element, or the form element itself.
  327. *
  328. * @param \DOMElement $node A \DOMElement instance
  329. *
  330. * @throws \LogicException If given node is not a button or input or does not have a form ancestor
  331. */
  332. protected function setNode(\DOMElement $node)
  333. {
  334. $this->button = $node;
  335. if ('button' === $node->nodeName || ('input' === $node->nodeName && in_array(strtolower($node->getAttribute('type')), array('submit', 'button', 'image')))) {
  336. if ($node->hasAttribute('form')) {
  337. // if the node has the HTML5-compliant 'form' attribute, use it
  338. $formId = $node->getAttribute('form');
  339. $form = $node->ownerDocument->getElementById($formId);
  340. if (null === $form) {
  341. throw new \LogicException(sprintf('The selected node has an invalid form attribute (%s).', $formId));
  342. }
  343. $this->node = $form;
  344. return;
  345. }
  346. // we loop until we find a form ancestor
  347. do {
  348. if (null === $node = $node->parentNode) {
  349. throw new \LogicException('The selected node does not have a form ancestor.');
  350. }
  351. } while ('form' !== $node->nodeName);
  352. } elseif ('form' !== $node->nodeName) {
  353. throw new \LogicException(sprintf('Unable to submit on a "%s" tag.', $node->nodeName));
  354. }
  355. $this->node = $node;
  356. }
  357. /**
  358. * Adds form elements related to this form.
  359. *
  360. * Creates an internal copy of the submitted 'button' element and
  361. * the form node or the entire document depending on whether we need
  362. * to find non-descendant elements through HTML5 'form' attribute.
  363. */
  364. private function initialize()
  365. {
  366. $this->fields = new FormFieldRegistry();
  367. $xpath = new \DOMXPath($this->node->ownerDocument);
  368. // add submitted button if it has a valid name
  369. if ('form' !== $this->button->nodeName && $this->button->hasAttribute('name') && $this->button->getAttribute('name')) {
  370. if ('input' == $this->button->nodeName && 'image' == strtolower($this->button->getAttribute('type'))) {
  371. $name = $this->button->getAttribute('name');
  372. $this->button->setAttribute('value', '0');
  373. // temporarily change the name of the input node for the x coordinate
  374. $this->button->setAttribute('name', $name.'.x');
  375. $this->set(new Field\InputFormField($this->button));
  376. // temporarily change the name of the input node for the y coordinate
  377. $this->button->setAttribute('name', $name.'.y');
  378. $this->set(new Field\InputFormField($this->button));
  379. // restore the original name of the input node
  380. $this->button->setAttribute('name', $name);
  381. } else {
  382. $this->set(new Field\InputFormField($this->button));
  383. }
  384. }
  385. // find form elements corresponding to the current form
  386. if ($this->node->hasAttribute('id')) {
  387. // corresponding elements are either descendants or have a matching HTML5 form attribute
  388. $formId = Crawler::xpathLiteral($this->node->getAttribute('id'));
  389. $fieldNodes = $xpath->query(sprintf('descendant::input[@form=%s] | descendant::button[@form=%s] | descendant::textarea[@form=%s] | descendant::select[@form=%s] | //form[@id=%s]//input[not(@form)] | //form[@id=%s]//button[not(@form)] | //form[@id=%s]//textarea[not(@form)] | //form[@id=%s]//select[not(@form)]', $formId, $formId, $formId, $formId, $formId, $formId, $formId, $formId));
  390. foreach ($fieldNodes as $node) {
  391. $this->addField($node);
  392. }
  393. } else {
  394. // do the xpath query with $this->node as the context node, to only find descendant elements
  395. // however, descendant elements with form attribute are not part of this form
  396. $fieldNodes = $xpath->query('descendant::input[not(@form)] | descendant::button[not(@form)] | descendant::textarea[not(@form)] | descendant::select[not(@form)]', $this->node);
  397. foreach ($fieldNodes as $node) {
  398. $this->addField($node);
  399. }
  400. }
  401. }
  402. private function addField(\DOMElement $node)
  403. {
  404. if (!$node->hasAttribute('name') || !$node->getAttribute('name')) {
  405. return;
  406. }
  407. $nodeName = $node->nodeName;
  408. if ('select' == $nodeName || 'input' == $nodeName && 'checkbox' == strtolower($node->getAttribute('type'))) {
  409. $this->set(new Field\ChoiceFormField($node));
  410. } elseif ('input' == $nodeName && 'radio' == strtolower($node->getAttribute('type'))) {
  411. if ($this->has($node->getAttribute('name'))) {
  412. $this->get($node->getAttribute('name'))->addChoice($node);
  413. } else {
  414. $this->set(new Field\ChoiceFormField($node));
  415. }
  416. } elseif ('input' == $nodeName && 'file' == strtolower($node->getAttribute('type'))) {
  417. $this->set(new Field\FileFormField($node));
  418. } elseif ('input' == $nodeName && !in_array(strtolower($node->getAttribute('type')), array('submit', 'button', 'image'))) {
  419. $this->set(new Field\InputFormField($node));
  420. } elseif ('textarea' == $nodeName) {
  421. $this->set(new Field\TextareaFormField($node));
  422. }
  423. }
  424. }