Skip to content

Instantly share code, notes, and snippets.

@maarekj
Last active July 9, 2020 13:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save maarekj/6b61bf1d5183a804259ba1dc2464304f to your computer and use it in GitHub Desktop.
Save maarekj/6b61bf1d5183a804259ba1dc2464304f to your computer and use it in GitHub Desktop.
Example using of SameAs
<?php
namespace App\Validator;
use Symfony\Component\Validator\Constraint;
/**
* Constraint for the Unique Entity validator.
*
* @Annotation
* @Target({"PROPERTY", "ANNOTATION"})
*/
class SameAs extends Constraint
{
/** @var string */
public $class;
/** @var string|null */
public $property;
/** @var array|null */
public $targetGroups;
/** {@inheritdoc} */
public function getDefaultOption()
{
return 'class';
}
/** {@inheritdoc} */
public function getRequiredOptions()
{
return ['class'];
}
}
<?php
namespace App\Validator;
use Illuminate\Support\Arr;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Mapping\ClassMetadataInterface;
use Symfony\Component\Validator\Mapping\PropertyMetadata;
final class SameAsValidator extends ConstraintValidator
{
/** {@inheritdoc} */
public function validate($value, Constraint $constraint)
{
if (!$constraint instanceof SameAs) {
throw new UnexpectedTypeException($constraint, SameAs::class);
}
$class = $constraint->class;
$property = null === $constraint->property ? $this->context->getPropertyName() : $constraint->property;
if (null === $property) {
throw new UnexpectedTypeException($property, 'string');
}
$targetGroups = (array) (null === $constraint->targetGroups ? $this->context->getGroup() : $constraint->targetGroups);
$classMetadata = $this->context->getValidator()->getMetadataFor($class);
if (!$classMetadata instanceof ClassMetadataInterface) {
throw new UnexpectedTypeException($classMetadata, ClassMetadataInterface::class);
}
$propertyMetadatas = $classMetadata->getPropertyMetadata($property);
$constraints = Arr::flatten(\array_map(function (PropertyMetadata $propertyMetadata) {
return $propertyMetadata->getConstraints();
}, $propertyMetadatas), 1);
$this->context->getValidator()->inContext($this->context)->validate($value, $constraints, $targetGroups);
}
}
<?php
use App\Validator\SameAs;
final class EditPost {
/** @var Post */
public $post;
/**
* @var string|null
* @SameAs(class=Post::class, property="slug")
*/
public $slug;
/**
* @var string|null
* @SameAs(class=Post::class, property="content")
*/
public $content;
/**
* @var Category|null
* @SameAs(class=Post::class, property="category")
*/
public $category;
public function __construct(Post $post) {
$this->post = $post;
$this->slug = $post->getSlug();
$this->content = $post->getContent();
$this->category = $post->getCategory();
}
}
@Pixelshaped
Copy link

Pixelshaped commented Dec 14, 2019

Hi!
Would you care to share the code for your SameAs validator? Seems like a neat way to handle DTOs!
Thanks!

@maarekj
Copy link
Author

maarekj commented Jan 15, 2020

Hi!
Would you care to share the code for your SameAs validator? Seems like a neat way to handle DTOs!
Thanks!

I just added the SameAs.php and SameAsValidator.php in revision 3 of this gist.

@Pixelshaped
Copy link

Well thank you my good sir!

@Pixelshaped
Copy link

Pixelshaped commented Jul 9, 2020

I've noticed that the SameAs validator doesn't work on Constraints that are cascading.

I ended up using the cached annotation reader, here's my full code for the SameAsValidator.php class

<?php
declare(strict_types=1);

namespace App\Validator;

use App\Utils\Utils;
use Doctrine\Common\Annotations\Reader;
use ReflectionProperty;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Mapping\CascadingStrategy;
use Symfony\Component\Validator\Mapping\ClassMetadataInterface;
use Symfony\Component\Validator\Mapping\PropertyMetadata;

/**
 * Validator handling the SameAs constraint
 * Class SameAsValidator
 * @package App\Validator
 */
final class SameAsValidator extends ConstraintValidator
{
    /**
     * @var Reader
     */
    private $docReader;

    public function __construct(Reader $docReader)
    {
        $this->docReader = $docReader;
    }

    /** {@inheritdoc} */
    public function validate($value, Constraint $constraint)
    {
        // Check against constraint type
        if (!$constraint instanceof SameAs) {
            throw new UnexpectedTypeException($constraint, SameAs::class);
        }

        $class = $constraint->class;
        // Getting the property of the class
        $property = null === $constraint->property ? $this->context->getPropertyName() : $constraint->property;

        if (null === $property) {
            throw new UnexpectedTypeException($property, 'string');
        }

        // Getting target group
        $targetGroups = (array)(null === $constraint->targetGroups ? $this->context->getGroup() : $constraint->targetGroups);
        // Metadata
        $classMetadata = $this->context->getValidator()->getMetadataFor($class);

        if (!$classMetadata instanceof ClassMetadataInterface) {
            throw new UnexpectedTypeException($classMetadata, ClassMetadataInterface::class);
        }

        // Metadata of targeted property
        $propertyMetaDatas = $classMetadata->getPropertyMetadata($property);
        // Retrieving all constraints from targeted class property
        $constraints = Utils::array_flatten(array_map(function (PropertyMetadata $propertyMetadata) use ($class, $property) {
            if(empty($propertyMetadata->getConstraints()) && $propertyMetadata->getCascadingStrategy() === CascadingStrategy::CASCADE) {
                $reflectionProperty = new ReflectionProperty($class, $property);
                $propertyAnnotations = $this->docReader->getPropertyAnnotations($reflectionProperty);
                $cascadedConstraints = [];
                foreach($propertyAnnotations as $annotation) {
                    if($annotation instanceof Constraint) {
                        $cascadedConstraints[] = $annotation;
                    }
                }
                return $cascadedConstraints;
            }
            return $propertyMetadata->getConstraints();
        }, $propertyMetaDatas));

        // Apply all constraints in this validation context
        $this->context->getValidator()->inContext($this->context)->validate($value, $constraints, $targetGroups);
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment