I have some problems with custom validator constraints. I want to validate a field code in my entity TicketCode.
But the validator is not called when I use if(form->isValid), I don't understand why.
My custom Validator needs the entity Manager to check with the DataBase if this code corresponds, else the validator sends message error.
My Entity
<?php
namespace My\MyBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use My\MyBundle\Validator as TicketAssert;
/**
* My\MyBundle\Entity\TicketCode
*
* #ORM\Table()
* #ORM\Entity(repositoryClass="My\MyBundle\Entity\TicketCodeRepository")
*/
class TicketCode
{
/**
* #var integer $id
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string $code
*
* #ORM\Column(name="code", type="string", length=255)
* #TicketAssert:ValidCode(entity="TicketBundle:TicketCode", property="code")
*/
protected $code;
}
My Class Constraint
<?php
namespace My\MyBundle\Validator;
use Symfony\Component\Validator\Constraint;
/**
* #Annotation
*/
class ValidCode extends Constraint
{
public $message = 'Code Invalide';
public $entity;
public $property;
public function ValidatedBy()
{
return 'my_ticket_validator';
}
public function requiredOptions()
{
return array('entity', 'property');
}
public function targets()
{
return self::CLASS_CONSTRAINT;
}
}
My Validator
<?php
namespace My\MyBundle\Validator;
use Doctrine\ORM\EntityManager;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class ValidCodeValidator extends ConstraintValidator
{
private $entityManager;
public function __construct(EntityManager $entityManager)
{
$this->entityManager = $entityManager;
}
public function isValid($value, Constraint $constraint)
{
$todayInObject = new \DateTime(); //Renvoit la date du jour
var_dump($todayInObject);
$ticketCode = $this->entityManager->getRepository('MyMyBundle:TicketCode')->findOneBy(array('code' => $constraint->property));
if ($ticketCode != null) {
if ($ticketCode->nbMaxUse != 0 && ($todayInObject >= $ticketCode->getDateBegin && $todayInObject <= $ticketCode->getDateEnd())) {
return true;
}
else {
$this->setMessage($constraint->message);
return false;
}
}
else {
$this->setMessage($constraint->message);
return false;
}
return false;
}
}
Finally my service.yml in my bundle.
parameters:
my_ticket.validator.validcode.class: My\MyBundle\Validator\ValideCodeValidator
services:
my_ticket.validator.validcode:
class: %my_ticket.validator.validcode.class%
arguments: [#doctrine.orm.entity_manager]
tags:
- { name: validator.constraint_validator, alias: my_ticket_validator }
If you have any idea why my validator is never called, explain to me thanks.
You have an upper V in your function name ValidatedBy(). It is supposed to be validatedBy().
Related
based on the example LessThan.html#propertypath I would like to write a FunctionalTest for my own validator #Assert\Amount. $amount is to be validated depending on $currency.
My question is, how can I set the value for $currency in the FunctionalTest for the Amount Validator? I have looked at the tests of the package symfony/validator but can't find a clue. I have already written FunctionalTests for validators. But I am not getting anywhere with this requirement.
Could someone give me a hint or show me an example.
Example:
class Entity
{
/**
* #var \string
* #Assert\Currency()
*/
protected $currency;
/**
* #var \float
* #Assert\Amount(propertyPath="currency")
*/
protected $amount;
}
I have found a solution. I validated the complete entity and looked at the object in the validator and found the complete entity.
When I thought about how to test it, I did some research and found other tests that did the same. I don't know if this is the best solution, but I can now write my tests.
ConstraintValidatorTestCase
<?php
namespace App\Tests\Validator\Constraint;
use App\Entity\Entity;
use App\Validator\MinimumAmount;
use App\Validator\MinimumAmountValidator;
use Symfony\Component\Validator\Context\ExecutionContext;
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Validator\ConstraintValidatorInterface;
class MinimumAmountValidatorTest extends ConstraintValidatorTestCase
{
/**
* #var ExecutionContextInterface
*/
protected $context;
/**
* #var ConstraintValidatorInterface
*/
protected $validator;
/**
* #var Constraint
*/
protected $constraint;
protected function setUp(): void
{
$this->constraint = new Entity();
$this->context = $this->createContext();
$this->validator = $this->createValidator();
$this->validator->initialize($this->context);
}
public function createValidator(): MinimumAmountValidator
{
return new MinimumAmountValidator();
}
public function createContext(): ExecutionContext
{
$myEntity = new Entity();
$myEntity->setCurrency('EUR');
$translator = $this->createMock(TranslatorInterface::class);
$translator->expects($this->any())->method('trans')->willReturnArgument(0);
$validator = $this->createMock(ValidatorInterface::class);
$executionContext = new ExecutionContext($validator, $myEntity, $translator);
$context->setNode('InvalidValue', null, null, 'property.path');
$context->setConstraint($this->constraint);
return $executionContext;
}
public function testValidation()
{
$this->validator->validate(40.00, new MinimumAmount());
$this->assertNoViolation();
}
}
Constraint
<?php
namespace App\Validator;
use Symfony\Component\Validator\Constraint;
/**
* #Annotation
*/
class MinimumAmount extends Constraint
{
/**
* #var string
*/
public $message = '';
/**
* #return string
*/
public function validatedBy(): string
{
return MinimumAmountValidator::class;
}
}
Validator
<?php
namespace App\Validator;
use App\Entity\Entity;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class MinimumAmountValidator extends ConstraintValidator
{
/**
* #param float $value
* #param Constraint $constraint
*
* #return void
*/
public function validate($value, Constraint $constraint)
{
/**
* #var Entity
*/
$contextRoot = $this->context->getRoot();
$currency = $contextRoot->getCurrency(); // EUR
$amount = $value; // 40.00
// Validation implementation
}
}
I'm trying to use the DoctrineBehaviors translatable extension in Symfony 4.Just setup a test following the documentation example:
translatable entity:
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Knp\DoctrineBehaviors\Model as ORMBehaviors;
/**
* #ORM\Entity(repositoryClass="App\Repository\FAQRepository")
*/
class FAQ
{
use ORMBehaviors\Translatable\Translatable;
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
protected $id;
/.**
* #ORM\Column(type="datetime", nullable=true)
*/
protected $updatedAt;
public function getId(): ?int
{
return $this->id;
}
public function getUpdatedAt(): ?\DateTimeInterface
{
return $this->updatedAt;
}
public function setUpdatedAt(?\DateTimeInterface $updatedAt): self
{
$this->updatedAt = $updatedAt;
return $this;
}
}
translation entity:
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Knp\DoctrineBehaviors\Model as ORMBehaviors;
/**
* #ORM\Entity
*/
class FAQTranslation
{
use ORMBehaviors\Translatable\Translation;
/**
* #ORM\Column(type="text")
*/
protected $question;
/**
* #ORM\Column(type="text")
*/
protected $answer;
/**
* #ORM\Column(type="integer", nullable=true)
*/
protected $category;
public function getQuestion(): ?string
{
return $this->question;
}
public function setQuestion(string $question): self
{
$this->question = $question;
return $this;
}
public function getAnswer(): ?string
{
return $this->answer;
}
public function setAnswer(string $answer): self
{
$this->answer = $answer;
return $this;
}
public function getCategory(): ?int
{
return $this->category;
}
public function setCategory(?int $category): self
{
$this->category = $category;
return $this;
}
}
Testing the translatable entity:
/**
* #Route("/test", name="test")
*/
public function testfaq()
{
$em = $this->getDoctrine()->getManager();
$faq = new FAQ();
$faq->translate('fr')->setQuestion('Quelle est la couleur ?');
$faq->translate('en')->setQuestion('What is the color ?');
$em->persist($faq);
$faq->mergeNewTranslations();
$em->flush();
return $this->render('app/test.html.twig', [
]);
}
A new ID is added in the faq table.
But nothing is persisted in the faqtranslation table.
Bundles.php :
Knp\DoctrineBehaviors\Bundle\DoctrineBehaviorsBundle::class => ['all' => true],
All the documentations I found seem to refer to Symfony 3 or even Symfony 2, is it possible to use DoctrineBehaviors translatable in Symfony 4 ?
I don't know if you found your answer since (I hope you did), but yes you can use KnpLabs/DoctrineBehaviors with Symfony 4. Maybe, you just needed to wait a little longer for an update.
My doubt is about Api Platform. (https://api-platform.com)
I have two entities. Question and Answer. And I want to have a POST call to create a question with one answer. I show my entities.
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Serializer\Annotation\Groups;
/**
* #ApiResource(
* normalizationContext={"groups"={"question"}},
* denormalizationContext={"groups"={"question"}})
* #ORM\Entity
*/
class Question
{
/**
* #Groups({"question"})
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #Groups({"question"})
* #ORM\Column
* #Assert\NotBlank
*/
public $name = '';
/**
* #Groups({"question"})
* #ORM\OneToMany(targetEntity="Answer", mappedBy="question", cascade={"persist"})
*/
private $answers;
public function getAnswers()
{
return $this->answers;
}
public function setAnswers($answers): void
{
$this->answers = $answers;
}
public function __construct() {
$this->answers = new ArrayCollection();
}
public function getName(): string
{
return $this->name;
}
public function setName(string $name): void
{
$this->name = $name;
}
public function getId(): int
{
return $this->id;
}
}
And a Answer Entity
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Serializer\Annotation\Groups;
/**
*
* #ApiResource
* #ORM\Entity
*/
class Answer
{
/**
* #Groups({"question"})
* #ORM\Id
* #ORM\Column(type="guid")
*/
public $id;
/**
* #Groups({"question"})
* #ORM\Column
* #Assert\NotBlank
*/
public $name = '';
/**
* #ORM\ManyToOne(targetEntity="Question", inversedBy="answers")
* #ORM\JoinColumn(name="question_id", referencedColumnName="id")
*/
public $question;
public function getQuestion()
{
return $this->question;
}
public function setQuestion($question): void
{
$this->question = $question;
}
public function getName(): string
{
return $this->name;
}
public function setName(string $name): void
{
$this->name = $name;
}
public function getId(): string
{
return $this->id;
}
public function __toString()
{
return $this->getName();
}
}
Now I can create from dashboard of nelmio a question and into an answer. But in database, my answer doesnt have saved the relation with question.
{
"name": "my new question number 1",
"answers": [
{
"id": "ddb66b71-5523-4158-9aa3-2691cae9d473",
"name": "my answer 1 to question number 1"
}
]
}
And other question is... I've changed my id of answer by a guid, because I get and error when I create and answer into question without id. Can I create a question, and answers without to specify an id ?
Thanks in advance
Well, I see some errors in your entities... first since the relation between Question and Answer entity is a OneToMany, your Qestion entity should have this implemented, the:
use ApiPlatform\Core\Annotation\ApiProperty;
//..... the rest of your code
/**
* #ApiProperty(
* readableLink=true
* writableLink=true
* )
* #Groups({"question"})
* #ORM\OneToMany(targetEntity="Answer", mappedBy="question", cascade={"persist"})
*/
private $answers;
public function __construct()
{
//....
$this->answers = new ArrayCollection();
//...
}
public function addAnswer(Answer $answer): self
{
if (!$this->answers->contains($answer)) {
$this->answers[] = $answer;
$answer->setQuestion($this)
}
return $this;
}
public function removeAnswer(Answer $answer): self
{
if ($this->answers->contains($answer)) {
$this->answers->removeElement($answer);
}
return $this;
}
the command
PHP bin/console make:entity
allows you to create a field in your entity of type relation and it creates these methods for you just follow the instructions (after both entities are created, use the command for update Question entity...)
the readableLink ApiProperty annotation is for see embedded object on GET request, is the same if you use serialization groups, if you set it on false then the response will look like this:
{
"name": "my new question number 1",
"answers": [
"/api/answers/1",
"/api/answers/2",
....
]
}
it is used to make responses smaller (among other things)... and the writableLink is for allowing POST request like this (see this example for more info here):
{
"name": "my new question number 1",
"answers": [
{
"id": "ddb66b71-5523-4158-9aa3-2691cae9d473",
"name": "my answer 1 to question number 1"
}
]
}
of course, using the corresponding serialization groups in each entity...
in ApiPlatform the embedded objects are persisted through setters and getters method but also add and remove methods for OneToMany relations, and the ORM does the rest of the work.
let me know if this helps. Cheers!
For the first point, it should persist in database without problem. Anyway, you could create a PostValidateSubscriber for Question entity and check if the relation is there.
<?php /** #noinspection PhpUnhandledExceptionInspection */
namespace App\EventSubscriber;
use ApiPlatform\Core\EventListener\EventPriorities;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
final class QuestionPostValidateSubscriber implements EventSubscriberInterface
{
private $tokenStorage;
public function __construct(
TokenStorageInterface $tokenStorage
) {
$this->tokenStorage = $tokenStorage;
}
/**
* {#inheritdoc}
*/
public static function getSubscribedEvents()
{
return [
KernelEvents::VIEW => ['checkQuestionData', EventPriorities::POST_VALIDATE]
];
}
/**
* #param GetResponseForControllerResultEvent $event
*/
public function checkQuestionData(GetResponseForControllerResultEvent $event)
{
$bid = $event->getControllerResult();
$method = $event->getRequest()->getMethod();
if (!$question instanceof Question || (Request::METHOD_POST !== $method && Request::METHOD_PUT !== $method))
return;
$currentUser = $this->tokenStorage->getToken()->getUser();
if (!$currentUser instanceof User)
return;
}
}
And do an echo or use xdebug for check question.
For the second point, you can add these annotations for the id of entities, so the id's will generate theirself.
#ORM\GeneratedValue()
#ORM\Column(type="integer")
I try to make a custom deletion logic via a custom repository:
namespace AppBundle\Repository;
use AppBundle\Entity\ContactEmail;
class ContactEmailRepository extends \Doctrine\ORM\EntityRepository
{
/**
* Remove an email from the database
* #param String $email
*/
public function deleteEmail($email)
{
$em=$this->getEntityManager();
$queryBuilder = $em->createQueryBuilder();
$queryBuilder->delete('AppBundle::ContactEmail','c')
->where('c.email=:email')
->setParameter('email',$email);
$query=$queryBuilder->getQuery();
$p = $query->execute();
return $p;
}
}
And I test it via a functional test:
namespace Tests\AppBundle\Repository;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use AppBundle\Entity\ContactEmail;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use Doctrine\ORM\Tools\SchemaTool;
Use Doctrine\Common\DataFixtures\Purger\ORMPurger;
use AppBundle\DataFixtures\ContactEmailDataFixture;
class ContactEmailTest extends KernelTestCase
{
/**
* #var \Doctrine\ORM\EntityManager
*/
private $entityManager;
/**
* {#inheritDoc}
*/
protected function setUp()
{
$kernel = self::bootKernel();
$this->entityManager = $kernel->getContainer()
->get('doctrine')
->getManager();
//In case leftover entries exist
$schemaTool = new SchemaTool($this->entityManager);
$metadata = $this->entityManager->getMetadataFactory()->getAllMetadata();
// Drop and recreate tables for all entities
$schemaTool->dropSchema($metadata);
$schemaTool->createSchema($metadata);
}
/**
* Testing whether a preloaded email will get deleted
*/
public function testDeletion()
{
$fixture = new ContactEmailDataFixture();
$fixture->load($this->entityManager);
/**
* #var Appbundle\Repository\ContactEmailRepository
*/
$repository=$this->entityManager->getRepository(ContactEmail::class);
$emailToDelete='jdoe#example.com';
$repository->deleteEmail($emailToDelete);
$emailSearched=$repository->findOneBy(['email'=>$emailToDelete]);
$this->assertEmpty($emailSearched);
}
}
My entity is the following:
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
/**
* ContactEmail
*
* #ORM\Table(name="contact_email")
* #ORM\Entity(repositoryClass="AppBundle\Repository\ContactEmailRepository")
* #UniqueEntity("email")
*/
class ContactEmail
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var String
* #ORM\Column(name="email",type="string",unique=true)
* #Assert\Email( message = "The email '{{ value }}' is not a valid email.")
*/
private $email;
/**
* #ORM\Column(name="date", type="datetime")
*/
private $createdAt;
public function __construct()
{
$this->createdAt=new \DateTime("now", new \DateTimeZone("UTC"));
}
/**
* Get id
*
* #return int
*/
public function getId()
{
return $this->id;
}
/**
* #param String $email
* #return ContactEmail
*/
public function setEmail($email){
$this->email=$email;
return $this;
}
/**
* #return String
*/
public function getEmail(){
return $this->email;
}
public function getCreatedAt()
{
return $this->createdAt;
}
}
And I use the following DataFixture:
namespace AppBundle\DataFixtures;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\Persistence\ObjectManager;
use AppBundle\Entity\ContactEmail;
class ContactEmailDataFixture extends Fixture
{
public function load(ObjectManager $manager)
{
$emails=['jdoe#example.com','example#gmail.com','user1#example.com'];
foreach($emails as $email){
$emailEntityInstance=new ContactEmail();
$emailEntityInstance->setEmail($email);
$manager->persist($emailEntityInstance);
$manager->flush();
}
}
}
The problem is that I get the following error when I run the test:
Doctrine\ORM\Query\QueryException: [Semantical Error] line 0, col 7 near 'AppBundle::ContactEmail': Error: Class 'AppBundle\Entity\' is not defined.
So I want to know what I did wrong on the query syntax?
In a class that extends Doctrine\ORM\EntityRepository and that is referenced in the entity's #Entity docblock annotation's repositoryClass attribute, you can get a query builder like this:
$queryBuilder = $this->createQueryBuilder('c')
->where('c.email=:email')
->setParameter('email',$email);
I have a Symfony2 form that I want to add a file upload dialog to.
According to the Symfony docs (http://symfony.com/doc/2.0/cookbook/doctrine/file_uploads.html), I have created a Document class:
<?php
namespace Acme\AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\Validator\Constraints as Assert;
/**
* #ORM\Entity
* #ORM\HasLifecycleCallbacks
*/
class Document
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
public $id;
/**
* #ORM\Column(type="string", length=255, nullable=true)
*/
public $path;
/**
* #Assert\File(maxSize="6000000")
*/
public $file;
public function getAbsolutePath()
{
return null === $this->path ? null : $this->getUploadRootDir().'/'.$this->path;
}
public function getWebPath()
{
return null === $this->path ? null : $this->getUploadDir().'/'.$this->path;
}
protected function getUploadRootDir()
{
// the absolute directory path where uploaded documents should be saved
return __DIR__.'/../../../../web/'.$this->getUploadDir();
}
protected function getUploadDir()
{
// get rid of the __DIR__ so it doesn't screw when displaying uploaded doc/image in the view.
return 'uploads/documents';
}
/**
* #ORM\PrePersist()
* #ORM\PreUpdate()
*/
public function preUpload()
{
if (null !== $this->file) {
$this->path = sha1(uniqid(mt_rand(), true)).'.'.$this->file->guessExtension();
}
}
/**
* #ORM\PostPersist()
* #ORM\PostUpdate()
*/
public function upload()
{
if (null === $this->file) {
return;
}
$this->file->move($this->getUploadRootDir(), $this->path);
unset($this->file);
}
/**
* #ORM\PostRemove()
*/
public function removeUpload()
{
if ($file = $this->getAbsolutePath()) {
unlink($file);
}
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set path
*
* #param string $path
*/
public function setPath($path)
{
$this->path = $path;
}
/**
* Get path
*
* #return string
*/
public function getPath()
{
return $this->path;
}
}
And a DocumentType form class:
<?php
namespace Acme\AppBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class DocumentType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('file')
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\AppBundle\Entity\Document',
));
}
public function getName()
{
return 'document_form';
}
}
However, when I add this to my existing entity and form class:
<?php
namespace Acme\AppBundle\Entity\Profile;
use Doctrine\ORM\Mapping as ORM;
use Acme\AppBundle\Entity\jDocument;
use Symfony\Component\Validator\Constraints as Assert;
/**
* #ORM\Entity
* #ORM\Table(name="UserProfile")
*/
class UserProfile extends GenericProfile
{
//... Other entity params
/**
* #ORM\OneToOne(cascade={"persist", "remove"}, targetEntity="Acme\AppBundle\Entity\Document")
* #ORM\JoinColumn(name="picture_id", referencedColumnName="id", onDelete="set null")
*/
protected $picture;
/**
* Set picture
*
* #param Acme\AppBundle\Entity\Document $picture
*/
//\Acme\AppBundle\Entity\Document
public function setPicture($picture)
{
$this->picture = $picture;
}
/**
* Get picture
*
* #return Acme\AppBundle\Entity\Document
*/
public function getPicture()
{
return $this->picture;
}
//... Other entity getters and setters
}
Whenever I submit the form, I get the following error:
ErrorException: Warning: ini_set(): A session is active. You cannot change the session module's ini settings at this time in /var/www/wolseley-integrated-services/builds/dev/app/cache/prod/classes.php line 421
But the page title is "Entity was not found. (500 Internal Server Error)".
Can anybody spot which entity it can't find? Or if that's even the issue?
I've done some googling and checked that session.auto_start is set to 0 in php.ini, I've cleared all my sessions and caches... I'm stumped!
It turns out I was getting this strange session error message because of an error page definition in my nginx config.
The entity not found issue was resolved by correcting some errors in my entities. The Symfony developer bar provided me with enough information to track the issue down.