Jump to content
NotionCommotion

What is the entities roll for validation?

Recommended Posts

For user content validation, I built my own custom class which is injected with an array which specified how the data will be validated and how to respond to the user if invalid data.  It has a method to validate server side as well as a method to produce the jQuery validation plugin's client configuration JSON.  The configuration array is defined by one or more JSON file located on the server.

I am trying to determine where I should define which files should be used to validate data for a given entity.  The easiest location is to store them directly in the entity in question, however, I have heard people voice that the entity shouldn't be responsible for validation.  It isn't directly responsible how I am doing this, but would like to know whether there are any specific reasons why doing so might have undesirable consequences.  Alternately, I can have them defined in the repository, however, it requires more logic.

    public function update(int $idPublic, array $params): Entity {

        $entity=$this->read($idPublic);

        //Option 1
        $validationFiles=$entity->getValidatorFiles();

        //Option 2
        $repo=$this->em->getRepository(get_class($entity));
        $validationFiles=$repo->getValidatorFiles();
        
        $rules=$this->filesToArr($files);
        $validator=$this->validator($rules);    //Closure will create new object
        $validator->validateNameValue($params);
        //Update entity and save
        return $entity;
    }

 

Share this post


Link to post
Share on other sites

I'd probably take one of two approaches.

Option 1)  Get the data directly from the entity, either as you propose or some other means.  For example in Symfony you can define a static method on an class that will setup the constraints for the entity.   You could create an interface that defines such a method and then implement it on your entities.

//Interface
interface ValidatorFiles {
    public function getValidationFiles();
}

//Implement
class YourEntity implements ValidatorFiles {
    public function getValidatorFiles(){
        return ['validation1.json', 'validation2.json'];
    }
}

//Use
public function validate($entity){
    $ruleFiles = $entity instanceof ValidatorFiles?$entity->getValidatorFiles():[];
    //...
}

Option 2) Define the validation files for your entities as part of your validator component and it can look up the files based on the entity class.  For example:

//Somewhere in your services configuration/bootstrap code
$validator = new ValidatorService();
$validator->addValidationFiles(YourEntity::class, ['validation1.json', 'validation2.json']);
$validator->addValidationFiles(YourOtherEntity::class, ['validation3.json', 'validation4.json']);

//To validate
$entity = new YourEntity();
//...
$validator = $container->get(ValidatorService::class);
$validator->validate($entity);

//In the validator
public function validate($entity){
    $ruleFiles = $this->files[get_class($entity)] ?? [];
    //...
}

I don't think putting it in the repository makes any sense.

Share this post


Link to post
Share on other sites

Thanks kicken,

I like both your approaches.  Not sure which one to use but am confident there isn't a wrong choice.

Another variation of Option 1 is to validate in the lifecycle, however, it will validate the entire entity upon updating a single property which may or may not be a good thing.

<doctrine-mapping>
    <entity name="Order">
        <lifecycle-callbacks>
            <lifecycle-callback type="prePersist" method="assertCustomerallowedBuying" />
            <lifecycle-callback type="preUpdate" method="assertCustomerallowedBuying" />
        </lifecycle-callbacks>
    </entity>
</doctrine-mapping>

 

Share this post


Link to post
Share on other sites
On 4/1/2019 at 11:12 AM, kicken said:

I'd probably take one of two approaches.

Option 1)  Get the data directly from the entity...

Option 2) Define the validation files for your entities as part of your validator component...

I don't think putting it in the repository makes any sense.

Based on your reply, I implemented using Option 1 as shown below.  Factory is a pretty elaborate class which only creates an instance of a concrete entity based on the requested type provided by the user.  Since the validation rules are unknown until after the entity is created and I felt that injecting the validator class into the factory wasn't appropriate (or is it?), Factory::create() returns an unpopulated entity.  I then use the entity to get the validation rules and validate the user data, get the repository using the entity class name and populate the entity, and save the entity.

<?php
namespace MyProject\Service;

class PersonService
{
    public function create(array $params):Entity {
        //Create an instance of the concrete object based on $params['type']
        //Potential entities include Employee, Manager, Student, and Teacher which all extend Person
        //Does not set properties as the data is not yet validated
        $entity=$this->factory->create($params);

        //Validate the user data by using validation rules defined by the concrete object
        $validator=($this->validator)($entity->getValidationRules());
        $validator->validate($params);

        //Set properties of entity using user provided data and method in specific repository
        $repo=$this->em->getRepository(get_class($entity));
        $repo->setEntityValues($entity, $params);

        //Save and return entity
        $this->em->persist($entity);
        $this->em->flush();
        return $entity;
    }
}

Why do you say that placing the validation rules in the repository doesn't make sense?  I was thinking of changing to the following, but it gets the rules from the repository.  Maybe still put them in a static method in the entity, and have the repo get them from there?  Any comments on this approach?  Am I able to get the specific repository (or the specific entity class name and then use $em->getRepository('MyProject\Model\Teacher') ) using the discriminator string directly from a built in EntityManager method, or will I need to duplicate the map as I have shown below?

<?php
namespace MyProject\Service;
class PersonService
{
    public function create(array $params):Entity {
        //Get specific repository based on $params['type']
        //Potential repos include EmployeeRepo, ManagerRepo, StudentRepo, and TeacherRepo which all extend PersonRepo
        //Ideally, getRepository would use Doctrine's existing discriminator map to fetch the repo.
        //Otherwise, this mapping can be duplicated in an array injected into the serve. 
        $repo=$this->getRepository($params['type']);

        //Validate the user data by using validation rules defined by specific respository
        $validator=($this->validator)($repo->getValidationRules());
        $validator->validate($params);
        
        //Create an instance of the concrete object and sets values using method in repo.
        $entity=$repo->create($params);

        //Save and return entity
        $this->em->persist($entity);
        $this->em->flush();
        return $entity;
    }

    protected function getRepository($discriminator)
    {
        $map=[
            "employee" => "MyProject\Model\Employee",
            "manager" => "MyProject\Model\Manager",
            "student" => "MyProject\Model\Student",
            "teacher" => "MyProject\Model\Teacher"
        ];
        $class=$map[$discriminator];
        return $this->em->getRepository($class);
    }
}

For reference, entities are defined as follows:

<?php
namespace MyProject\Model;

/**
 * @Entity(repositoryClass="PersonRepository")
 * @InheritanceType("JOINED")
 * @DiscriminatorColumn(name="discr", type="string")
 * @DiscriminatorMap({"employee" = "})
 */
class Person{}

/** @Entity(repositoryClass="EmployeeRepository") */
class Employee extends Person{}

/** @Entity(repositoryClass="ManagerRepository") */
class Manager extends Person{}

/** @Entity(repositoryClass="StudentRepository") */
class Student extends Person{}

/** @Entity(repositoryClass="TeacherRepository") */
class Teacher extends Person{}

 

Share this post


Link to post
Share on other sites

Regarding getting the repository based on the discriminator, this works.  If $this->getParentClassname() returns `MyProject\Model\Person`, then $this->getRepository('teacher') will return an instance of TeacherRepository.

    protected function getRepository(string $discriminator):\Doctrine\ORM\EntityRepository
    {
        $map=$this->em->getClassMetadata($this->getParentClassname())->discriminatorMap;
        if(!isset($map[$discriminator])){
            throw new \Exception("Invalid type $discriminator");
        }
        return $this->em->getRepository($map[$discriminator]);
    }


 

Share this post


Link to post
Share on other sites
14 hours ago, NotionCommotion said:

Why do you say that placing the validation rules in the repository doesn't make sense?

In doctrine, the entity repository's main purpose is for finding entities by querying the database for them.  It provides a few default find* methods, and you can add your own custom ones if you have more unique/complex situations.  Stuffing validation stuff in there is a violation of the Single Responsibility Principle. You don't always have to follow such principle's strictly, but if you're going to violate them you should have a decent reason for doing so and I don't think such a reason exists in your described situation given the alternative options.

To me, option 2 above would be the best step toward the ideal solution, but option 1 is a reasonable compromise that may save a decent amount of time and unnecessary coding for a simple setup.

 

Share this post


Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • Create New...

Important Information

We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.