<p dir="auto">Version: symfony/validator 3.1.6, PHP 7.0.7</p>
<p dir="auto"><strong>Background:</strong> I've been using the validator component for APIs for a long time (without forms though). One of the problems was that apart from the descriptive violation messages I wanted to return a fixed error name that can be handled by the application on the other end. I've achieved that by extending the base constraints and overriding the error messages (which means no descriptive message). I could live with that, waited for better times.</p>
<p dir="auto">I noticed that the ConstraintViolation includes the <code class="notranslate">::getCode</code> method now! Perfect for my little APIs. The code seems to be an UUID and each of the constraints includes a mapping to nice error names. UUIDs are usable but they are not the best UX for the end-programmer :).</p>
<p dir="auto"><strong>The meat</strong>: The easiest way to get the error name would be to use <code class="notranslate">Constraint::getErrorName</code> method. This is where the problem is - in some cases the <code class="notranslate">ConstraintViolation</code> has the wrong <code class="notranslate">Constraint</code> attached. From the first look - it's the last tested constraint, not the one that failed.</p>
<p dir="auto">I am aware that I am assuming the the <code class="notranslate">ConstraintViolationInterface</code> is actually a <code class="notranslate">ConstraintViolation</code> but IMO it's fair to assume when I'm using Symfony's validator component. Also the <code class="notranslate">ConstraintViolation::getConstraint</code> and <code class="notranslate">Constraint::getErrorName</code> methods are public so I am (I hope) not relying on some internal implementation details.</p>
<p dir="auto">I am attaching a testcase for further illustration:<br>
<a href="https://gist.github.com/pinkeen/7a3b6688efc3c493fc16807ac89d3a3f">https://gist.github.com/pinkeen/7a3b6688efc3c493fc16807ac89d3a3f</a></p>
<p dir="auto">Hi,</p>
<p dir="auto">It's possible that when calling the <code class="notranslate">getConstraint()</code> method on a <code class="notranslate">ConstraintViolation</code> generated by the <code class="notranslate">Assert\Collection</code> will return a different constraint than the <code class="notranslate">Assert\Collection</code> that caused the violation.</p>
<p dir="auto">An example:</p>
<div class="highlight highlight-text-html-php notranslate position-relative overflow-auto" dir="auto" data-snippet-clipboard-copy-content="$data = ['email' =&gt; 'some.email@domain.com'];

$constraints = new Assert\Collection([
    'password' =&gt; new Assert\NotBlank(),
    'email'    =&gt; new Assert\NotBlank(),
]);

$violations = $-&gt;validate($data, $constraints);

dump($violations);"><pre class="notranslate"><span class="pl-s1"><span class="pl-c1">$</span>data</span> = [<span class="pl-s">'email'</span> =&gt; <span class="pl-s">'some.email@domain.com'</span>];

<span class="pl-s1"><span class="pl-c1">$</span>constraints</span> = <span class="pl-k">new</span> <span class="pl-v">Assert</span>\<span class="pl-v">Collection</span>([
    <span class="pl-s">'password'</span> =&gt; <span class="pl-k">new</span> <span class="pl-v">Assert</span>\<span class="pl-v">NotBlank</span>(),
    <span class="pl-s">'email'</span>    =&gt; <span class="pl-k">new</span> <span class="pl-v">Assert</span>\<span class="pl-v">NotBlank</span>(),
]);

<span class="pl-s1"><span class="pl-c1">$</span>violations</span> = <span class="pl-s1"><span class="pl-c1">$</span>-&gt;validate</span>(<span class="pl-s1"><span class="pl-c1">$</span>data</span>, <span class="pl-s1"><span class="pl-c1">$</span>constraints</span>);

dump(<span class="pl-s1"><span class="pl-c1">$</span>violations</span>);</pre></div>
<p dir="auto">Outputs</p>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="Symfony\Component\Validator\ConstraintViolationList {#2675
  -violations: array:1 [
    0 =&gt; Symfony\Component\Validator\ConstraintViolation {#2677
      -message: &quot;This field is missing.&quot;
      --- &gt;8 ---
      -constraint: Symfony\Component\Validator\Constraints\Collection { --- &gt;8 --- }
      -code: 1
    }
  ]
}"><pre class="notranslate"><code class="notranslate">Symfony\Component\Validator\ConstraintViolationList {#2675
  -violations: array:1 [
    0 =&gt; Symfony\Component\Validator\ConstraintViolation {#2677
      -message: "This field is missing."
      --- &gt;8 ---
      -constraint: Symfony\Component\Validator\Constraints\Collection { --- &gt;8 --- }
      -code: 1
    }
  ]
}
</code></pre></div>
<p dir="auto">This is the correct and expected behavior. The constraint within the <code class="notranslate">ConstraintViolation</code> maps correctly to the constraint that caused the violation.</p>
<p dir="auto">However, if we were to flip the order of field checks in the <code class="notranslate">CollectionConstraint</code> like so:</p>
<div class="highlight highlight-text-html-php notranslate position-relative overflow-auto" dir="auto" data-snippet-clipboard-copy-content="$constraints = new Assert\Collection([
    'email'    =&gt; new Assert\NotBlank(),
    'password' =&gt; new Assert\NotBlank(),
]);"><pre class="notranslate"><span class="pl-s1"><span class="pl-c1">$</span>constraints</span> = <span class="pl-k">new</span> <span class="pl-v">Assert</span>\<span class="pl-v">Collection</span>([
    <span class="pl-s">'email'</span>    =&gt; <span class="pl-k">new</span> <span class="pl-v">Assert</span>\<span class="pl-v">NotBlank</span>(),
    <span class="pl-s">'password'</span> =&gt; <span class="pl-k">new</span> <span class="pl-v">Assert</span>\<span class="pl-v">NotBlank</span>(),
]);</pre></div>
<p dir="auto">The output will look like this</p>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="Symfony\Component\Validator\ConstraintViolationList {#2675
  -violations: array:1 [
    0 =&gt; Symfony\Component\Validator\ConstraintViolation {#2677
      -message: &quot;This field is missing.&quot;
      --- &gt;8 ---
      -constraint: Symfony\Component\Validator\Constraints\NotBlank { --- &gt;8 --- }
      -code: 1
    }
  ]
}"><pre class="notranslate"><code class="notranslate">Symfony\Component\Validator\ConstraintViolationList {#2675
  -violations: array:1 [
    0 =&gt; Symfony\Component\Validator\ConstraintViolation {#2677
      -message: "This field is missing."
      --- &gt;8 ---
      -constraint: Symfony\Component\Validator\Constraints\NotBlank { --- &gt;8 --- }
      -code: 1
    }
  ]
}
</code></pre></div>
<p dir="auto"><strong>The constraint in the <code class="notranslate">ConstraintViolation</code> is now mismatched from what the actual violation is</strong>.</p>
<h4 dir="auto">Investigation</h4>
<p dir="auto">The reason this is happening is because of <a href="https://github.com/symfony/symfony/blob/f187d9aa1cfbf963ff22cb0944efdf51af4d18a6/src/Symfony/Component/Validator/Constraints/CollectionValidator.php#L62-L65">the way the context is passed into the validator</a> when validating items in the <code class="notranslate">Assert\Collection</code>.</p>
<p dir="auto">Since the context is passed in, the constraint is <a href="https://github.com/symfony/symfony/blob/f187d9aa1cfbf963ff22cb0944efdf51af4d18a6/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php#L859">modified on it</a> and this change persists during the validation of the rest of the fields in the Collection constraint. So if a field is missing the violation created will be created with the same context that no longer references the Collection constraint.</p>
<h4 dir="auto">Proposed solution</h4>
<p dir="auto"><code class="notranslate">clone</code>ing the context <a href="https://github.com/symfony/symfony/blob/f187d9aa1cfbf963ff22cb0944efdf51af4d18a6/src/Symfony/Component/Validator/Constraints/CollectionValidator.php#L63">in the <code class="notranslate">inContext</code> call</a> appears to fix the problem. I haven't dived in deep enough though to fully understand if there would be other side effects.</p>