Skip to content
This repository has been archived by the owner on Mar 28, 2022. It is now read-only.

Commit

Permalink
Add context validation errors and write more documetnation
Browse files Browse the repository at this point in the history
  • Loading branch information
DASPRiD committed Jun 9, 2016
1 parent 22dabbf commit edd3f8b
Show file tree
Hide file tree
Showing 9 changed files with 146 additions and 14 deletions.
97 changes: 97 additions & 0 deletions doc/src/constraints.md
@@ -0,0 +1,97 @@
All build-in mappings support additionally validation. To assign a constraint to a mapping, you can call the
`verifying(ConstraintInterface $constraint)` method on it, which will return a new instance of the mapping with the
constraint added to it. You can call the method multiple times to generated an object with multiple constraints
assigned.

While Formidable ships with a small set of constraints, those are primarily consumed by the `FieldMappingFactory`, so
generally you will want to write your own constraints. To do so, create a new class which implements the
`ConstaintInterface`. That class will have a single method `__invoke($value)', which must return a `ValidationResult`.
In case an empty validation result is returned, it is considered successful.

A constraint always gets the converted value passed. So in case of a field mapping, you'll get a string, float, integer
or similar. In case of an object mapping, you'll get an object and a repeated mapping would give you an array of the
wrapped mappings.

# Creating a simple constraint

Let's say you want to verify that an input matches a concrete pattern, your constraint could look like this:

```php
<?php
use Assert\Assertion;
use DASPRiD\Formidable\Mapping\Constraint\ConstraintInterface;

class PatternConstraint implements ConstraintInterface
{
public function __invoke($value) : ValidationResult
{
Assertion::string($value);

if (!preg_match('(^[a-z]{5}$)', $value)) {
return new ValidationResult(new ValidationError('error.pattern'));
}

return new ValidationResult();
}
}
```

To assign the new constraint to a mapping, you could do something like this:

```php
<?php
$mapping = FieldMappingFactory::text()->verifying(new PatternConstraint());
```

!!!note "Type Assertion"
You may ask yourself, what the assertion is for. Theoretically, you should always receive a string here, as long
as you assign the constraint to the correct mapping. But since we do not have generics yet, you should actually
assert the input type which you receive.

You can install the assertion library via composer:

```
$ composer require beberlei/assert
```

# Context validation

Sometimes you need to validate fields based on other fields in the form. Instead of assigning a constraint to the
specific field, where you wouldn't know the context, you actually assign it to the parent object. For example, you may
want to validate that two passwords are equal:

```php
<?php
use Assert\Assertion;
use DASPRiD\Formidable\Mapping\Constraint\ConstraintInterface;

class PasswordConfirmationConstraint implements ConstraintInterface
{
public function __invoke($value) : ValidationResult
{
Assertion::instanceOf($value, UserFormData::class);

if ($value->getPassword() !== $value->getPasswordConfirm()) {
return new ValidationResult(new ValidationError('error.password-mismatch', [], 'passwordConfirm'));
}

return new ValidationResult();
}
}
```

Now when creating your mapping, you would assign the constraint to the object mapping:

```php
<?php
use DASPRiD\Formidable\Mapping\FieldMappingFactory;

$mapping = (new ObjectMapping([
'password' => FieldMappingFactory::text(1),
'passwordConfirm' => FieldMappingFactory::text(),
], UserFormData::class))->verifying(new PasswordConfirmationConstraint());
```

As you probably noted, we passed a third parameter to the `ValidationError` in our constraint. This specifies to which
child mapping the validation error should be assigned. If we had omitted that parameter, the error would have been
assigned to the object mapping, which in case of a root mapping would have resulted in a global form error.
9 changes: 1 addition & 8 deletions doc/src/mappings.md
Expand Up @@ -69,7 +69,7 @@ mapping during construction. Formidable comes with a set of standard formatters
field mappings with them. The `FieldMappingFactory` has the following methods which will all return configured field
mappings:

- `text($minLength = 0, $maxLength = null, $encoding = 'utf-8')`<br />
- `text($minLength = 0, $maxLength = null, $encoding = 'utf-8')`<br />
Creates a simple string-to-string mapping, optionally applying constraints for the length of the string.

- `emailAddress()`<br />
Expand Down Expand Up @@ -104,10 +104,3 @@ mappings:
`datetime`, you don't have to set the `$localTime` parameter. When using the `datetime-local` type, the browser will
not submit a time zone, so it is important to pass a time zone to the factory in which the datetime should be
interpreted, if not UTC, and set `$localTime` to `true`.

# Validation

All build-in mappings support additionally validation. To assign a constraint to a mapping, you can call the
`verifying(ConstraintInterface $constraint)` method on it, which will return a new instance of the mapping with the
constraint added to it. You can call the method multiple times to generated an object with multiple constraints
assigned.
3 changes: 3 additions & 0 deletions mkdocs.yml
Expand Up @@ -10,4 +10,7 @@ pages:
- 'Rendering Forms' : rendering-forms.md
- 'Build-in Helpers' : build-in-helpers.md
- 'Mappings' : mappings.md
- 'Constraints' : constraints.md

markdown_extensions:
- admonition:
13 changes: 12 additions & 1 deletion src/Mapping/Constraint/ValidationError.php
Expand Up @@ -15,10 +15,16 @@ final class ValidationError
*/
private $arguments;

public function __construct(string $message, array $arguments = [])
/**
* @var string
*/
private $keySuffix;

public function __construct(string $message, array $arguments = [], string $keySuffix = '')
{
$this->message = $message;
$this->arguments = $arguments;
$this->keySuffix = $keySuffix;
}

public function getMessage() : string
Expand All @@ -30,4 +36,9 @@ public function getArguments() : array
{
return $this->arguments;
}

public function getKeySuffix() : string
{
return $this->keySuffix;
}
}
14 changes: 13 additions & 1 deletion src/Mapping/MappingTrait.php
Expand Up @@ -36,7 +36,19 @@ protected function applyConstraints($value, string $key) : BindResult

return BindResult::fromFormErrors(...array_map(
function (ValidationError $validationError) use ($key) {
return new FormError($key, $validationError->getMessage(), $validationError->getArguments());
if ('' === $key) {
$finalKey = $validationError->getKeySuffix();
} elseif ('' === $validationError->getKeySuffix()) {
$finalKey = $key;
} else {
$finalKey = $key . preg_replace('(^[^\[]+)', '[\0]', $validationError->getKeySuffix());
}

return new FormError(
$finalKey,
$validationError->getMessage(),
$validationError->getArguments()
);
},
iterator_to_array($validationResult->getValidationErrors())
));
Expand Down
12 changes: 12 additions & 0 deletions test/Mapping/Constraint/ValidationErrorTest.php
Expand Up @@ -20,4 +20,16 @@ public function testArgumentsRetrieval()
{
$this->assertSame(['foo'], (new ValidationError('', ['foo']))->getArguments());
}

public function testKeySuffixRetrieval()
{
$this->assertSame('foo', (new ValidationError('', [], 'foo'))->getKeySuffix());
}

public function testDefaults()
{
$validationError = new ValidationError('');
$this->assertSame([], $validationError->getArguments());
$this->assertSame('', $validationError->getKeySuffix());
}
}
1 change: 1 addition & 0 deletions test/Mapping/FieldMappingTest.php
Expand Up @@ -62,6 +62,7 @@ public function testBindAppliesConstraints()
$bindResult = $mapping->bind($data);
$this->assertFalse($bindResult->isSuccess());
$this->assertSame('bar', $bindResult->getFormErrorSequence()->getIterator()->current()->getMessage());
$this->assertSame('foo', $bindResult->getFormErrorSequence()->getIterator()->current()->getKey());
}

public function testUnbind()
Expand Down
8 changes: 5 additions & 3 deletions test/Mapping/ObjectMappingTest.php
Expand Up @@ -82,8 +82,8 @@ public function testBindAppliesConstraints()
{
$constraint = $this->prophesize(ConstraintInterface::class);
$constraint->__invoke(Argument::type(SimpleObject::class))->willReturn(new ValidationResult(
new ValidationError('error')
))->shouldBeCalled();
new ValidationError('error', [], 'foo[bar]')
));

$data = Data::fromFlatArray(['foo' => 'baz', 'bar' => 'bat']);
$objectMapping = (new ObjectMapping([
Expand All @@ -93,7 +93,9 @@ public function testBindAppliesConstraints()

$bindResult = $objectMapping->bind($data);
$this->assertFalse($bindResult->isSuccess());
$this->assertSame('error', iterator_to_array($bindResult->getFormErrorSequence())[0]->getMessage());
$formError = iterator_to_array($bindResult->getFormErrorSequence())[0];
$this->assertSame('error', $formError->getMessage());
$this->assertSame('foo[bar]', $formError->getKey());
}

public function testUnbindObject()
Expand Down
3 changes: 2 additions & 1 deletion test/Mapping/RepeatedMappingTest.php
Expand Up @@ -83,14 +83,15 @@ public function testBindAppliesConstraintsToValidResult()
$wrappedMapping->bind($data)->willReturn(BindResult::fromValue('baz'));

$constraint = $this->prophesize(ConstraintInterface::class);
$constraint->__invoke(['baz'])->willReturn(new ValidationResult(new ValidationError('bar')));
$constraint->__invoke(['baz'])->willReturn(new ValidationResult(new ValidationError('bar', [], '0')));

$mapping = (new RepeatedMapping($wrappedMapping->reveal()))->withPrefixAndRelativeKey('foo', 'bar')->verifying(
$constraint->reveal()
);
$bindResult = $mapping->bind($data);
$this->assertFalse($bindResult->isSuccess());
$this->assertSame('bar', $bindResult->getFormErrorSequence()->getIterator()->current()->getMessage());
$this->assertSame('foo[bar][0]', $bindResult->getFormErrorSequence()->getIterator()->current()->getKey());
}

public function testUnbindInvalidValue()
Expand Down

0 comments on commit edd3f8b

Please sign in to comment.