diff --git a/README.md b/README.md index 6aa39dc..211a77e 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ For reasons that I can't quite imagine, you might only want to just add client s TextType::class, [ 'parsleys' => [ - new \C0ntax\ParsleyBundle\Directive\Field\Constraint\MinLength(2, 'You need more than %s chars'), + new \C0ntax\ParsleyBundle\Parsleys\Directive\Field\Constraint\MinLength(2, 'You need more than %s chars'), ], ] ) @@ -146,7 +146,7 @@ Let's assume that the Entity in the example above is out of your control. It's c TextType::class, [ 'parsleys' => [ - new \C0ntax\ParsleyBundle\Directive\Field\ConstraintErrorMessage(\C0ntax\ParsleyBundle\Directive\Field\Constraint\MinLength::class, 'You need more than %s chars'), + new \C0ntax\ParsleyBundle\Parsleys\Directive\Field\ConstraintErrorMessage(\C0ntax\ParsleyBundle\Parsleys\Directive\Field\Constraint\MinLength::class, 'You need more than %s chars'), ], ] ) @@ -156,6 +156,23 @@ Let's assume that the Entity in the example above is out of your control. It's c *NOTE* The class passed to identify where to attache the error message is the ParsleyBundle one and not the Symfony one! +## Removals + +There may be occassions where you want the bridge between Symfony and Parsley enabled, but specific validations 'removed' from a form element. For example, in the case of [Group Sequences](https://symfony.com/doc/current/validation/sequence_provider.html) where there is no equivalent in Parsley. With removals, you can 'turn off' the Symfony Constraint and manually add your own custom Parsely validation. For example, say we wanted to have a Regex symfony validation on the server, but not on the client side: + +```php + $builder->add( + 'field', + TextType::class, + [ + 'constraints' => [new Regex(['pattern' => '/bla/]), + 'parsleys' => [new RemoveSymfonyConstraint(Regex::class)],] + ] + ); +``` + +There is also the ``RemoveParsleyConstraint()`` class that can be used to remove specific Parsley constrains. This is handy if you want to remove something that was auto-generated from a Symfony Constraint. + ## Rolling your own You can add your own parsley directives by simply implementing the ``DirectiveInterface``. The only requirement is that it passes back an array of attributes that will be injected into your form HTML. diff --git a/composer.json b/composer.json index b631b63..2806f4c 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "c0ntax/parsley-bundle", - "version": "0.4.0", + "version": "0.5.0", "type": "symfony-bundle", "description": "A bridge between Symfony and Parsley.js", "license": "Apache-2.0", diff --git a/src/Contracts/ConstraintInterface.php b/src/Contracts/ConstraintInterface.php index fdee9c7..a2273ff 100644 --- a/src/Contracts/ConstraintInterface.php +++ b/src/Contracts/ConstraintInterface.php @@ -3,12 +3,12 @@ namespace C0ntax\ParsleyBundle\Contracts; -use C0ntax\ParsleyBundle\Directive\Field\ConstraintErrorMessage; +use C0ntax\ParsleyBundle\Parsleys\Directive\Field\ConstraintErrorMessage; /** * Interface ConstraintInterface * - * @package C0ntax\ParsleyBundle\Directive\Field\Constraint + * @package C0ntax\ParsleyBundle\Parsleys\Directive\Field\Constraint */ interface ConstraintInterface extends DirectiveInterface { diff --git a/src/Contracts/DirectiveInterface.php b/src/Contracts/DirectiveInterface.php index 2698431..652271c 100644 --- a/src/Contracts/DirectiveInterface.php +++ b/src/Contracts/DirectiveInterface.php @@ -1,4 +1,5 @@ getAnnotatedConstraintsFromForm($form), - $this->getConstraintsFromForm($form) + $parsleys = $options[self::OPTION_NAME]; + + $symfonyConstraints = $this->removeFromConstraints( + array_merge( + $this->getAnnotatedConstraintsFromForm($form), + $this->getConstraintsFromForm($form) + ), + $this->getRemoveSymfonyConstraintsFromParsleys($parsleys) ); - $parsleyConstraints = array_merge( - $this->createParsleyConstraintsFromValidationConstraints($constraints, $form), - $options[self::OPTION_NAME] + $parsleyConstraints = $this->removeFromConstraints( + array_merge( + $this->createParsleyConstraintsFromValidationConstraints($symfonyConstraints, $form), + $this->getDirectivesFromParsleys($parsleys) + ), + $this->getRemoveParsleyConstraintsFromParsleys($parsleys) ); $this->addParsleyToView($view, $parsleyConstraints); @@ -76,13 +88,89 @@ public function getExtendedType(): string /** * @param OptionsResolver $resolver * @throws \Symfony\Component\OptionsResolver\Exception\AccessException + * @throws \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException */ public function configureOptions(OptionsResolver $resolver): void { - $resolver->setDefaults( - [ - self::OPTION_NAME => [], - ] + $resolver + ->setDefaults( + [ + self::OPTION_NAME => [], + ] + ) + ->addAllowedTypes(self::OPTION_NAME, 'array'); + } + + /** + * @param ParsleyInterface[] $parsleys + * @return DirectiveInterface[] + */ + private function getDirectivesFromParsleys(array $parsleys): array + { + $dir = array_values( + array_filter( + $parsleys, + function (ParsleyInterface $parsley) { + return $parsley instanceof DirectiveInterface; + } + ) + ); + + return $dir; + } + + /** + * @param ParsleyInterface[] $parsleys + * @return RemoveParsleyConstraint[] + */ + private function getRemoveParsleyConstraintsFromParsleys(array $parsleys): array + { + return array_values( + array_filter( + $parsleys, + function (ParsleyInterface $parsley) { + return $parsley instanceof RemoveParsleyConstraint; + } + ) + ); + } + + /** + * @param ParsleyInterface[] $parsleys + * @return RemoveSymfonyConstraint[] + */ + private function getRemoveSymfonyConstraintsFromParsleys(array $parsleys): array + { + return array_values( + array_filter( + $parsleys, + function (ParsleyInterface $parsley) { + return $parsley instanceof RemoveSymfonyConstraint; + } + ) + ); + } + + /** + * @param \Symfony\Component\Validator\Constraint[]|ConstraintInterface[] $constraints + * @param RemoveInterface[] $removals + * @return \Symfony\Component\Validator\Constraint[]|ConstraintInterface[] + */ + private function removeFromConstraints(array $constraints, array $removals): array + { + return array_values( + array_filter( + $constraints, + function ($constraint) use ($removals) { + foreach ($removals as $removal) { + if ($removal->getClassName() === get_class($constraint)) { + return false; + } + } + + return true; + } + ) ); } diff --git a/src/Parsleys/AbstractRemove.php b/src/Parsleys/AbstractRemove.php new file mode 100644 index 0000000..0363778 --- /dev/null +++ b/src/Parsleys/AbstractRemove.php @@ -0,0 +1,57 @@ +setClassName($className); + } + + /** + * @return string + */ + public function getClassName(): string + { + return $this->className; + } + + /** + * @param string $className + * @return AbstractRemove + */ + private function setClassName(string $className): AbstractRemove + { + $this->className = $className; + + return $this; + } +} diff --git a/src/Directive/Field/Constraint/AbstractComparison.php b/src/Parsleys/Directive/Field/Constraint/AbstractComparison.php similarity index 90% rename from src/Directive/Field/Constraint/AbstractComparison.php rename to src/Parsleys/Directive/Field/Constraint/AbstractComparison.php index ad769a1..462497e 100644 --- a/src/Directive/Field/Constraint/AbstractComparison.php +++ b/src/Parsleys/Directive/Field/Constraint/AbstractComparison.php @@ -1,12 +1,12 @@ id1; + } + + /** + * @param null|string $id1 + * @return TestRemovalEntity + */ + public function setId1(?string $id1) + { + $this->id1 = $id1; + + return $this; + } + + /** + * @return null|string + */ + public function getId2(): ?string + { + return $this->id2; + } + + /** + * @param null|string $id2 + * @return TestRemovalEntity + */ + public function setId2(?string $id2) + { + $this->id2 = $id2; + + return $this; + } + + /** + * @return null|string + */ + public function getId3(): ?string + { + return $this->id3; + } + + /** + * @param null|string $id3 + * @return TestRemovalEntity + */ + public function setId3(?string $id3) + { + $this->id3 = $id3; + + return $this; + } +} diff --git a/tests/Fixtures/Form/Type/TestRemovalType.php b/tests/Fixtures/Form/Type/TestRemovalType.php new file mode 100644 index 0000000..670031a --- /dev/null +++ b/tests/Fixtures/Form/Type/TestRemovalType.php @@ -0,0 +1,69 @@ +add( + 'id1', + TextType::class, + [ + 'constraints' => new Regex('/[e]+/'), + 'parsleys' => [ + new MinLength(2, 'You need more than %s chars'), + new ConstraintErrorMessage(MaxLength::class, 'You need less than %s chars'), + new RemoveSymfonyConstraint(Regex::class), + new RemoveSymfonyConstraint(Length::class), + ], + ] + ) + ->add( + 'id2', + TextType::class, + [ + 'constraints' => new Regex('/[e]+/'), + 'parsleys' => [ + new MinLength(2, 'You need more than %s chars'), + new ConstraintErrorMessage(MaxLength::class, 'You need less than %s chars'), + new RemoveParsleyConstraint(MinLength::class), + ], + ] + ) + ->add( + 'id3', + TextType::class, + [ + 'constraints' => new Regex('/[e]+/'), + 'parsleys' => [ + new MinLength(2, 'You need more than %s chars'), + new ConstraintErrorMessage(MaxLength::class, 'You need less than %s chars'), + new RemoveSymfonyConstraint(Regex::class), + new RemoveSymfonyConstraint(Length::class), + new RemoveParsleyConstraint(MinLength::class), + ], + ] + ) + ; + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults(['data_class' => TestRemovalEntity::class]); + } +} diff --git a/tests/Fixtures/Form/Type/TestType.php b/tests/Fixtures/Form/Type/TestType.php index ffcf31f..53872fd 100644 --- a/tests/Fixtures/Form/Type/TestType.php +++ b/tests/Fixtures/Form/Type/TestType.php @@ -2,9 +2,9 @@ namespace C0ntax\ParsleyBundle\Tests\Fixtures\Form\Type; -use C0ntax\ParsleyBundle\Directive\Field\Constraint\MaxLength; -use C0ntax\ParsleyBundle\Directive\Field\Constraint\MinLength; -use C0ntax\ParsleyBundle\Directive\Field\ConstraintErrorMessage; +use C0ntax\ParsleyBundle\Parsleys\Directive\Field\Constraint\MaxLength; +use C0ntax\ParsleyBundle\Parsleys\Directive\Field\Constraint\MinLength; +use C0ntax\ParsleyBundle\Parsleys\Directive\Field\ConstraintErrorMessage; use C0ntax\ParsleyBundle\Tests\Fixtures\Entity\TestEntity; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\BirthdayType; diff --git a/tests/Form/Extension/ParsleyTypeExtensionTest.php b/tests/Form/Extension/ParsleyTypeExtensionTest.php index 0455076..d9a5cca 100644 --- a/tests/Form/Extension/ParsleyTypeExtensionTest.php +++ b/tests/Form/Extension/ParsleyTypeExtensionTest.php @@ -2,13 +2,14 @@ namespace C0ntax\ParsleyBundle\Tests\Form\Extension; +use C0ntax\ParsleyBundle\Tests\Fixtures\Form\Type\TestRemovalType; use C0ntax\ParsleyBundle\Tests\Fixtures\Form\Type\TestType; use C0ntax\ParsleyBundle\Tests\Form\AbstractTypeTestCase; use Symfony\Component\Form\FormInterface; class ParsleyTypeExtensionTest extends AbstractTypeTestCase { - public function testSomething() + public function testWithoutRemoval() { $form = $this->createForm(); $form->submit(['id' => 9, 'email' => 'sausage', 'string' => str_repeat('a', 51)]); @@ -29,6 +30,46 @@ public function testSomething() ); } + public function testRemoval() + { + $form = $this->createRemovalForm(); + $form->submit(['id1' => 9, 'id2' => 10, 'id3' => 11]); + + $view = $form->createView(); + + // Symfony constraint removal + self::assertEquals( + [ + 'data-parsley-trigger' => 'focusout', + 'data-parsley-minlength' => '2', + 'data-parsley-minlength-message' => 'You need more than %s chars', + 'data-parsley-maxlength-message' => 'You need less than %s chars', // TODO Make this not be the case! + ], + $view->children['id1']->vars['attr'] + ); + + // Parsley constraint removal + self::assertEquals( + [ + 'data-parsley-trigger' => 'focusout', + 'data-parsley-pattern-message' => 'This value is not valid.', + 'data-parsley-pattern' => '/[e]+/', + 'data-parsley-maxlength-message' => 'You need less than %s chars', + 'data-parsley-maxlength' => '10', + ], + $view->children['id2']->vars['attr'] + ); + + // Both constraint removal + self::assertEquals( + [ + 'data-parsley-maxlength-message' => 'You need less than %s chars', // TODO Make this not be the case! + 'data-parsley-trigger' => 'focusout', // TODO Make this not be the case! + ], + $view->children['id3']->vars['attr'] + ); + } + protected function getParsleyTypeConfig() { return [ @@ -48,4 +89,13 @@ private function createForm(): FormInterface return $this->factory->create(TestType::class); } + /** + * @return FormInterface + * @throws \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + private function createRemovalForm(): FormInterface + { + return $this->factory->create(TestRemovalType::class); + } + } diff --git a/tests/Directive/Field/Constraint/EmailTest.php b/tests/Parsleys/Directive/Field/Constraint/EmailTest.php similarity index 79% rename from tests/Directive/Field/Constraint/EmailTest.php rename to tests/Parsleys/Directive/Field/Constraint/EmailTest.php index dc61730..3d4811f 100644 --- a/tests/Directive/Field/Constraint/EmailTest.php +++ b/tests/Parsleys/Directive/Field/Constraint/EmailTest.php @@ -1,8 +1,8 @@ ParsleyConstraint\Email::class, + ], + [ + 'className' => ParsleyConstraint\Length::class, + ], + [ + 'className' => ParsleyConstraint\Max::class, + ], + [ + 'className' => ParsleyConstraint\MaxLength::class, + ], + [ + 'className' => ParsleyConstraint\Min::class, + ], + [ + 'className' => ParsleyConstraint\MinLength::class, + ], + [ + 'className' => ParsleyConstraint\Pattern::class, + ], + [ + 'className' => ParsleyConstraint\Range::class, + ], + [ + 'className' => ParsleyConstraint\Required::class, + ], + ]; + } + + public function getInvalidClassNames() + { + return [ + [ + 'className' => ConstraintErrorMessage::class, + ], + [ + 'className' => Generic::class, + ], + [ + 'className' => SymfonyConstraint\Email::class, + ], + [ + 'className' => SymfonyConstraint\Length::class, + ], + [ + 'className' => SymfonyConstraint\Regex::class, + ], + ]; + } +} diff --git a/tests/Parsleys/RemoveSymfonyConstraintTest.php b/tests/Parsleys/RemoveSymfonyConstraintTest.php new file mode 100644 index 0000000..5f34eca --- /dev/null +++ b/tests/Parsleys/RemoveSymfonyConstraintTest.php @@ -0,0 +1,78 @@ + SymfonyConstraint\Email::class, + ], + [ + 'className' => SymfonyConstraint\Length::class, + ], + [ + 'className' => SymfonyConstraint\Regex::class, + ], + ]; + } + + public function getInValidClassNames() + { + return [ + [ + 'className' => ParsleyConstraint\Email::class, + ], + [ + 'className' => ParsleyConstraint\Length::class, + ], + [ + 'className' => ParsleyConstraint\Max::class, + ], + [ + 'className' => ParsleyConstraint\MaxLength::class, + ], + [ + 'className' => ParsleyConstraint\Min::class, + ], + [ + 'className' => ParsleyConstraint\MinLength::class, + ], + [ + 'className' => ParsleyConstraint\Pattern::class, + ], + [ + 'className' => ParsleyConstraint\Range::class, + ], + [ + 'className' => ParsleyConstraint\Required::class, + ], + ]; + } +}