Here is the error message when displaying: "An exception occurred while executing a query: SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'done' cannot be null"
I don't understand why this error occurs when everything seems very normal.
Here's my DefaultController.php file:
<?php
namespace App\Controller;
use App\Entity\Todo;
use App\Form\TodoType;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class DefaultController extends AbstractController {
#[Route('/', name:'index')]
public function index(Request $request, ManagerRegistry $mr) {
$todo = new Todo();
$todoForm = $this->createForm(TodoType::class, $todo);
$todoForm->handleRequest($request);
// dd($todoForm);
if($todoForm->isSubmitted() && $todoForm->isValid() ){
$todo->setCreatedAt(new \DateTime());
$entityManager = $mr->getManager();
$entityManager->persist($todo);
$entityManager->flush();
}
return $this->render('./page1.html.twig',
['form'=>$todoForm->createView()]
);
}
}
Here is the code of my entity class todo:
<?php
namespace App\Entity;
use App\Repository\TodoRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: TodoRepository::class)]
class Todo
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private $id;
#[ORM\Column(type: 'string', length: 255)]
private $content;
#[ORM\Column(type: 'boolean', options:["default" => false])]
private $done;
#[ORM\Column(type: 'datetime' , options:["default" => "CURRENT_TIMESTAMP"])]
private $createdAt;
public function getId(): ?int
{
return $this->id;
}
public function getContent(): ?string
{
return $this->content;
}
public function setContent(string $content): self
{
$this->content = $content;
return $this;
}
public function getDone(): ?bool
{
return $this->done;
}
public function setDone(bool $done): self
{
$this->done = $done;
return $this;
}
public function getCreatedAt(): ?\DateTimeInterface
{
return $this->createdAt;
}
public function setCreatedAt(\DateTimeInterface $createdAt): self
{
$this->createdAt = $createdAt;
return $this;
}
}
Here is my TodoType form:
<?php
namespace App\Form;
use App\Entity\Todo;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class TodoType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('content')
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Todo::class,
]);
}
}
I tried another method by injecting the EntityManagerInterface service in my controller, unfortunately. I get the same error message. Need help please.
I think instead of initialising your property withing the ORM you can do it underneath
#[ORM\Column(type: 'boolean')]
private $done = false;
Or you could do it as soon as you submit the form, by any means when you submit the form it sets the done value to false. However this method isn't practical.
if($todoForm->isSubmitted() && $todoForm->isValid() ){
$todo->setCreatedAt(new \DateTime());
$todo->setDone(0); // Sets the value to false as soon as the form is submitted
$entityManager = $mr->getManager();
$entityManager->persist($todo);
$entityManager->flush();
Related
I have a non required field in my 'Session' entity, field is named 'location'. I want to make it required only if another field is equal to true, this other property is named 'isVirtual'
I thaught i'd create an eventSubscriber with the event BeforeEntityUpdatedEvent but I don't know how to add a constraint and I am not sure when this event is triggered.
I tried to do a custom validation constraint but I don't know how to get value of 'isVirtual' value in my ConstraintValidator class.
I need someone to help me look in the right direction. I put there my Entity
<?php
namespace App\Entity;
use App\Repository\SessionRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: SessionRepository::class)]
class Session
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column]
private ?\DateTimeImmutable $beginsAt = null;
#[ORM\Column]
private ?\DateTimeImmutable $endsAt = null;
#[ORM\ManyToOne(inversedBy: 'sessions')]
#[ORM\JoinColumn(nullable: false)]
private ?Formation $formation = null;
#[ORM\ManyToOne(inversedBy: 'sessions')]
#[ORM\JoinColumn(nullable: true)]
private ?Location $location = null;
#[ORM\Column]
private ?bool $isVirtual = null;
#[ORM\OneToMany(mappedBy: 'session', targetEntity: UserSession::class)]
private Collection $users;
#[ORM\Column(nullable: true)]
private ?int $numberOfSeats = null;
public function __construct()
{
$this->users = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getBeginsAt(): ?\DateTimeImmutable
{
return $this->beginsAt;
}
public function setBeginsAt(\DateTimeImmutable $beginsAt): self
{
$this->beginsAt = $beginsAt;
return $this;
}
public function getEndsAt(): ?\DateTimeImmutable
{
return $this->endsAt;
}
public function setEndsAt(\DateTimeImmutable $endsAt): self
{
$this->endsAt = $endsAt;
return $this;
}
public function getFormation(): ?Formation
{
return $this->formation;
}
public function setFormation(?Formation $formation): self
{
$this->formation = $formation;
return $this;
}
public function getLocation(): ?Location
{
return $this->location;
}
public function setLocation(?Location $location): self
{
$this->location = $location;
return $this;
}
public function isIsVirtual(): ?bool
{
return $this->isVirtual;
}
public function setIsVirtual(bool $isVirtual): self
{
$this->isVirtual = $isVirtual;
return $this;
}
/**
* #return Collection<int, UserSession>
*/
public function getUsers(): Collection
{
return $this->users;
}
public function addUser(UserSession $user): self
{
if (!$this->users->contains($user)) {
$this->users->add($user);
$user->setSession($this);
}
return $this;
}
public function removeUser(UserSession $user): self
{
if ($this->users->removeElement($user)) {
// set the owning side to null (unless already changed)
if ($user->getSession() === $this) {
$user->setSession(null);
}
}
return $this;
}
public function getNumberOfSeats(): ?int
{
return $this->numberOfSeats;
}
public function setNumberOfSeats(int $numberOfSeats): self
{
$this->numberOfSeats = $numberOfSeats;
return $this;
}
public function retrieveSeat($number): self
{
$this->setNumberOfSeats($this->getNumberOfSeats() - $number);
return $this;
}
}
and my EasyAdmin Controller
<?php
namespace App\Controller\Admin;
use App\Entity\Session;
use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
use EasyCorp\Bundle\EasyAdminBundle\Field\DateField;
use EasyCorp\Bundle\EasyAdminBundle\Field\BooleanField;
use EasyCorp\Bundle\EasyAdminBundle\Field\IntegerField;
use EasyCorp\Bundle\EasyAdminBundle\Field\AssociationField;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
class SessionCrudController extends AbstractCrudController
{
public static function getEntityFqcn(): string
{
return Session::class;
}
public function configureFields(string $pageName): iterable
{
return [
IdField::new('id')->hideOnForm(),
AssociationField::new('formation'),
DateField::new('beginsAt'),
DateField::new('endsAt'),
BooleanField::new('isVirtual'),
AssociationField::new('location')
->setRequired(false)
->setFormTypeOption('attr', ['placeholder' => 'Enter location']),
IntegerField::new('numberOfSeats')
->setRequired(false)
];
}
}
Thanks a lot in advance.
Here is the callback implementation. Thanks to craigh
<?php
namespace App\Entity;
........
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
class Session
{
........
#[Assert\Callback]
public function validate(ExecutionContextInterface $context, $payload)
{
if (!$this->isIsVirtual()) {
if ($this->getLocation() === null) {
$context->buildViolation("Session needs a location when it's not virtual")
->atPath('location')
->addViolation();
}
if ($this->getNumberOfSeats() === null) {
$context->buildViolation("Session needs a number of seats when it's not virtual")
->atPath('numberOfSeats')
->addViolation();
}
}
}
}
I'm new to symfony, I work on an e-commerce website. I solved some bugs already trying different solutions but with this one I feel stuck. I'm trying to display my articles by categories but it's not working because it displays an error:
Warning: Trying to access array offset on value of type null
I made a consoles controller in symfony to call the view associated to display only articles with that category, expected to do the same with the games after but it doesn't seem to find the category associated with the articles ?
This is my consolescontroller:
namespace App\Controller;
use App\Repository\ArticlesRepository;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class ConsolesController extends AbstractController
{
#[Route('/consoles', name: 'app_consoles')]
public function index(ArticlesRepository $repositoryArticles): Response
{
$consoles = $repositoryArticles->findBy(['category' => 1]);
return $this->render('consoles/index.html.twig', [
'controller_name' => 'ConsolesController',
'consoles' => $consoles,
]);
}
}
My Entity Articles
<?php
namespace App\Entity;
use App\Repository\ArticlesRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: ArticlesRepository::class)]
class Articles
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 80)]
private ?string $nomArticle = null;
#[ORM\Column(length: 255)]
private ?string $imageArticle = null;
#[ORM\Column]
private ?float $prixArticle = null;
#[ORM\Column(length: 200)]
private ?string $descriptionArticle = null;
#[ORM\ManyToMany(targetEntity: Categories::class, inversedBy: 'articles')]
#[ORM\JoinColumn(nullable: false)]
private Collection $category;
public function __construct()
{
$this->category = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getNomArticle(): ?string
{
return $this->nomArticle;
}
public function setNomArticle(string $nomArticle): self
{
$this->nomArticle = $nomArticle;
return $this;
}
public function getImageArticle(): ?string
{
return $this->imageArticle;
}
public function setImageArticle(string $imageArticle): self
{
$this->imageArticle = $imageArticle;
return $this;
}
public function getPrixArticle(): ?float
{
return $this->prixArticle;
}
public function setPrixArticle(float $prixArticle): self
{
$this->prixArticle = $prixArticle;
return $this;
}
public function getDescriptionArticle(): ?string
{
return $this->descriptionArticle;
}
public function setDescriptionArticle(string $descriptionArticle): self
{
$this->descriptionArticle = $descriptionArticle;
return $this;
}
/**
* #return Collection<int, Categories>
*/
public function getCategory(): Collection
{
return $this->category;
}
public function addCategory(Categories $category): self
{
if (!$this->category->contains($category)) {
$this->category->add($category);
}
return $this;
}
public function removeCategory(Categories $category): self
{
$this->category->removeElement($category);
return $this;
}
public function __toString()
{
return $this->nomArticle;
}
}
And my Entity Categories
<?php
namespace App\Entity;
use App\Repository\CategoriesRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: CategoriesRepository::class)]
class Categories
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 50)]
private ?string $nomCategorie = null;
#[ORM\ManyToMany(targetEntity: Articles::class, mappedBy: 'category')]
private Collection $articles;
public function __construct()
{
$this->articles = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getNomCategorie(): ?string
{
return $this->nomCategorie;
}
public function setNomCategorie(string $nomCategorie): self
{
$this->nomCategorie = $nomCategorie;
return $this;
}
/**
* #return Collection<int, Articles>
*/
public function getArticles(): Collection
{
return $this->articles;
}
public function addArticle(Articles $article): self
{
if (!$this->articles->contains($article)) {
$this->articles->add($article);
$article->addCategory($this);
}
return $this;
}
public function removeArticle(Articles $article): self
{
if ($this->articles->removeElement($article)) {
$article->removeCategory($this);
}
return $this;
}
public function __toString()
{
return $this->nomCategorie;
}
}
They're both linked by a "many to many" relationship and they're both supposed to have access to the other but there still something not working.
Thank you in advance for your help.
I am currently using EasyAdmin 4 with Symfony to create a backend admin page to manage users.
I did all the step that was in the documentation but yet it still giving me an error:
"Unable to find the controller related to the "App\Controller\Admin\User" Entity; did you forget to extend "EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController"?"
Here is the code in DashboardController:
<?php
namespace App\Controller\Admin;
use EasyCorp\Bundle\EasyAdminBundle\Config\Dashboard;
use EasyCorp\Bundle\EasyAdminBundle\Config\MenuItem;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractDashboardController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class DashboardController extends AbstractDashboardController
{
#[Route('/admin', name: 'admin')]
public function index(): Response
{
return $this->render('admin/dashboard.html.twig');
}
public function configureDashboard(): Dashboard
{
return Dashboard::new()
->setTitle('Symfony Melody')
->renderContentMaximized();
}
public function configureMenuItems(): iterable
{
yield MenuItem::linkToDashboard('Dashboard', 'fa fa-home');
yield MenuItem::linkToCrud('User', 'fas fa-user', User::class);
}
}
Here is the code in UserCrudController:
<?php
namespace App\Controller\Admin;
use App\Entity\User;
use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
class UserCrudController extends AbstractCrudController
{
public static function getEntityFqcn(): string
{
return User::class;
}
public function configureCrud(Crud $crud): Crud
{
return $crud;
}
/*
public function configureFields(string $pageName): iterable
{
return [
IdField::new('id'),
TextField::new('title'),
TextEditorField::new('description'),
];
}
*/
}
Here is the code in the User class:
<?php
namespace App\Entity;
use App\Repository\UserRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
#[ORM\Entity(repositoryClass: UserRepository::class)]
#[UniqueEntity(fields: ['email'], message: 'There is already an account with this email')]
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 180, unique: true)]
private ?string $email = null;
#[ORM\Column]
private array $roles = [];
/**
* #var string The hashed password
*/
#[ORM\Column]
private ?string $password = null;
#[ORM\Column(length: 255)]
private ?string $nom = null;
#[ORM\Column(length: 255)]
private ?string $prenom = null;
public function getId(): ?int
{
return $this->id;
}
public function getEmail(): ?string
{
return $this->email;
}
public function setEmail(string $email): self
{
$this->email = $email;
return $this;
}
/**
* A visual identifier that represents this user.
*
* #see UserInterface
*/
public function getUserIdentifier(): string
{
return (string) $this->email;
}
/**
* #see UserInterface
*/
public function getRoles(): array
{
$roles = $this->roles;
// guarantee every user at least has ROLE_USER
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
public function setRoles(array $roles): self
{
$this->roles = $roles;
return $this;
}
/**
* #see PasswordAuthenticatedUserInterface
*/
public function getPassword(): string
{
return $this->password;
}
public function setPassword(string $password): self
{
$this->password = $password;
return $this;
}
/**
* #see UserInterface
*/
public function eraseCredentials()
{
// If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null;
}
public function getNom(): ?string
{
return $this->nom;
}
public function setNom(string $nom): self
{
$this->nom = $nom;
return $this;
}
public function getPrenom(): ?string
{
return $this->prenom;
}
public function setPrenom(string $prenom): self
{
$this->prenom = $prenom;
return $this;
}
}
I tried reading the documentation again but i can't find any answers.
Can someone help please? it would be great, thank you.
You are Missing the use of App\Entity\User
In your DashboardController try to add this line :
use App\Entity\User;
On the top of your class file
Happy coding 🙂
I'm trying to display orders and order items in view like below but the result is an empty collection.
Result from Dump(order.getItems)
Controller:
<?php
namespace App\Controller;
use App\Repository\OrderRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class MyController extends AbstractController
{
#[Route('/all', name: 'app_index')]
public function index(OrderRepository $orderRepository): Response
{
return $this->render('/index.html.twig', [
'orders' => $orderRepository->findAll()
]);
}
}
Twig:
{% for order in orders %}
{{ dump(order.getItems()) }}
{% endfor %}
I have class Order mapped by orderRef in OrderItem class. The mapping is inversed by the "items" collection in Order class.
Order entity:
<?php
namespace App\Entity;
use App\Repository\OrderRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: OrderRepository::class)]
#[ORM\Table(name: '`order`')]
class Order
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\OneToMany(mappedBy: 'orderRef', targetEntity: OrderItem::class, orphanRemoval: true)]
private Collection $items;
public function __construct()
{
$this->items = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
/**
* #return Collection<int, OrderItem>
*/
public function getItems(): Collection
{
return $this->items;
}
}
OrderItem entity:
<?php
namespace App\Entity;
use App\Repository\OrderItemRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: OrderItemRepository::class)]
class OrderItem
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(inversedBy: 'items')]
#[ORM\JoinColumn(nullable: false)]
private ?Order $orderRef = null;
public function getId(): ?int
{
return $this->id;
}
public function getOrderRef(): ?Order
{
return $this->orderRef;
}
public function setOrderRef(?Order $orderRef): self
{
$this->orderRef = $orderRef;
return $this;
}
}
Instead of a findAll() you can call a new query builder in your repository with an orderBy
public function getSortedOrders()
{
$qb = $this->createQueryBuilder('o')
->orderBy('o.orderRef', 'DESC');
return $qb->getQuery()->getResult();
}```
Intention: I want to test if the validations I want are in place on the School entity, for which I want to write a test class extending TypeTestCase
Questions/problems:
I want to clear the error Error: Class "doctrine.orm.validator.unique" not found
I want to assert the error messages for each constraints of my elements. When I remove #[UniqueEntity('name')] from the model, then problem one vanishes but still the assertion self::assertCount(1, $form->getErrors()); fails. Which means $form->getErrors() does not have the validation error for the name being blank.
I am trying to write a symfony test a symfony Form type with a DB entity, with the following (stripped) definitions:
namespace App\Entity;
use App\Repository\SchoolRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity(repositoryClass: SchoolRepository::class)]
// >>>>>>> If I remove it problem 1 will be solved
#[UniqueEntity('name')]
class School implements TenantAwareInterface
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private $id;
#[Assert\NotBlank]
#[ORM\Column(type: 'string', length: 255, unique: true)]
private $name;
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
}
And form being:
namespace App\Form;
use App\Entity\School;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class SchoolType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('name');
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => School::class,
'required' => false
]);
}
}
The test:
namespace App\Tests\Integration\Form;
use App\Entity\School;
use App\Form\SchoolType;
use Doctrine\Persistence\ManagerRegistry;
use Mockery as m;
use Symfony\Bridge\Doctrine\Form\DoctrineOrmExtension;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Form\Extension\Validator\ValidatorExtension;
use Symfony\Component\Form\PreloadedExtension;
use Symfony\Component\Form\Test\Traits\ValidatorExtensionTrait;
use Symfony\Component\Form\Test\TypeTestCase;
use Symfony\Component\Validator\Validation;
use Symfony\Contracts\Translation\TranslatorInterface;
class SchoolTypeTest extends TypeTestCase
{
use ValidatorExtensionTrait;
protected function getExtensions(): array
{
$validator = Validation::createValidatorBuilder()
->enableAnnotationMapping()
->addDefaultDoctrineAnnotationReader()
->getValidator();
$mockedManagerRegistry = m::mock(ManagerRegistry::class, ['getManagers' => []]);
return [
new ValidatorExtension($validator),
new DoctrineOrmExtension($mockedManagerRegistry),
];
}
public function testValidationReturnsError()
{
$school = new School();
$form = $this->factory->create(SchoolType::class, $school);
$form->submit([]);
self::assertTrue($form->isSynchronized());
self::assertFalse($form->isValid());
// >>>>>>> I want this to assert, problem 2
self::assertCount(1, $form->getErrors());
}
}
A more simple solution :
namespace App\Tests\Service;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntityValidator;
use Symfony\Component\Form\Extension\Validator\ValidatorExtension;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\Test\Traits\ValidatorExtensionTrait;
use Symfony\Component\Form\Test\TypeTestCase;
use Symfony\Component\Validator\Validation;
class AppTypeWithValidationTestCase extends TypeTestCase
{
use ValidatorExtensionTrait;
protected function getExtensions(): array
{
$factory = new AppConstraintValidatorFactory();
$factory->addValidator(
'doctrine.orm.validator.unique',
$this->createMock(UniqueEntityValidator::class))
);
$validator = Validation::createValidatorBuilder()
->setConstraintValidatorFactory($factory)
->enableAnnotationMapping()
->addDefaultDoctrineAnnotationReader()
->getValidator();
return [
new ValidatorExtension($validator),
];
}
// *** Following is a helper function which ease the way to
// *** assert validation error messages
public static function assertFormViewHasError(FormView $formElement, string $message): void
{
foreach ($formElement->vars['errors'] as $error) {
self::assertSame($message, $error->getMessage());
}
}
}
In short, I ended up writing adding a mocked UniqueEntity validator. I added some generic codes to ease testing other form types, which are as following:
A base for tests:
namespace App\Tests\Service;
use Doctrine\Persistence\ManagerRegistry;
use Mockery as m;
use Symfony\Bridge\Doctrine\Form\DoctrineOrmExtension;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntityValidator;
use Symfony\Component\Form\Extension\Validator\ValidatorExtension;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\Test\Traits\ValidatorExtensionTrait;
use Symfony\Component\Form\Test\TypeTestCase;
use Symfony\Component\Validator\Validation;
class AppTypeWithValidationTestCase extends TypeTestCase
{
use ValidatorExtensionTrait;
protected function getExtensions(): array
{
$mockedManagerRegistry = m::mock(
ManagerRegistry::class,
[
'getManagers' => []
]
);
$factory = new AppConstraintValidatorFactory();
$factory->addValidator(
'doctrine.orm.validator.unique',
m::mock(UniqueEntityValidator::class, [
'initialize' => null,
'validate' => true,
])
);
$validator = Validation::createValidatorBuilder()
->setConstraintValidatorFactory($factory)
->enableAnnotationMapping()
->addDefaultDoctrineAnnotationReader()
->getValidator();
return [
new ValidatorExtension($validator),
new DoctrineOrmExtension($mockedManagerRegistry),
];
}
// *** Following is a helper function which ease the way to
// *** assert validation error messages
public static function assertFormViewHasError(FormView $formElement, string $message): void
{
foreach ($formElement->vars['errors'] as $error) {
self::assertSame($message, $error->getMessage());
}
}
}
A constraint validator which accepts a validator, it is needed so we can add the (mocked) definition of UniqeEntity:
namespace App\Tests\Service;
use Symfony\Component\Validator\ConstraintValidatorFactory;
use Symfony\Component\Validator\ConstraintValidatorInterface;
class AppConstraintValidatorFactory extends ConstraintValidatorFactory
{
public function addValidator(string $className, ConstraintValidatorInterface $validator): void
{
$this->validators[$className] = $validator;
}
}
And the final unit test class:
<?php
declare(strict_types=1);
namespace App\Tests\Unit\Form;
use App\Entity\School;
use App\Form\SchoolType;
use App\Tests\Service\AppTypeWithValidationTestCase;
class SchoolTypeTest extends AppTypeWithValidationTestCase
{
public function testValidationReturnsError() {
$input = [
// *** Note that 'name' is missing here
'is_enabled' => true,
];
$school = new School();
$form = $this->factory->create(SchoolType::class, $school);
$form->submit($input);
self::assertTrue($form->isSynchronized());
self::assertFalse($form->isValid());
$view = $form->createView();
self::assertFormViewHasError($view->children['name'], 'This value should not be blank.');
}
}