How to make a Symfony Callback validator Doctrine aware? - php

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.

Related

Binding to Laravel IoC instead of instantiating again and again

In my app I have a service called "LogService" to log events and other items. I basically need to use this on every controller to log events by users. Instead of having to instantiate this service in each controller, I had two thoughts for accomplishing this.
Option 1: Bind the service into the IoC and then resolve it that way
Option 2: Make a master class with the service in it and then extend it for other classes so they come with the service already bound
I have questions for each of these methods:
Option 1: Is this even possible? If so, would it just be with "App::make()" that it would be called? That way doesn't seem to play too well with IDE's
Option 2: I have done this kind of thing in the past but PHPStorm does not seem to recognize the service from the parent object because it is instantiated by "App::make()" and not through the regular dependency injection.
What would be the best course of action?
Thanks!
You can have it both ways, I think the neatest way would be:
1) Have an interface that describes your class, let's call it LogServiceInterface
2) Create a Service Provider that instantiates your class, like so:
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class LoggerServiceProvider extends ServiceProvider
{
/**
* Register bindings in the container.
*
* #return void
*/
public function register()
{
$this->app->bind(LogServiceInterface::class, function($app)
{
return new LogService();
});
}
}
3) Register this service provider in config/app.ph file:
'providers' => [
// Other Service Providers
App\Providers\LoggerServiceProvider::class,
],
4) Now, in controller you can request the instance of something that implements LoggerServiceInterface straight in the constructor:
(Some controller):
<?php namespace App\Http\Controllers;
use Illuminate\Routing\Controller;
use App\Repositories\OrderRepository;
class OrdersController extends Controller {
/**
* The logger service.
* #var LoggerServiceInterface $loggerService
*/
protected $loggerService;
/**
* Create a controller instance.
*
* #param OrderRepository $orders
* #return void
*/
public function __construct(LoggerServiceInterface $loggerService)
{
$this->loggerService = $loggerService;
}
/**
* Show all of the orders.
*
* #return Response
*/
public function index()
{
// $this->loggerService will be an instance of your LoggerService class that
// is instantiated in your service provider
}
}
This way, you have got an easy way to quickly change the implementation of your service, moreover, Phpstorm can handle this very easily.
You will still be able to use app()->make() to obtain an instance of your service.
This, however, will not be automatically picked up by Phpstorm. But you can help it to understand that, all you need to do is to use #var annotation, see:
/**
* #var LoggerServiceInterface $logger
*/
$logger = app()->make(LoggerServiceInterface::class);
That way, Phpstorm will know what to expect from that $logger object.

Missing argument 1 for method with argument being a service injection in Symfony 3

I have such service linked to one method in my entity:
entity_form:
class: AppBundle\Entity\EntityForm
calls:
- [setDoctrine, ['#doctrine']]
The argument injects (or at least should) doctrine into my entity so that I can get stuff from the db inside methods.
I set it for the getter because I had to omit setting the argument in the __construct because in code the class is never really "constructed"
The entity itself looks like this:
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="entity_form")
*/
class EntityForm
{
protected $id;
protected $url;
protected $name;
protected $className;
protected $description;
private $doctrine;
public function getId()
{
return $this->id;
}
/* _All Getters An Setters Here.._ */
private function setDoctrine($doctrine)
{
return $doctrine;
}
public function createEntity($id)
{
$this->setDoctrine();
$entityPath = sprintf('AppBundle:%s:%s', __NAMESPACE__, $this->getClassName());
var_dump($this->doctrine);
exit;
//var_dump is temponary and only for testing the doctrine
$entity = $this->doctrine->getRepository($entityPath)->find($id);
return $entity;
}
}
This entity stores information about forms on the website. With this I would like to nicely and quickly create entity of class for which the form is made, from given Id.
Unfortunately
Running such code gives me this error:
Warning: Missing argument 1 for
AppBundle\Entity\EntityForm::setDoctrine(), called in
C:\PHP\Repos\centaur\src\AppBundle\Entity\EntityForm.php on line 139
and defined
What could be the reason for this error to happen? How can I fix it?
Any help would be amazing
The argument injects (or at least should) doctrine into my entity so
that I can get stuff from the db inside methods.
No.
You defined entity_form service that is nothing more than AppBundle\Entity\EntityForm with setter-injected doctrine service.
So your EntityForm with injected service is available in container, it won't be injected into every created EntityForm.
Looks like you should read more about responsibilities. What you want to achieve in createEntity method should be a part of application service layer, not a model itself.
Model is not aware of application/infrastructure concepts, so injecting ORM is not ok.
About mentioned error, considering only service definition and that call:
$this->setDoctrine();
You want to call setter without required argument, and probably missed that your service definition already did setter-injection.
As I mentioned before, entity is basically not a service, so move that part into specific class and inject database-related service there using standard DI.

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:
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.

How to get the instance of Kernel in the Entity class in Symfony2

The title explains the question pretty well. I am in the lifecycle callback of the Doctrine Entity class and want to do some extra DB entries. For this I need to get an instance of the Kernel. How can I do this?
Needing the container/kernel in an entity is most of the time, wrong. An entity shouldn't be aware of any services. Why is that?
Basically, an entity is an object which represents a thing. An entity is mostly used in a relationnal database, but you can at any time use this entity for other matters (serialize it, instanciate it from an HTTP layer...).
You want your entity to be unit-testable, this means you need to be able to instanciate your entity easily, without anything around, mostly, without any piece of business logic.
You should move your logic into another layer, the one that will instanciate your entity.
For your use case, I think, the most easy way is to use a doctrine event.
services.yml
services:
acme_foo.bar_listener:
class: Acme\FooBundle\Bar\BarListener
arguments:
- #kernel
tags:
- { name: doctrine.event_listener, event: postLoad }
Acme\FooBundle\Bar\BarListener
use Symfony\Component\HttpKernel\KernelInterface;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Acme\FooBundle\Entity\Bar;
class BarListener
{
protected $kernel;
/**
* Constructor
*
* #param KernelInterface $kernel A kernel instance
*/
public function __construct(KernelInterface $kernel)
{
$this->kernel = $kernel;
}
/**
* On Post Load
* This method will be trigerred once an entity gets loaded
*
* #param LifecycleEventArgs $args Doctrine event
*/
public function postLoad(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
if (!($entity instanceof Bar)) {
return;
}
$entity->setEnvironment($this->kernel->getEnvironment());
}
}
And there you go, your entity remains flat without dependencies, and you can easily unit test your event listener
if you have to use some service, you shouldn't use whole container or kernel instance especially.
use the services itself - always try to inject single service, not whole container
your case looks like you should use doctrine events

Implementing a Custom Field on a Doctrine Entity

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));
}
}
}

Categories