Doctrine 2 Index Inheritance - php

i am new to Doctrine 2 am i trying to figure out how to do index inheritance with it.
What i am trying to achieve is to have a base class that defines some default columns along with necessary indexes for all entities within the application.
Example: All tables in my application will have created_on and modified_on, so i prepared one base #MappedSuperclass with those 2 columns in it.
Here is my code:
<?php
/**
* #MappedSuperclass
*/
abstract class EntityAbstract
{
/**
* #Column(type="datetime", name="created_on", nullable=false)
*/
protected $createdOn;
/**
* #Column(type="datetime", name="modified_on", nullable=true)
*/
protected $modifiedOn;
}
/**
* #Entity
* #Table(name="member", indexes={#Index(name="credential", columns={"email_address", "password"})})
*/
class Member extends EntityAbstract
{
/**
* #Column(type="string", name="full_name", length=50)
*/
protected $fullName;
/**
* #Column(type="string", name="email_address", length=50, unique=true, nullable=false)
*/
protected $emailAddress;
/**
* #Column(type="string", name="password", length=40, nullable=false)
*/
protected $password;
}
?>
I want to enforce created_on to be an index, so i put #Index annotation in the base class for this particular column. Hoping that this will result in 2 indexes for Member which is created_on and email_address+password combination. This however results in indexes of the base class being overriden by the child class, thus created_on is not an index.
/**
* #MappedSuperclass
* #Table(indexes={#Index(name="timestampcreated", columns={"created_on"})})
*/
abstract class EntityAbstract
How do i achieve this in Doctrine 2? Have looked at single table inheritance, but my understanding is that it's meant for different purpose.

Here's a full solution for the way laid out at https://github.com/doctrine/orm/issues/5928#issuecomment-273624392, thanks to https://medium.com/#alexkunin/doctrine-symfony-adding-indexes-to-fields-defined-in-traits-a8e480af66b2
namespace App\EventListener;
use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
use App\Entity\Member;
class MemberIndexListener
{
public function loadClassMetadata(LoadClassMetadataEventArgs $eventArgs)
{
/** #var Doctrine\ORM\Mapping\ClassMetadata $classMetadata */
$classMetadata = $eventArgs->getClassMetadata();
if (Member::class === $classMetadata->getName())
{
$prefix = strtolower((new \ReflectionClass($classMetadata->getName()))->getShortName()); // you can omit this line and ...
$classMetadata->table['indexes'][$prefix.'_created_on'] = // ... replace `[$prefix.'_created_on']` with `[]` for automatic index naming
['columns' => ['username']
];
// For UniqueConstraints, use:
// $classMetadata->table['uniqueConstraints'][...] = ...
}
}
}
And in services.yaml:
services:
App\EventListener\MemberIndexListener:
tags:
- { name: doctrine.event_listener, event: loadClassMetadata }

I wanted to do the same thing, but #Table is not allowed in #mappedSuperclass classes. I've posted issue on doctrine2 github page https://github.com/doctrine/doctrine2/issues/5928 and the answer is:
#Table is not valid on a mappedsuperclass.
When you think about it is sounds logical, but still, it would be nice if there is such functionality, to inherit indexes and other declarations.

This is my solution for automatic index inheritance.
You can specify indexes in table annotation of mapped superclass. And all children will extend them.
The listener takes indexes of all parent mapped superclases.
#src/EventListener/InheritIndexListener.php
<?php
namespace App\EventListener;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
use Doctrine\ORM\Mapping\ClassMetadata;
final class InheritIndexListener
{
/** #var EntityManagerInterface */
private $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
public function loadClassMetadata(LoadClassMetadataEventArgs $eventArgs)
{
$classMetadata = $eventArgs->getClassMetadata();
$name = $classMetadata->name;
if (!$classMetadata->isMappedSuperclass) {
foreach (class_parents($name) as $parent) {
$this->addParentIndexes($parent, $classMetadata);
}
}
}
private function addParentIndexes(string $parent, ClassMetadata $classMetadata): void
{
$parentMetadata = $this->entityManager
->getClassMetadata($parent);
foreach ($parentMetadata->table['indexes'] as $index) {
if (!$this->hasSameIndex($index, $classMetadata->table['indexes'])) {
$classMetadata->table['indexes'][] = $index;
}
}
foreach ($parentMetadata->table['uniqueConstraints'] as $uniqueConstraint) {
if (!$this->hasSameIndex($uniqueConstraint, $classMetadata->table['uniqueConstraints'])) {
$classMetadata->table['uniqueConstraints'][] = $uniqueConstraint;
}
}
}
private function hasSameIndex(array $needle, array $haystack): bool
{
foreach ($haystack as $item) {
if ($item['columns'] === $needle['columns']) {
return true;
}
}
return false;
}
}
#config/services.yaml
services:
App\EventListener\InheritIndexListener:
tags:
- { name: doctrine.event_listener, event: loadClassMetadata }
And entities.
#src/Entity/BaseResource.php
<?php
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\MappedSuperclass
* #ORM\Table(
* indexes={
* #ORM\Index(columns={"external_id"}),
* }
* )
*/
abstract class BaseResource
{
/**
* #var int|null
* #ORM\Column(type="integer")
*/
private $externalId;
//...
}
#src/Entity/BaseSlugableResource.php
<?php
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\MappedSuperclass
* #ORM\Table
* uniqueConstraints={
* #ORM\UniqueConstraint(columns={"slug"}),
* },
* )
*/
abstract class BaseSlugableResource extends BaseResource
{
/**
* #var string|null
* #ORM\Column(type="string")
*/
private $slug;
//...
}
#src/Entity/Article.php
<?php
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
*/
class Article extends BaseSlugableResource
{
//...
}
In result the Article entity will have externalId index and slug unique index.

Related

Inheritance relationship with interfaces in symfony/doctrine

I have entities like this :
Request.php (parent)
/**
* #ORM\Entity(repositoryClass=RequestRepository::class)
* #ORM\InheritanceType("JOINED")
* #ORM\DiscriminatorColumn(name="type", type="string")
* #ORM\DiscriminatorMap({
* "requestA" = "RequestA",
* "requestB" = "RequestB"
* })
*/
abstract class Request
{
/*...*/
}
RequestA.php (child A)
/**
* #ORM\Entity(repositoryClass=DemenagementRepository::class)
*/
class RequestA extends Request implements AddressEntityInterface
{
/**
* #ORM\OneToOne(targetEntity=Address::class, inversedBy="request")
* #ORM\JoinColumn(nullable=false)
*/
private $address;
// +others...
}
RequestB.php (child B)
/**
* #ORM\Entity(repositoryClass=DemenagementRepository::class)
*/
class RequestB extends Request implements AddressEntityInterface
{
/**
* #ORM\OneToOne(targetEntity=Address::class, inversedBy="request")
* #ORM\JoinColumn(nullable=false)
*/
private $address;
// +others...
}
AddressEntityInterface.php
interface AddressEntityInterface
{
public function getAddress(): ?Address;
public function setAddress(Address $address): self;
}
Address.php
/**
* #ORM\Entity(repositoryClass=AddressRepository::class)
*/
class Address
{
/**
* #ORM\OneToOne(targetEntity=??????, mappedBy="address")
*/
private $request;
public function getRequest() { /* ... */ }
}
I want to use getRequest() revert relationship how can i do for make targetEntity dynamically ?
Thanks
I believe you need polymorphic relationships or as they like to call them from docrine 'Inheritance Mapping'
I leave you some sites to view on which you can find everything you need
Official Documentation (Doctrine)
Stackoverflow Practical example
Youtube tutorial
Stackoverflow, discussion <-
personally I advise you to read all this so you get a concrete idea of ​​what you need
I hope my little routing will help you.

Doctrine always filter rows with certain column values

I am using Symfony 5.1 with doctrine. I would like to know how to put a filter on a field/column for all doctrine queries that do a search on an entity. For example, with the entity Sejour I would like to make sure all queries that search for this entity have the where by clause on the field/column: "sejAnnule != 'Y'". Here is the Sejour entity:
<?php
namespace App\Entity;
use DateTime;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* Sejour.
*
* #ORM\Table(name="Sejour")})
* #ORM\Entity(repositoryClass="App\Repository\SejourRepository")
*/
class Sejour
{
/**
* #ORM\Column(name="SEJ_NO", type="integer", nullable=false)
* #ORM\Id
*/
private int $sejNo;
/**
* #var string|null
*
* #ORM\Column(name="SEJ_ANNULE", type="string", length=1, nullable=true)
*/
private string $sejAnnule;
public function getSejAnnule(): ?string
{
return $this->sejAnnule;
}
public function setSejAnnule(?string $sejAnnule): void
{
$this->sejAnnule = $sejAnnule;
}
public function getSejNo(): int
{
return $this->sejNo;
}
public function setSejNo(int $sejNo): void
{
$this->sejNo = $sejNo;
}
}
I think this is possible with doctrine filters but I was wondering if anyone knows a quicker way to do this (e.g. an annotation on the field or a bundle)?
The easiest way to this I think is to use a doctrine filter. No need to create an event listener (as i first thought). Create the filter:
<?php
namespace App\Filter;
use App\Entity\Sejour;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Query\Filter\SQLFilter;
class SoftDeleteFilter extends SQLFilter
{
/**
* {#inheritdoc}
*/
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias)
{
if (Sejour::class == $targetEntity->getReflectionClass()->name) {
return sprintf(' %s.SEJ_ANNULE != \'Y\'', $targetTableAlias);
}
return '';
}
}
Then enable the filter in doctrine.yaml:
orm:
filters:
soft_delete_filter:
class: AppBundle\Doctrine\SoftDeleteFilter
enabled: true
Symfony documentation: https://symfony.com/doc/current/bundles/DoctrineBundle/configuration.html#filters-configuration
The Symfony Casts documentation (although this is with an event listener) https://symfonycasts.com/screencast/doctrine-queries/filters

Could not determine access type for property "games" in class "App\Entity\GameGenre":

I have a Symfony 4.2 application. There are Entities Game and GameGenre. They have ManyToMany relation between each other. I am trying to load fixtures and receive the following error:
Could not determine access type for property "games" in class "App\Entity\GameGenre": The property "games" in class "App\Entity\GameGenre" can be defined with the methods "addGame()", "removeGame()" but the new value must be an array or an instance of \Traversable, "App\Entity\Game" given.
My code is the following.
Game.php
<?php
namespace App\Entity;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* #ORM\Entity(repositoryClass="App\Repository\GameRepository")
*/
class Game
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
protected $id;
...
/**
* #ORM\ManyToMany(
* targetEntity="App\Entity\GameGenre",
* inversedBy="games"
* )
*/
private $genres;
...
/**
* #return Collection|GameGenre[]
*/
public function getGenres() : Collection
{
return $this->genres;
}
public function addGenre(GameGenre $genre): self
{
if (!$this->genres->contains($genre)) {
$this->genres[] = $genre;
$genre->addGame($this);
}
return $this;
}
public function removeGenre(GameGenre $genre): self
{
if ($this->genres->contains($genre)) {
$this->genres->removeElement($genre);
$genre->removeGame($this);
}
return $this;
}
...
public function __construct()
{
$this->genres = new ArrayCollection();
}
}
GameGenre.php
<?php
namespace App\Entity;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* #ORM\Entity(repositoryClass="App\Repository\GameGenreRepository")
*/
class GameGenre
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
protected $id;
/**
* #ORM\ManyToMany(
* targetEntity="App\Entity\Game",
* mappedBy="genres"
* )
* #ORM\OrderBy({"name" = "ASC"})
*/
private $games;
...
/**
* #return Collection|Game[]
*/
public function getGames() : Collection
{
return $this->games;
}
public function addGame(Game $game): self
{
if (!$this->games->contains($game)) {
$this->games[] = $game;
$game->addGenre($this);
}
return $this;
}
public function removeGame(Game $game): self
{
if ($this->games->contains($game)) {
$this->games->removeElement($game);
$game->removeGenre($this);
}
return $this;
}
public function __construct()
{
$this->games = new ArrayCollection();
}
}
And it looks like there is nothing really strange in fixtures yamls:
genre.yaml
App\Entity\GameGenre:
genre_{1..9}:
...
games: '#game_*'
game.yaml has no mentioning of genre field but I tried to change relation side by calling addGenre() instead of addGame() or use them both in both fixture files but nothing help, so I think there is some other problem.
Could you please help me?
Your field is an array, but you are trying to insert a single value, it should be:
App\Entity\GameGenre:
genre_{1..9}:
...
games: ['#game_*']
or
App\Entity\GameGenre:
genre_{1..9}:
...
games:
- '#game_*'

Serializing objects with relationships to ZF3 MVC Response JSON

I have a Zend Framework 3 app. I added the ViewJsonStrategy to module.config.php. But i wants return a JSON Object with their relation objects ONE TO MANY in Array:
On my controller
public function getdirectoriojsonAction(){
$idraiz = $this->cfgGral->getIdDirectorioRaiz();
if ($idraiz <= 0) {
return $this->redirect()->toRoute('configuracion', ['action' => 'index']);
} else {
if ($this->params()->fromRoute('id') > 0) {
$idraiz = $this->params()->fromRoute('id');
}
$directorio = $this->em->find($this->rutaEntityDirectorio, $idraiz);
if ($directorio->getEstado() != 0) {
$directorio = $directorio->getPadre();
$directorio->getDirectoriosHijos();
$directorio->getArchivosHijos();
}
}
$hydrator = new Reflection;
return new JsonModel($hydrator->extract($directorio));
}
The Entity Directorio
<?php
namespace Directorios\Entity;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Zend\Form\Annotation as ZendAnnotation;
use Directorios\Model\ArchivoInterface;
use Doctrine\Common\Collections\ArrayCollection;
/**
* #ORM\Entity
* #ORM\Table (name="directorio")
*
*/
class Directorio
{
/**
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
* #ORM\Column(type="integer")
* #ZendAnnotation\Exclude()
* #var int|null
*/
private $id;
/**
* #ORM\Column(type="string")
* #var string
* #Required
* #ZendAnnotation\Filter({"name":"StringTrim"})
* #ZendAnnotation\Validator({"name":"StringLength", "options":{"min":1, "max":60}})
* #ZendAnnotation\Validator({"name":"Regex", "options":{"pattern":"/^[a-zA-Z][a-zA-Z0-9_-]{0,24}$/"}})
* #ZendAnnotation\Attributes({"type":"text"})
* #ZendAnnotation\Options({"label":"Nombre:"})
*/
private $nombre;
/**
* #ORM\ManyToOne(targetEntity="Directorios\Entity\Directorio", inversedBy="directoriosHijos")
* #ORM\JoinColumn(name="padre", referencedColumnName="id")
*/
private $padre;
/**
* #ORM\OneToMany(targetEntity="Directorios\Entity\Directorio", mappedBy="padre",cascade={"persist", "remove"})
*/
private $directoriosHijos;
/**
* #ORM\OneToMany(targetEntity="Directorios\Entity\Archivo", mappedBy="padre", cascade={"persist", "remove"})
*/
private $archivosHijos;
/**
* #ORM\Column(type="datetime")
* #var \DateTime
*/
private $fechaCreacion;
/**
* #ORM\Column(type="datetime")
* #var \DateTime
*/
private $fechaModificacion;
/**
* #ORM\Column(name="ruta_real")
* #ORM\Column(type="text")
*/
private $ruta_real;
/**
*
* #ORM\Column(type="integer")
*
*/
private $estado=0;
/**
*
* #ORM\Column(type="integer")
*
*/
private $tipo;
//....... methods
public function __construct(){
$this->archivosHijos=new ArrayCollection();
$this->directoriosHijos=new ArrayCollection();
}
}
The JSON response:
id 2
nombre "Nuevo directorio"
padre Object <- Returns Objects
directoriosHijos Object <- Returns Objects
archivosHijos Object <- Returns Objects
fechaCreacion
date "2017-09-09 21:23:20.000000"
timezone_type 3
timezone "Europe/Berlin"
fechaModificacion
date "2017-09-09 21:23:20.000000"
timezone_type 3
timezone "Europe/Berlin"
ruta_real "D:\\testDirectorioRaiz"
estado 0
tipo 0
The Objects relationated come like Object not like Array().
How i can do that relationated objects arrives like Json Array() too?
The Reflection Hydrator itself does not allow nested hydration/extraction.
However Hydrator Aggregates do so, but you have to invest a bit more work into it then just simply instantiating it. If you choose this route I would invest a bit more time and injecting it into the controller in order to keep testability high
Also consider using the Doctrine Hydrator provided by the doctrine/doctrine-module composer package. The project also has a short documentation on hydration
Thanks jeger, i was search for a most simply solution, but i see that's not for this case. Just now i start investigate the doctrine hydrators, however for now i fix with a rustic recursion XD. I lets the code here bellow, maybe will be helpfull.
In my controller ...
// Method with response JSON
public function getdirectoriojsonAction(){
$idraiz = $this->cfgGral->getIdDirectorioRaiz();
if ($idraiz <= 0) {
return $this->redirect()->toRoute('configuracion', ['action' => 'index']);
} else {
if ($this->params()->fromRoute('id') > 0) {
$idraiz = $this->params()->fromRoute('id');
}
$directorio = $this->em->find($this->rutaEntityDirectorio,$idraiz);
if ($directorio->getEstado() == 0) {
$hydrator = new Reflection();
$dir = $hydrator->extract($directorio);
$dir = $this->getArrayHijosRec($dir, $hydrator);
}else{
return $this->redirect()->toRoute('directorios',['action'=>'error','id'=>2]);
}
}
return new JsonModel($dir);
}
// recursive method ... is not the best practice but works ...
private function getArrayHijosRec($directorioArray, Reflection $hydrator){
$directoriosHijos=$directorioArray['directoriosHijos'];
$archivosHijos=$directorioArray['archivosHijos'];
$padre=$directorioArray['padre'];
$directorioArray['directoriosHijos']=[];
$directorioArray['archivosHijos']=[];
$directorioArray['padre']=[];
$padre=(is_object($padre))?$hydrator->extract($padre):[];
$directorioArray['padre']=$padre;
foreach ($archivosHijos as $archHijo){
$archHijo=$hydrator->extract($archHijo);
$archHijo['padre']=$padre;
array_push($directorioArray['archivosHijos'],$archHijo);
}
foreach ($directoriosHijos as $dirHijo) {
$dirHijo=($hydrator->extract($dirHijo))
array_push($directorioArray['directoriosHijos'],($this->getArrayHijosRec($dirHijo,$hydrator)));
}
return $directorioArray;
}

Symfony2: Unique constraint for a string property is not working for values set in prePersist() method in a subscriber class

I have a form where a user enters his phone number. A common problem is that a phone number can be written in many different ways: "+49 711 XXXXXX", "0049 (0)711 XXXXXX" or "+49 711 - XXXXXX" are all presentations of the same phone number. In order to detect duplicates I use the "phone-number-bundle" (https://github.com/misd-service-development/phone-number-bundle) to get a "normalized" E.164 representation of the phone number that can be used for comparison. If a duplicate is detected, the entered number must not be stored and a notice has to be shown to the user.
If the entered phone number is a valid phone number, I want to check if the E.164-formatted value of the phone number is already stored in the database table.
This is the MySQL table for phone numbers:
-+----+---------------------+----------------+
| id | original | phonenumber |
-+----+---------------------+----------------+
| 1 | 0711-xxxxxxx | +49711xxxxxxx |
-+----+---------------------+----------------+
| 2 | +49 7034 / xxxxx-xx | +497034xxxxxxx |
-+----+---------------------+----------------+
| 3 | +49 (0)171/xxxxxxx | +49171xxxxxxx |
-+----+---------------------+----------------+
| .. | ... | ... |
-+----+---------------------+----------------+
"phonenumber" contains the E.164 formatted value of the value entered in the form. The first originally entered value is stored in the column "original" as additional information.
The form is defined in "src/AppBundle/Form/PhonenumberType.php":
<?php
namespace AppBundle\Form;
use Symfony\Component\Form\AbstractType;
use libphonenumber\PhoneNumberFormat;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class PhonenumberType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
//->add('phonenumber') // Remove comments to see that the unique constraint works when the phonenumber is submitted via form
->add('original')
;
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Phonenumber'
));
}
}
Phonenumber Entity "src/AppBundle/Entity/Phonenumber.php":
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use AppBundle\Validator\Constraints as PhonenumberAssert;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
/**
* Phonenumber
*
* #ORM\Table(name="phonenumber",
* uniqueConstraints={
* #ORM\UniqueConstraint(columns={"phonenumber"})
* })
* #ORM\Entity
* #UniqueEntity("phonenumber")
* #ORM\HasLifecycleCallbacks()
*/
class Phonenumber
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer", precision=0, scale=0, nullable=false, unique=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="phonenumber", type="string", length=255, unique=true)
*/
private $phonenumber;
/**
* #var string
*
* #ORM\Column(name="original", type="string", length=255, precision=0, scale=0, nullable=true, unique=false)
* #Assert\NotBlank
* #PhonenumberAssert\IsValidPhoneNumber
*/
private $original;
/**
* Constructor
*/
public function __construct()
{
}
/**
* Returns phonenumber.
*
* #return string
*/
public function __toString()
{
return $this->getPhonenumber();
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set phonenumber
*
* #param string $phonenumber
*
* #return Phonenumber
*/
public function setPhonenumber($phonenumber)
{
$this->phonenumber = $phonenumber;
return $this;
}
/**
* Get phonenumber
*
* #return string
*/
public function getPhonenumber()
{
return $this->phonenumber;
}
/**
* Set original
*
* #param string $original
*
* #return Phonenumber
*/
public function setOriginal($original)
{
$this->original = $original;
return $this;
}
/**
* Get original
*
* #return string
*/
public function getOriginal()
{
return $this->original;
}
}
Defined services in "app/config/services.yml":
services:
phonenumber_validation:
class: AppBundle\Validator\Constraints\IsValidPhoneNumberValidator
arguments: ["#service_container"]
tags:
- { name: validator.constraint_validator, alias: phonenumber_validation }
my.subscriber:
class: AppBundle\EventListener\PhoneNumberNormalizerSubscriber
calls:
- [setContainer, ["#service_container"]]
tags:
- { name: doctrine.event_subscriber, connection: default }
The subscriber class "src/AppBundle/EventListener/PhoneNumberNormalizerSubscriber.php":
<?php
namespace AppBundle\EventListener;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\LifecycleEventArgs;
use AppBundle\Entity\Phonenumber;
use libphonenumber\NumberParseException;
use libphonenumber\PhoneNumber;
use libphonenumber\PhoneNumberFormat;
use libphonenumber\PhoneNumberUtil;
class PhoneNumberNormalizerSubscriber implements EventSubscriber
{
/** #var ContainerInterface */
protected $container;
/**
* #param ContainerInterface #container
*/
public function setContainer(ContainerInterface $container)
{
$this->container = $container;
}
public function getSubscribedEvents()
{
return array(
'prePersist',
'preUpdate',
);
}
// Executed when data is stored for the first time
public function prePersist(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
// only act on "Phonenumber" entity
if ($entity instanceof Phonenumber)
{
$entityManager = $args->getEntityManager();
$phoneNumberObj = $this->container->get('libphonenumber.phone_number_util')->parse($entity->getOriginal(), 'DE');
$normalized_phonenumber = $this->container->get('libphonenumber.phone_number_util')->format($phoneNumberObj, PhoneNumberFormat::E164);
$entity->setPhonenumber($normalized_phonenumber);
}
}
// Executed when data is already stored
public function preUpdate(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
// only act on "Phonenumber" entity
if ($entity instanceof Phonenumber)
{
$entityManager = $args->getEntityManager();
$phoneNumberObj = $this->container->get('libphonenumber.phone_number_util')->parse($entity->getOriginal(), 'DE');
$normalized_phonenumber = $this->container->get('libphonenumber.phone_number_util')->format($phoneNumberObj, PhoneNumberFormat::E164);
$entity->setPhonenumber($normalized_phonenumber);
}
}
}
The constraint class "src/AppBundle/Validator/Constraints/IsValidPhoneNumber.php":
<?php
namespace AppBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
/**
* #Annotation
*/
class IsValidPhoneNumber extends Constraint
{
public $message_invalid = 'Not a valid phone number: "%string%"';
/**
* #return string
*/
public function validatedBy()
{
return 'phonenumber_validation';
}
}
The validator class "src/AppBundle/Validator/Constraints/IsValidPhoneNumberValidator.php":
<?php
namespace AppBundle\Validator\Constraints;
use Symfony\Component\DependencyInjection\ContainerInterface as Container;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use libphonenumber\NumberParseException;
use libphonenumber\PhoneNumber;
use libphonenumber\PhoneNumberFormat;
use libphonenumber\PhoneNumberUtil;
class IsValidPhoneNumberValidator extends ConstraintValidator
{
private $container;
/**
* Construct
*/
public function __construct(Container $container)
{
$this->container = $container;
}
/**
* Validate
*
* #param mixed $value
* #param Constraint $constraint
*/
public function validate($value, Constraint $constraint)
{
if ($value != '' )
{
$phoneNumberObj = $this->container->get('libphonenumber.phone_number_util')->parse($value, 'DE');
if (!$this->container->get('libphonenumber.phone_number_util')->isValidNumber($phoneNumberObj))
{
$this->context->buildViolation($constraint->message_invalid)
->setParameter('%string%', $value)
->addViolation();
}
}
}
}
The phone number validator works - if a number is invalidated by the "phone-number-bundle", a message "Not a valid phone number: "3333333333333333" is displayed. The E.164 formatted value gets also saved correctly into the database table.
Problem: Although I use "#ORM\UniqueConstraint(columns={"phonenumber"})", "#UniqueEntity("phonenumber")" and "unique=true" for the $phonenumber attribute in the entity class, every entered valid number from the form gets stored in the database, no matter if there is already a duplicate in the table or not. The unique constraint does not work when the phonenumber field is not added in the form type class.
May be interesting: When I remove the comment in the PhonenumberType class so that
->add('phonenumber')
is included again and an existing number is entered in the associated form field "phonenumber", I get "This value is already used." like expected.
What am I doing wrong?
Thanks for helping!
Indeed, it is not.
You have to differentiate:
Form validation. It occurs when your controller calls $form->handle($request) or something similar, triggered on a form event
Doctrine callbacks, that are called during the EntityManager flush()
The solution is then to use that bundle not on prePersist(), but rather on a Form event. Data will be normalized before writing it to the entity, then effectively validating what you will save to DB.
Such a code would look like this:
<?php
namespace AppBundle\Form;
use Symfony\Component\Form\AbstractType;
use libphonenumber\PhoneNumberFormat;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
class PhonenumberType extends AbstractType
{
private $phoneNumberUtil;
public function __construct(PhoneNumberUtil $phoneNumberUtil)
{
$this->phoneNumberUtil = $phoneNumberUtil;
}
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('phonenumber', 'hidden')
->add('original')
->addEventListener(FormEvents::SUBMIT, function(FormEvent $event) use ($this) {
$entity = $event->getData();
$phoneNumber = $this->phoneNumberUtil->parse($entity->getOriginal(), PhoneNumberUtil::UNKNOWN_REGION);
$normalized_phonenumber = $this->phoneNumberUtil->format($phoneNumberObj, PhoneNumberFormat::E164);
$entity->setPhoneNumber($normalized_phonenumber);
;
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Phonenumber'
));
}
}
One more thing, declaring your form type as a service will make the build simplfer, have a look there: http://symfony.com/doc/current/book/forms.html#defining-your-forms-as-services
Edit, here is the YML service definition:
app.contact_type:
class: AppBundle\Form\PhonenumberType
arguments:
- #libphonenumber.phone_number_util
tags:
- { name: form.type, alias: 'phone_number' }

Categories