LuyandaSiko LuyandaSiko - 6 months ago 101
PHP Question

Symfony Validator Component issue in Standalone Applicatin

I am realizing that perhaps the way I want to make use of the Validator Component from Symfony is not possible. Here is the idea.

I have a class called Package which for now has only one property named namespace. Usually I would include the ClassMetadata and any constraint object I would like to validate against within my Package class. However, my idea is that instead of doing that I would rather keep my subject clean and only responsible for the things it must be responsible for.

Below is a class I wrote and call it PackageValidater:

<?php
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Validation;

class PackageValidator
{
protected $subject;

public function PackageValidator($subject){
$this->subject = $subject;
}

public static function loadMetadata(){
$metadata->addPropertyConstraint('namespace', new new Assert\Type(['type' => 'string']));
}

public function getViolations(){
$validator = Validation::createValidatorBuilder()
->addMethodMapping('loadMetadata')
->getValidator();

$violations = $validator->validate($this->subject);

return !empty($violations) ? $violations : [];
}

}


Despite of the fact that I am not sure about the usage of my constraint since most reference uses annotations and I do not we can ignore that part. I also am aware of the fact that my test fails due to this fact. However, my issue is with my design because I have not added the static function that the Validation object uses to build the validation. Instead of my method mapping where constraints reside being in the actual object it resides on a separate class.

The idea is to enforce separation of concerns and single responsibility on my objects. Below is a diagram that depicts exactly what I am trying to achieve:

enter image description here

I have written my test as shown below:

$packageValidator = new PackageValidator(new Package([0 => 'test']));
$this->assertTrue(true, empty($packageValidator->getViolations()));


Above I have passed in an array instead of a string which would make my test fail because there can never be a single namespace that is in a form of array - at least not in what I am trying to achieve.

The issue is with my getViolations method inside the PackageValidator object because I am not passing my subject outside the context of my validation process that is define the subject metadata inside the subject itself then when getting the validator object with the refence to the subject's metadata get the validation errors.

All in all Package does not have loadMetadata method but PackageValidator. How can I make this possible without polluting every object I want to validate with the metadata functionality?

Below is what I get from PHPUnit:


SimplexTest\Validate\Package\PackageValidatorTest::testIfValidatorInterfaceWorks
Symfony\Component\Validator\Exception\RuntimeException: Cannot
validate values of type "NULL" automatically. Please provide a
constraint.

Answer

You can use yml or xml configuration to add constraints to your object.

http://symfony.com/doc/current/book/validation.html#the-basics-of-validation

You do this by creating a file called validation.yml in your Bundle configuration directory. Add the following content to validate your object:

Some\Name\Space\Package:
    properties:
        name:
            - NotBlank: ~

That's one way to keep things you don't consider a responsibility for your object out of said object. It also removes the need for a custom validator class for every object you create. You can simply make use of the validator service already provided by the framework.

Edit

Alright I think I figured something out you might be looking for: you can create a MetadataFactory to load Metadata the way you want. There are a couple of examples here: https://github.com/symfony/validator/tree/master/Mapping/Factory

It basically boils down to a Factory class that returns an instance of MetadataInterface where you attach your constraints. This means that you can have the Factory read metadata from anything. You could for example do something like this:

use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Mapping\Factory\MetadataFactoryInterface;
use Your\Package;

class PackageMetadataFactory implements MetadataFactoryInterface
{
    /**
     * Create a ClassMetaData object for your Package object
     *
     * @param object $value The object that will be validated
     */
    public function getMetadataFor($value)
    {

        // Create a class meta data object for your entity
        $metadata = new ClassMetadata(Package::class);

        // Add constraints to your metadata
        $metadata->addPropertyConstraint(
            'namespace', new Assert\Type(['type' => 'string']));

        // Return the class metadata object
        return $metadata;
    }

    /**
     * Test if the value provided is actually of type Package
     *
     * @param object $value The object that will be validated
     */
    public function hasMetadataForValue($value)
    {
        return $value instanceof Package::class;
    }
}

Then in your PackageValidator all you have to do is:

use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Validation;
use Your\PackageMetadataFactory;

class PackageValidator
{
    protected $subject;

    public function PackageValidator($subject) {
        $this->subject = $subject;
    }

    public function getViolations() {
        $validator = Validation::createValidatorBuilder()
            ->setMetadataFactory(new PackageMetadataFactory())
            ->getValidator();

        $violations = $validator->validate($this->subject);

        return !empty($violations) ? $violations : [];
    }

}

Hopefully this is more in line of what you're looking for.