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:
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.
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.
I have followed your suggestion above as you have put it. The only thing I had to change was the hasMetadaFor method implementation inside the PackageMetadataFactory. Below is how I rather check for property existence.
public function hasMetadataFor( $value ){
return property_exists(Package::class, $value);
}
Everything else as you suggested works perfectly. Below is my test function.
$validator = new PackageValidator(new OrderPackage(125787618));
$this->assertSame(true, $validator->validates());
The test fails because the namespace cannot be numbers. Passing the fully qualified class name of the OrderPackage by doing OrderPackage ::class validates the object.
Thank you very much for your advice.
Related
Recently we upgraded our applications to PHP8.
Since PHP8 introduced attributes and doctrine/orm supports them as of version 2.9 it seemed like a good idea to utilize this feature to incrementally (ie. not all entities at once) update entity metadata to the attributes' format.
In order to do so I need to somehow register both Doctrine\ORM\Mapping\Driver\AnnotationDriver and Doctrine\ORM\Mapping\Driver\AttributeDriver to parse the metadata.
The tricky part is to register both parsers for a set of entities decorated either using annotations or attributes. From the point of Doctrine\ORM\Configuration it seems what I need is not possible.
Am I correct (in assumption this cannot be reasonably achieved) or could this be done in some not-very-hackish way?
Doctrine by itself doesn't offer this possibility. But we can implement a custom mapping driver to make this happen.
The actual implementation could look like this:
<?php
namespace Utils\Doctrine;
use Doctrine\ORM\Mapping\Driver\AnnotationDriver;
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
use Doctrine\ORM\Mapping\MappingException;
use Doctrine\Persistence\Mapping\ClassMetadata;
use Doctrine\Persistence\Mapping\Driver\AnnotationDriver as AbstractAnnotationDriver;
class HybridMappingDriver extends AbstractAnnotationDriver
{
public function __construct(
private AnnotationDriver $annotationDriver,
private AttributeDriver $attributeDriver,
) {
}
public function loadMetadataForClass($className, ClassMetadata $metadata): void
{
try {
$this->attributeDriver->loadMetadataForClass($className, $metadata);
return;
} catch (MappingException $me) {
// Class X is not a valid entity, so try the other driver
if (!preg_match('/^Class(.)*$/', $me->getMessage())) {// meh
throw $me;
}
}
$this->annotationDriver->loadMetadataForClass($className, $metadata);
}
public function isTransient($className): bool
{
return $this->attributeDriver->isTransient($className)
|| $this->annotationDriver->isTransient($className);
}
}
In a nutshell:
the driver tries to use AttributeDriver first, then fallbacks to the AnnotationDriver in case the class under inspection is not evaluated as a valid entity
in order to comply with Doctrine\Persistence\Mapping\Driver\MappingDriver interface after extending Doctrine\Persistence\Mapping\Driver\AnnotationDriver class only 2 methods have to be implemented
as it can be seen in the example implementation both methods regard both metadata mapping drivers
distinguishing between various kinds of MappingExceptions by parsing the message is not elegant at all, but there is no better attribute to distinguish by; having different exception subtypes or some unique code per mapping error case would help a lot to differentiate between individual causes of mapping errors
The HybridMappingDriver can be hooked up in an EntityManagerFactory like this:
<?php
namespace App\Services\Doctrine;
use Doctrine\ORM\Tools\Setup;
use Doctrine\ORM\EntityManager;
use Doctrine\Common\Annotations\AnnotationRegistry;
use Doctrine\Common\Proxy\AbstractProxyFactory as APF;
use Doctrine\Persistence\Mapping\Driver\MappingDriver;
use Utils\Doctrine\NullCache;
class EntityManagerFactory
{
public static function create(
array $params,
MappingDriver $mappingDriver,
bool $devMode,
): EntityManager {
AnnotationRegistry::registerLoader('class_exists');
$config = Setup::createConfiguration(
$devMode,
$params['proxy_dir'],
new NullCache(), // must be an instance of Doctrine\Common\Cache\Cache
);
$config->setMetadataDriverImpl($mappingDriver); // <= this is the actual hook-up
if (!$devMode) {
$config->setAutoGenerateProxyClasses(APF::AUTOGENERATE_FILE_NOT_EXISTS);
}
return EntityManager::create($params['database'], $config);
}
}
I'm not sure if it can be done but you could take a look at Rector to automatically upgrade all your entities at once. There already seems to be a config for this.
https://github.com/rectorphp/rector
https://github.com/rectorphp/rector-doctrine/blob/4bbeb676e9ec8c146a81617f6362be4cafbdf3b3/config/sets/doctrine-orm-29.php
I have a custom repository returning raw data that wouldn't fit in an entity.
namespace App\Repository;
class RevenuesRepository
{
/**
* #return array Raw data about revenues
*/
public function getRevenuesRecap()
{
// ...
return $result;
}
}
I want to use it in a controller, but I can't use $em->getRepository(...) because this repository is not linked to an entity. How can I do that?
If you want to use your standalone custom repository in a controller function, it should be absolutely sufficient to add it to either action method signature or constructor signature:
use App\Repository\RevenuesRepository;
class RevenuesController {
private $revenuesRepository;
// inject it in constructor
public __construct(RevenuesRepository $revenuesRepository) {
$this->revenuesRepository = $revenuesRepository;
}
// OR (!) inject it in action
public function getRevenuesRecapAction(RevenuesRepository $revenuesRepository) {
$recap = $revenuesRepository->getRevenuesRecap();
$response->setContent(json_encode([
'data' => $recap,
]));
$response->headers->set('Content-Type', 'application/json');
return $response;
}
}
This way, when testing for example, it's clearly visible what the dependencies are and what you might have to mock or provide. Also, it provides your IDE with direct data for static analysis and code completion and more helpful information like method signatures.
This works due to auto-wiring. In symfony 4 and 5 repositories are by default services and thus can be auto-wired and injected quasi automatically.
As said in the title, I actually need to create a validation process with Symfony.
I'm using YAML file, everything is okay.
But in some cases, I need to check the database before saying that the data is validated.
I was searching in the Callback method, but it actually only allows me to basically check the values. I searched to make dependency injection, or even passing a defined service as a Callback, but it does not help too.
So the question, in short is: is it possible to achieve it? In which way?
With what #dragoste said in comments, I searched how to made it with my own constraint.
The solution is so to use a Custom Constraint. It is a bit messy to know what file to make and what to do, so here is what I have done.
To explain you what are my files, the goal was to validate a rent, not by how it is made but just check that there is no rent at the same moment. That's why I have to use a constraint with Doctrine inside it.
Creating the Validator folder inside the root of your bundle. Then, adding a Constraints folder inside the Validator folder.
Creating a file RentDatesConstraint.php in Validaor/Constraints folder.
Here is how it looks:
<?php
namespace ApiBundle\Validator\Constraints;
use ApiBundle\Validator\RentDatesValidator;
use Symfony\Component\Validator\Constraint;
class RentDatesConstraint extends Constraint
{
public $message = 'The beginning and ending date of the rent are not available for this vehicle.'; // note that you could use parameters inside it, by naming it with % to surround it
/**
* #inheritdoc
*/
public function validatedBy()
{
return RentDatesValidator::class; // this is the name of the class that will be triggered when you need to validate this constraint
}
/**
* #inheritdoc
*/
public function getTargets()
{
return self::CLASS_CONSTRAINT; // says that this constraints is a class constraint
}
}
Now you have created your own class constraint, you have to create your own validator.
Create a file RentDatesValidator.php in Validator folder.
<?php
namespace ApiBundle\Validator;
use Doctrine\Bundle\DoctrineBundle\Registry;
use Doctrine\Common\Collections\Collection;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class RentDatesValidator extends ConstraintValidator
{
/**
* #var Registry $doctrine
*/
private $doctrine;
/**
* RentDatesValidator constructor.
* #param Registry $_doctrine
*/
public function __construct(Registry $_doctrine)
{
$this
->setDoctrine($_doctrine)
;
}
/**
* #param Registry $_doctrine
* #return $this
*/
public function setDoctrine(Registry $_doctrine)
{
$this->doctrine = $_doctrine;
return $this;
}
/**
* #inheritdoc
* #param Rent $_value
*/
public function validate($_value, Constraint $_constraint)
{
//do your stuff here
if ($testFails) {
$this
->context
->buildViolation($_constraint->message) // here you can pass an array to set the parameters of the string, surrounded by %
->addViolation()
;
}
}
}
We are almost finished, we have to declare it as a service, so here we edit services.yml in Resources/config
services:
# [...]
validator.rent_dates:
class: ApiBundle\Validator\RentDatesValidator
tags:
- { name: validator.constraint_validator }
arguments: [ "#doctrine" ]
You can notice here that I passed #doctrine service, but you can actually pass any service you want, even many, as long as you are defining the RentDatesValidator class properly to accept those services in its constructor.
And now, all you have to do is to use this in your validation.
Here we edit Rent.yml in Resource/config/validation to add this only line:
ApiBundle\Entity\Rent:
constraints:
- ApiBundle\Validator\Constraints\RentDatesConstraint: ~
We are done! The validation will work when passing your object to the validator service.
You can notice that this is made with YAML, I personally prefer this way of doing things as it separate each parts (entity-definition, database schema, validation files, ...) but you can do it with annotation, XML or even pure PHP. It's up to you, so if you want to see more syntax, you can still go on the link to Symfony Documentation to know how to do this.
I'm creating a custom validator constraint to validate a "Contact", which is something like "John Doe <jdoe#example.com>". Following the Cookbook I've created the Constraint Class:
<?php
namespace MyCompany\MyBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
/**
* #Annotation
*/
class Contact extends Constraint
{
public $message = 'The string "%string%" is not a valid Contact.';
}
and also created the validator:
<?php
namespace MyCompany\MyBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Constraints\Email;
use Symfony\Component\Validator\Constraints\EmailValidator;
class ContactValidator extends ConstraintValidator
{
public function validate($value, Constraint $constraint)
{
if (!preg_match('#(.*)\s+<(.*)>#', $value, $matches)) {
$this->context->addViolation($constraint->message, array('%string%' => $value));
}
$emailValidator = new EmailValidator();
if (isset($matches[2]) && $emailValidator->validate($matches[2], new Email())) {
$this->context->addViolation($constraint->message, array('%string%' => $value));
}
}
}
The point is that I'm trying to use the Symfony's EmailValidator inside my custom validator to check the email is valid. I don't want to reinvent the wheel and validate the email using my own regex.
Everything is ok when trying to validate a valid contact but, testing a contact with invalid email ("Gabriel Garcia <infoinv4l1d3mai1.com>") it craches with a PHP Fatal error:
Fatal error: Call to a member function addViolation() on a non-object in /home/dev/myproject/vendor/symfony/symfony/src/Symfony/Component/Validator/Constraints/EmailValidator.php on line 58
Digging into the EmailValidator.php class, I've realized that the issue is related to the $context (ExecutionContext). Here is line 58 of EmailValidator.php:
$this->context->addViolation($constraint->message, array('{{ value }}' => $value));
Seems that context attribute of the class is null. Anyone knows why? I need to inject it somewhere?
Thanks in advance.
P.S.: I'm using Symfony 2.3. Do not pay attention to the regex, I know it can be so much better. It's just for testing right now.
I think the original question was about using EmailValidator inside a Custom Validator and in this scenario container is unavailable, so
$this->get('validator');
will not work. It seems that the only issue the poster had is to have EmailValidator addViolation to the correct context. This should work:
$emailValidator = new EmailValidator();
$emailValidator->initialize($this->context);
$emailValidator->validate($matches[2], $constraint);
You can directly use the constraint
see http://symfony.com/doc/current/book/validation.html
use Symfony\Component\Validator\Constraints\Email
$emailConstraint = new Email();
// use the validator to validate the value
$errorList = $this->get('validator')->validateValue(
$email,
$emailConstraint
);
Best regard
Finding this topic after have try to call custom inside custom, I made a deeply research and I may just found another better way (simpler according to me).
Valid : Sf2.6>=
$this->context->getValidator()
->inContext($this->context)
->atPath("time_on_point")
->validate($timeOnPoint, new AssertCustom\DateTimeRange(array('min' => '+1 day')));
In this case, I declared a new custom validator like class specific Validator and I could go directly to the field by its name.
The advantage of this : I can call another custom by only applying the "new AssertCustom" and if this "AssertCustom" needs a service like a construct, I won't have a dependency because the configuration service will call all the stuff transparently.
Be careful, if you call recursively (deep) field, you will need to adapt the context according to the comments found in this file : Symfony\Component\Validator\Constraints\CollectionValidator
I have an Attachment Entity in Doctrine which references a file on Amazon S3. I need to be able to provide a sort of 'Calculated Field' on the Entity that works out what I call the downloadpath. The downloadpath would be a calculated URL, for example http://site.s3.amazon.com/%s/attach/%s where I need to replace the two string values with values on the entity itself (account and filename), so;
http://site.s3.amazon.com/1/attach/test1234.txt
Although we use a Service Layer, I'd like the downloadpath to be available on the Entity at all times without it having to pass through the SL.
I've considered the obvious route of adding say a constant to the Entity;
const DOWNLOAD_PATH = 'http://site.s3.amazon.com/%s/attach/%s'; and a custom getDownloadPath() but I'd like to keep specifics like this URL in my app's configuration, not the Entities class (also, see update below)
Does anyone have any ideas on how I could achieve this?
UPDATE To add to this, I am aware now that I would need to generate a temporary URL with the AmazonS3 library to allow temporary authed access to the file - I'd prefer not to make a static call to our Amazon/Attachment Service to do this as It just doesn't feel right.
Turns out the cleanest way to do this is using the postLoad event like so;
<?php
namespace My\Listener;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Events;
use Doctrine\ORM\Event\LifecycleEventArgs;
use My\Entity\Attachment as AttachmentEntity;
use My\Service\Attachment as AttachmentService;
class AttachmentPath implements EventSubscriber
{
/**
* Attachment Service
* #param \My\Service\Attachment $service
*/
protected $service;
public function __construct(AttachmentService $service)
{
$this->service = $service;
}
public function getSubscribedEvents()
{
return array(Events::postLoad);
}
public function postLoad(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
if ($entity instanceof AttachmentEntity) {
$entity->setDownloadPath($this->service->getDownloadPath($entity));
}
}
}