annotate vendor/symfony/dom-crawler/Form.php @ 19:fa3358dc1485 tip

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