Skip to content

Commit

Permalink
[Form] Hardened code of ViolationMapper against errors
Browse files Browse the repository at this point in the history
  • Loading branch information
webmozart committed Oct 3, 2012
1 parent 39eb3e7 commit a0661e5
Showing 1 changed file with 48 additions and 64 deletions.
112 changes: 48 additions & 64 deletions Extension/Validator/ViolationMapper/ViolationMapper.php
Expand Up @@ -25,21 +25,6 @@
*/
class ViolationMapper implements ViolationMapperInterface
{
/**
* @var FormInterface
*/
private $scope;

/**
* @var array
*/
private $children;

/**
* @var array
*/
private $rules = array();

/**
* @var Boolean
*/
Expand All @@ -52,6 +37,13 @@ public function mapViolation(ConstraintViolation $violation, FormInterface $form
{
$this->allowNonSynchronized = $allowNonSynchronized;

// The scope is the currently found most specific form that
// an error should be mapped to. After setting the scope, the
// mapper will try to continue to find more specific matches in
// the children of scope. If it cannot, the error will be
// mapped to this scope.
$scope = null;

$violationPath = null;
$relativePath = null;
$match = false;
Expand All @@ -65,7 +57,7 @@ public function mapViolation(ConstraintViolation $violation, FormInterface $form
// This case happens if the violation path is empty and thus
// the violation should be mapped to the root form
if (null === $violationPath) {
$this->scope = $form;
$scope = $form;
}

// In general, mapping happens from the root form to the leaf forms
Expand All @@ -85,11 +77,11 @@ public function mapViolation(ConstraintViolation $violation, FormInterface $form
// This root will usually be $form. If the path contains
// an unmapped form though, the last unmapped form found
// will be the root of the path.
$this->setScope($relativePath->getRoot());
$scope = $relativePath->getRoot();
$it = new PropertyPathIterator($relativePath);

while ($this->isValidScope() && null !== ($child = $this->matchChild($it))) {
$this->setScope($child);
while ($this->acceptsErrors($scope) && null !== ($child = $this->matchChild($scope, $it))) {
$scope = $child;
$it->next();
$match = true;
}
Expand All @@ -105,33 +97,32 @@ public function mapViolation(ConstraintViolation $violation, FormInterface $form
// e.g. "children[foo].children[bar].data.baz"
// Here the innermost directly mapped child is "bar"

$scope = $form;
$it = new ViolationPathIterator($violationPath);
// The overhead of setScope() is not needed anymore here
$this->scope = $form;

while ($this->isValidScope() && $it->valid() && $it->mapsForm()) {
if (!$this->scope->has($it->current())) {
while ($this->acceptsErrors($scope) && $it->valid() && $it->mapsForm()) {
if (!$scope->has($it->current())) {
// Break if we find a reference to a non-existing child
break;
}

$this->scope = $this->scope->get($it->current());
$scope = $scope->get($it->current());
$it->next();
}
}

// Follow dot rules until we have the final target
$mapping = $this->scope->getConfig()->getOption('error_mapping');
$mapping = $scope->getConfig()->getOption('error_mapping');

while ($this->isValidScope() && isset($mapping['.'])) {
$dotRule = new MappingRule($this->scope, '.', $mapping['.']);
$this->scope = $dotRule->getTarget();
$mapping = $this->scope->getConfig()->getOption('error_mapping');
while ($this->acceptsErrors($scope) && isset($mapping['.'])) {
$dotRule = new MappingRule($scope, '.', $mapping['.']);
$scope = $dotRule->getTarget();
$mapping = $scope->getConfig()->getOption('error_mapping');
}

// Only add the error if the form is synchronized
if ($this->isValidScope()) {
$this->scope->addError(new FormError(
if ($this->acceptsErrors($scope)) {
$scope->addError(new FormError(
$violation->getMessageTemplate(),
$violation->getMessageParameters(),
$violation->getMessagePluralization()
Expand All @@ -146,11 +137,12 @@ public function mapViolation(ConstraintViolation $violation, FormInterface $form
* If a matching child is found, it is returned. Otherwise
* null is returned.
*
* @param PropertyPathIteratorInterface $it The iterator at its current position.
* @param FormInterface $form The form to search.
* @param PropertyPathIteratorInterface $it The iterator at its current position.
*
* @return null|FormInterface The found match or null.
*/
private function matchChild(PropertyPathIteratorInterface $it)
private function matchChild(FormInterface $form, PropertyPathIteratorInterface $it)
{
// Remember at what property path underneath "data"
// we are looking. Check if there is a child with that
Expand All @@ -159,6 +151,21 @@ private function matchChild(PropertyPathIteratorInterface $it)
$foundChild = null;
$foundAtIndex = 0;

// Construct mapping rules for the given form
$rules = array();

foreach ($form->getConfig()->getOption('error_mapping') as $propertyPath => $targetPath) {
// Dot rules are considered at the very end
if ('.' !== $propertyPath) {
$rules[] = new MappingRule($form, $propertyPath, $targetPath);
}
}

// Ignore virtual forms when iterating the children
$childIterator = new \RecursiveIteratorIterator(
new VirtualFormAwareIterator($form->all())
);

// Make the path longer until we find a matching child
while (true) {
if (!$it->valid()) {
Expand All @@ -172,7 +179,7 @@ private function matchChild(PropertyPathIteratorInterface $it)
}

// Test mapping rules as long as we have any
foreach ($this->rules as $key => $rule) {
foreach ($rules as $key => $rule) {
/* @var MappingRule $rule */

// Mapping rule matches completely, terminate.
Expand All @@ -182,17 +189,17 @@ private function matchChild(PropertyPathIteratorInterface $it)

// Keep only rules that have $chunk as prefix
if (!$rule->isPrefix($chunk)) {
unset($this->rules[$key]);
unset($rules[$key]);
}
}

// Test children unless we already found one
if (null === $foundChild) {
foreach ($this->children as $child) {
foreach ($childIterator as $child) {
/* @var FormInterface $child */
$childPath = (string) $child->getPropertyPath();

// Child found, move scope inwards
// Child found, mark as return value
if ($chunk === $childPath) {
$foundChild = $child;
$foundAtIndex = $it->key();
Expand All @@ -205,7 +212,7 @@ private function matchChild(PropertyPathIteratorInterface $it)

// If we reached the end of the path or if there are no
// more matching mapping rules, return the found child
if (null !== $foundChild && (!$it->valid() || count($this->rules) === 0)) {
if (null !== $foundChild && (!$it->valid() || count($rules) === 0)) {
// Reset index in case we tried to find mapping
// rules further down the path
$it->seek($foundAtIndex);
Expand Down Expand Up @@ -277,35 +284,12 @@ private function reconstructPath(ViolationPath $violationPath, FormInterface $or
}

/**
* Sets the scope of the mapper to the given form.
*
* The scope is the currently found most specific form that
* an error should be mapped to. After setting the scope, the
* mapper will try to continue to find more specific matches in
* the children of scope. If it cannot, the error will be
* mapped to this scope.
* @param FormInterface $form
*
* @param FormInterface $form The current scope.
*/
private function setScope(FormInterface $form)
{
$this->scope = $form;
$this->children = new \RecursiveIteratorIterator(
new VirtualFormAwareIterator($form->all())
);
foreach ($form->getConfig()->getOption('error_mapping') as $propertyPath => $targetPath) {
// Dot rules are considered at the very end
if ('.' !== $propertyPath) {
$this->rules[] = new MappingRule($form, $propertyPath, $targetPath);
}
}
}

/**
* @return Boolean
*/
private function isValidScope()
private function acceptsErrors(FormInterface $form)
{
return $this->allowNonSynchronized || $this->scope->isSynchronized();
return $this->allowNonSynchronized || $form->isSynchronized();
}
}

0 comments on commit a0661e5

Please sign in to comment.