Bladeren bron

Add an Inline Constraint/Validator

Thomas Rabaix 14 jaren geleden
bovenliggende
commit
4022538f66

+ 1 - 0
DependencyInjection/SonataAdminExtension.php

@@ -52,6 +52,7 @@ class SonataAdminExtension extends Extension
         $loader->load('twig.xml');
         $loader->load('core.xml');
         $loader->load('form_types.xml');
+        $loader->load('validator.xml');
 
         $configuration = new Configuration();
         $processor = new Processor();

+ 16 - 0
Resources/config/validator.xml

@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+
+<container xmlns="http://symfony.com/schema/dic/services"
+           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+           xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
+
+    <services>
+        <service id="sonata.admin.validator.inline" class="Sonata\AdminBundle\Validator\InlineValidator">
+            <argument type="service" id="service_container" />
+            <argument type="service" id="validator.validator_factory" />
+
+            <tag name="validator.constraint_validator" alias="sonata.admin.validator.inline" />
+        </service>
+    </services>
+
+</container>

+ 2 - 0
Resources/doc/index.rst

@@ -25,6 +25,8 @@ Reference Guide
    reference/routing
    reference/dashboard
    reference/security
+   reference/inline_validation
+
 
 Doctrine ORM
 ------------

+ 97 - 0
Resources/doc/reference/inline_validation.rst

@@ -0,0 +1,97 @@
+Inline Validation
+=================
+
+The inline validation is about delegating model validation to a dedicated service.
+The current validation implementation built in the Symfony2 framework is very powerful
+as it allows to declare validation on : class, field and getter. However these declaration
+can take a while to code for complexe rules. As a rules must be a set of a ``Constraint``
+and ``Validator`` instance.
+
+The inline validation try to provide a nice solution by introducting a ``ErrorElement``
+object. The object can be use to check assertion against a model :
+
+.. code-block:: php
+
+        $errorElement
+            ->with('settings.url')
+                ->assertNotNull(array())
+                ->assertNotBlank()
+            ->end()
+            ->with('settings.title')
+                ->assertNotNull(array())
+                ->assertNotBlank()
+                ->assertMinLength(array('limit' => 50))
+                ->addViolation('ho yeah!')
+            ->end();
+
+        if (/* complex rules */) {
+            $errorElement->with('value')->addViolation('Fail to check the complex rules')->end()
+        }
+
+        /* conditional validation */
+        if ($this->getSubject()->getState() == Post::STATUS_ONLINE) {
+            $errorElement
+                ->with('enabled')
+                    ->assertNotNull()
+                    ->assertTrue()
+                ->end()
+        }
+
+Please note, this solution rely on the validator component so validation defined through
+the validator component will be used.
+
+Using this validator
+--------------------
+
+Just add the ``InlineConstraint`` class constraint, like this :
+
+.. code-block:: xml
+
+    <class name="Application\Sonata\PageBundle\Entity\Block">
+        <constraint name="Sonata\AdminBundle\Validator\Constraints\InlineConstraint">
+            <option name="service">sonata.page.cms.page</option>
+            <option name="method">validateBlock</option>
+        </constraint>
+    </class>
+
+There are two important options :
+
+  - ``service`` : the service where the validation method is defined
+  - ``method``  : the service's method to call
+
+The method must accept two arguments :
+
+ - ``ErrorElement`` : the instance where assertion can be check
+ - ``value``  : the object instance
+
+
+Sample with the ``PageBundle``
+------------------------------
+
+.. code-block:: php
+
+    <?php
+    namespace Sonata\PageBundle\Block;
+
+    use Sonata\PageBundle\Model\PageInterface;
+    use Sonata\AdminBundle\Validator\ErrorElement;
+
+    class RssBlockService extends BaseBlockService
+    {
+        // ... code removed for simplification
+
+        function validateBlock(ErrorElement $errorElement, BlockInterface $block)
+        {
+            $errorElement
+                ->with('settings.url')
+                    ->assertNotNull(array())
+                    ->assertNotBlank()
+                ->end()
+                ->with('settings.title')
+                    ->assertNotNull(array())
+                    ->assertNotBlank()
+                    ->assertMinLength(array('limit' => 50))
+                    ->addViolation('ho yeah!')
+                ->end();
+        }
+    }

+ 51 - 0
Validator/Constraints/InlineConstraint.php

@@ -0,0 +1,51 @@
+<?php
+/*
+ * This file is part of the Sonata package.
+ *
+ * (c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Sonata\AdminBundle\Validator\Constraints;
+
+use Symfony\Component\Validator\Constraint;
+
+class InlineConstraint extends Constraint
+{
+    protected $service;
+
+    protected $method;
+
+    public function validatedBy()
+    {
+        return 'sonata.admin.validator.inline';
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function getTargets()
+    {
+        return self::CLASS_CONSTRAINT;
+    }
+
+    public function getRequiredOptions()
+    {
+        return array(
+            'service',
+            'method'
+        );
+    }
+
+    public function getMethod()
+    {
+      return $this->method;
+    }
+
+    public function getService()
+    {
+      return $this->service;
+    }
+}

+ 143 - 0
Validator/ErrorElement.php

@@ -0,0 +1,143 @@
+<?php
+/*
+ * This file is part of the Sonata package.
+ *
+ * (c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Sonata\AdminBundle\Validator;
+
+use Symfony\Bundle\FrameworkBundle\Validator\ConstraintValidatorFactory;
+use Symfony\Component\Validator\Exception\UnexpectedTypeException;
+use Symfony\Component\Validator\ExecutionContext;
+use Symfony\Component\Form\Util\PropertyPath;
+
+class ErrorElement
+{
+    protected $context;
+
+    protected $group;
+
+    protected $constraintValidatorFactory;
+
+    protected $stack = array();
+
+    protected $propertyPaths = array();
+
+    protected $subject;
+
+    protected $current = '';
+
+    public function __construct($subject, ConstraintValidatorFactory $constraintValidatorFactory, ExecutionContext $context, $group)
+    {
+        $this->subject = $subject;
+        $this->context = $context;
+        $this->group   = $group;
+        $this->constraintValidatorFactory = $constraintValidatorFactory;
+    }
+
+    public function __call($name, array $arguments = array())
+    {
+        if (substr($name, 0, 6) == 'assert') {
+            $this->validate($this->newConstraint(
+                substr($name, 6),
+                isset($arguments[0]) ? $arguments[0] : array()
+            ));
+        } else {
+            throw new \RunTimeException('Unable to recognize the command');
+        }
+
+        return $this;
+    }
+
+    public function with($name, $key = false)
+    {
+        $key = $key ? $name.'.'.$key : $name;
+        $this->stack[] = $key;
+
+        $this->current = implode('.', $this->stack);
+
+        if (!isset($this->propertyPaths[$this->current])) {
+            $this->propertyPaths[$this->current] = new PropertyPath($this->current);
+        }
+
+        return $this;
+    }
+
+    public function end()
+    {
+        array_pop($this->stack);
+
+        $this->current = implode('.', $this->stack);
+
+        return $this;
+    }
+
+    protected function validate($constraint)
+    {
+        $validator  = $this->constraintValidatorFactory->getInstance($constraint);
+        $value      = $this->getValue();
+
+        $validator->isValid($value, $constraint);
+
+        $this->context->setPropertyPath($this->propertyPaths[$this->current]);
+        $this->context->setGroup($this->group);
+
+        $validator->initialize($this->context);
+
+        if (!$validator->isValid($value, $constraint)) {
+            $this->context->addViolation(
+                $validator->getMessageTemplate(),
+                $validator->getMessageParameters(),
+                $value
+            );
+        }
+    }
+
+    /**
+     * Return the value linked to
+     *
+     * @return mixed
+     */
+    protected function getValue()
+    {
+        return $this->propertyPaths[$this->current]->getValue($this->subject);
+    }
+
+    public function getSubject()
+    {
+        return $this->subject;
+    }
+
+    protected function newConstraint($name, $options)
+    {
+        if (strpos($name, '\\') !== false && class_exists($name)) {
+            $className = (string) $name;
+        } else {
+            $className = 'Symfony\\Component\\Validator\\Constraints\\'.$name;
+        }
+
+        return new $className($options);
+    }
+
+    public function addViolation($error)
+    {
+        $this->context->setPropertyPath($this->propertyPaths[$this->current]);
+        $this->context->setGroup($this->group);
+
+        if (!is_array($error)) {
+            $this->context->addViolation($error, array(), null);
+        } else {
+            $this->context->addViolation(
+                isset($error[0]) ? $error[0] : 'error',
+                isset($error[1]) ? (array)$error[1] : array(),
+                isset($error[2]) ? $error[2] : null
+            );
+        }
+
+        return $this;
+    }
+}

+ 44 - 0
Validator/InlineValidator.php

@@ -0,0 +1,44 @@
+<?php
+/*
+ * This file is part of the Sonata package.
+ *
+ * (c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Sonata\AdminBundle\Validator;
+
+use Symfony\Component\Validator\ConstraintValidator;
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Bundle\FrameworkBundle\Validator\ConstraintValidatorFactory;
+use Sonata\AdminBundle\Validator\ErrorElement;
+
+class InlineValidator extends ConstraintValidator
+{
+    protected $container;
+
+    public function __construct(ContainerInterface $container, ConstraintValidatorFactory $constraintValidatorFactory)
+    {
+        $this->container = $container;
+        $this->constraintValidatorFactory = $constraintValidatorFactory;
+    }
+
+    public function isValid($value, Constraint $constraint)
+    {
+        $service = $this->container->get($constraint->getService());
+
+        $errorElement = new ErrorElement(
+            $value,
+            $this->constraintValidatorFactory,
+            $this->context,
+            $this->context->getGroup()
+        );
+
+        call_user_func(array($service, $constraint->getMethod()), $errorElement, $value);
+
+        return count($this->context->getViolations()) == 0;
+    }
+}