Symfony constraint to prevent duplicates in overlapping time frame - mysql

How can I enforce uniqueness of a value within overlapping date range in Symfony using Doctrine ORM.
I have the following entity
<?php
/**
* #ORM\Entity
* #ORM\Table("tax_component")
*/
class TaxComponent
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(name="tax_component_id", type="integer")
*/
private ?int $id;
/**
* #ORM\Column(name="tax_component_name", type="string", length=20)
*/
private string $name;
/**
* #ORM\Column(name="tax_component_rate", type="integer")
* #Assert\GreaterThanOrEqual(0)
*/
private int $rate;
/**
* #ORM\Column(name="tax_component_applicable_from", type="datetime_immutable")
*/
private DateTimeInterface $applicableFrom;
/**
* #ORM\Column(name="tax_component_applicable_to", type="datetime_immutable")
*/
public function __construct(string $name, int $rate, ?DateTimeImmutable $applicableFrom = null, ?DateTimeImmutable $applicableTo = null)
{
...
}
}
I want to make $name unique withing overlapping time frames of $applicableFrom and $applicableTo. For example,
$repository->save(
new TaxComponent('inter-state', 1800, new DateTime('2018-04-01:00:00:00'), new DateTime('2019-03-31T23:59:59'))
);
// The following should be allowed since there is no overlap between the two time ranges using the name 'inter-state'
$repository->save(
new TaxComponent('inter-state', 1200, new DateTime('2019-04-01:00:00:00'), new DateTime('2020-03-31T23:59:59'))
);
// The following should fail since 'inter-state' is ambiguous during the period 2019-09-01:00:00:00 to 2020-03-31T23:59:59
$repository->save(
new TaxComponent('inter-state', 1800, new DateTime('2019-09-01:00:00:00'), new DateTime('2020-09-31T23:59:59'))
);
Is there a constraint to enforce this is Symfony?
I am currency planning to check for existing entities from within TaxComponentRepository::save, before calling $this->entityManager->persist. Is there a better solution?

The cleaner way would be to create your own custom assert.
Starting by creating your constraint :
<?php
namespace App\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
/**
* #Annotation
*/
class TaxComponentConstraint extends Constraint
{
public $message = 'Another tax component overlap this one: {{ taxComponent}}';
public function getTargets()
{
return self::CLASS_CONSTRAINT;
}
public function validatedBy()
{
return 'App\Validator\Constraints\TaxComponentValidator';
}
}
And now you have to create a validator that will check if there exist an overlap with two tax component.
<?php
namespace App\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
class TaxComponentValidator extends ConstraintValidator
{
public function validate($taxComponentObject, Constraint $constraint)
{
//Check however you want if the tax component can be created (So no overlap between two existing TaxComponent)
if($overlappingTaxComponent){
$this->context->buildViolation($constraint->message)
->setParameter('{{ taxComponent }}', $overlappingTaxComponent->__toString())
->addViolation();
}
}
}
Here, $overlappingTaxComponent is a TaxComponent preventing us from making one because of your constraint.
If the constraint is properly done, you can now use it easily in your entity so that it check automatically when submitting the form :
<?php
//...
use App\Validator\Constraints as CustomAssert;
/**
* #ORM\Entity
* #ORM\Table("tax_component")
* #CustomAssert\TaxComponentConstraint
*/
class TaxComponent
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(name="tax_component_id", type="integer")
*/
private ?int $id;
/**
* #ORM\Column(name="tax_component_name", type="string", length=20)
*/
private string $name;

Related

how to serialize only one level deep of an entity with symfony serializer?

I would like to serialize my objects into text fields in order to store a representation of them for traceability.
What I really want is a JSON representation of the entity's properties, and whenever there is an object, I would like a JSON representation of that as well, but only on that first level, I don't want it to dig deeper into what possible objects and relations there is below that, I'm happy with the object ID's
Many objects reference other objects, and from this documentation https://symfony.com/doc/current/components/serializer.html#handling-circular-references it appears this can be easily handled by just storing the object's ID rather than serializing the entire object (again). But in my case it doesn't work =) Am I missing something critical here?
Entity
<?php
namespace App\Entity;
use App\Repository\RegularServiceHoursRepository;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass=RegularServiceHoursRepository::class)
*/
class RegularServiceHours
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="datetime")
*/
private $open;
/**
* #ORM\Column(type="datetime")
*/
private $close;
/**
* #var object \App\Entity\ACRGroup
*
* #ORM\ManyToOne(targetEntity="\App\Entity\ACRGroup", inversedBy="regularServiceHours")
* #ORM\JoinColumn(name="acr_group", referencedColumnName="id", nullable=false)
*/
protected $ACRGroup;
public function getId(): ?int
{
return $this->id;
}
public function getOpen(): ?\DateTimeInterface
{
return $this->open;
}
public function setOpen(\DateTimeInterface $open): self
{
$this->open = $open;
return $this;
}
public function getClose(): ?\DateTimeInterface
{
return $this->close;
}
public function setClose(\DateTimeInterface $close): self
{
$this->close = $close;
return $this;
}
/**
* Set aCRGroup
*
* #param \App\Entity\ACRGroup $aCRGroup
*
* #return DebitPeriod
*/
public function setACRGroup(\App\Entity\ACRGroup $aCRGroup)
{
$this->ACRGroup = $aCRGroup;
return $this;
}
/**
* Get aCRGroup
*
* #return \App\Entity\ACRGroup
*/
public function getACRGroup()
{
return $this->ACRGroup;
}
/**
* Get debitTimeSeconds
*
* #return int
*/
public function getTimeSeconds()
{
$open = $this->getOpen();
$close = $this->getClose();
$r = $close->format('U') - $open->format('U');
return $r;
}
}
Controller
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Encoder\XmlEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Serializer;
public function log($type,$message,$unit=null,$previous=null,$current=null) {
//We only log successful operations.
//If you want to log also errors, be very careful to not have anything persisted already, in que to be flushed. The flush below will store possible entities under conflict check and entities that sent you here due to a failed validation/conflict check, entities that weren't meant to be saved but rejected and forgotten.
if ($type == 'success') {
$encoder = new JsonEncoder();
$defaultContext = [
AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER => function ($object, $format, $context) {
return $object->getId();
},
];
$normalizer = new ObjectNormalizer(null, null, null, null, null, null, $defaultContext);
$serializer = new Serializer([$normalizer], [$encoder]);
dd($serializer->serialize($current, 'json'));
It never gets to the dump action, it chews for 30 seconds until memory is exhausted, and then it says:
Error: Maximum execution time of 30 seconds exceeded
Have I somehow missed where to set a default depth of 1? (I understood depth 1 to be the default when nothing was set).
Pay attention to Serialization Groups Attributes. With this attribute you can select the desired data when serializing an object.
Specify groups to the desired object properties:
class RegularServiceHours
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
* #Groups({"default"})
*/
private $id;
/**
* #ORM\Column(type="datetime")
*/
private $open;
/**
* #ORM\Column(type="datetime")
* #Groups({"default"})
*/
private $close;
/**
* #var object \App\Entity\ACRGroup
*
* #ORM\ManyToOne(targetEntity="\App\Entity\ACRGroup", inversedBy="regularServiceHours")
* #ORM\JoinColumn(name="acr_group", referencedColumnName="id", nullable=false)
* #Groups({"default"})
*/
private $ACRGroup;
}
Also specify a group for the ACRGroup identifier
class ACRGroup
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
* #Groups({"default"})
*/
private $id;
}
And just specify the group when serializing the object
$serializer->serialize($current, 'json', ['groups' => ['default']]);
I think that should solve your problem

doctrine2 , retrieve associated data from inverse side

i have 2 entities.
the simple logic is that a user has many delivery address, so in the future he will be able to choose one of them for their deliverys.
first entity, Direccion (address).
<?php
use Doctrine\ORM\Mapping as ORM;
/**
* Direccion
*
* #ORM\Table(name="direccion", indexes={#ORM\Index(name="id_usuario",
columns={"id_usuario"})})
* #ORM\Entity
*/
class Direccion
{
/**
* #var string
*
* #ORM\Column(name="Calle", type="string", length=100, nullable=false)
*/
private $calle;
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* #var \Usuario
*
* #ORM\ManyToOne(targetEntity="Usuario", inversedBy="direcciones", fetch="EAGER")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="id_usuario", referencedColumnName="id" )
* })
*/
private $idUsuario;
/**
* Set calle
*
* #param string $calle
* #return Direccion
*/
public function setCalle($calle)
{
$this->calle = $calle;
return $this;
}
/**
* Get calle
*
* #return string
*/
public function getCalle()
{
return $this->calle;
}
/**
* Set idUsuario
*
* #param \Usuario $idUsuario
* #return Direccion
*/
public function setIdUsuario(\Usuario $idUsuario = null)
{
$this->idUsuario = $idUsuario;
return $this;
}
/**
* Get idUsuario
*
* #return \Usuario
*/
public function getIdUsuario()
{
return $this->idUsuario;
}
}
and the second entity is, Usuario (User)
<?php
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* Usuario
*
* #ORM\Table(name="usuario")
* #ORM\Entity
*/
class Usuario
{
/**
* #var string
*
* #ORM\Column(name="nombre", type="string", length=255, nullable=true)
*/
private $nombre;
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* #ORM\OneToMany(targetEntity="Direccion", mappedBy="id_usuario", cascade={"persist"})
*/
private $direcciones;
public function __construct()
{
$this->direcciones = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Set nombre
*
* #param string $nombre
* #return Usuario
*/
public function setNombre($nombre)
{
$this->nombre = $nombre;
return $this;
}
/**
* Get nombre
*
* #return string
*/
public function getNombre()
{
return $this->nombre;
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
public function getDirecciones()
{
return $this->direcciones;
}
public function setDirecciones($direcciones)
{
$this->direcciones = $direcciones;
return $this;
}
}
i have already readed many blogs and similar questions , but i can't get the property $direcciones hydrated or filled with the associated data, when i try to achieve that on the inverse side of the relation (OneToMany), with $usuario->getDirecciones();
i'm working in with this code in a non MVC architecture, to this point everything works like charm in getting creating and updating through a DAO wich uses the Doctrine sentences,so i know that in the ending points (updating, creating,persist and flush,retrieving data with find, findBy,etc), everything works fine.
when i try to fill the ArrayCollection calling from the Service layer where i use both classes (Usuario-InverseSide and Direccion-OwnerSide), nothing happens, the $direcciones arrayCollection property on Usuario instance, doesn't fetch anything, even when i tried establishing fetch="EAGER"on the annotations.
for the rest of the data or properties, everything works fine, all the other attributes of the user get filled just fine.
For some reason, the annotations are not being considered, donĀ“t know why.
i've spend a few days trying to figure out how to acomplish this way of accessing the associated data, didn't wanted to use DQL, but at this point i think i will take that road.
I tried something hard-coded to test and, the result is the same, $direcciones doesn't get his data.Obvious are already discarted, that exact ID for a Usuario(user) has his related direcciones(address) setted.
$usuario = $this->usuarioDAO->find(20);
$direcciones = $usuario->getDirecciones();
var_dump($direcciones);
return $usuario;
the relation was only defined with a FK called id_usuario on Direccion on a mysql InnoDb table.
don't know if i should be setting something else on the Usuario table.
or if there is something wrong in the way i store the entities on my project.
please help me, any recommendation will be appreciated.
doctrine 2, hwo do get data from the inverse side (many to one)
Provided that your bidirectional mapping between Direccion and Usuaro is correct, you need to use setter methods on both sides.
And the other thing, when using ArrayCollection, an easier (perhaps the only way) is to add elements one by one.
Therefore, instead of setDirecciones() you add this method to Usuario class:
public function addDireccion(Direccion $direccion) {
$direccion->setIdUsuario($this);
$this->direcciones->add($direccion);
}
Hope that helps.
And, it would be better to name it $usuario and setUsuario instead od setIdUsuario because you work with objects and PHP should not be concerned about actual database field names (to put it that way).
i resolved it.
happens that for some reason, the php annotations were not being actually readed, or on a different way to say it, it was listening to the XML annotations first, so i put the xml sentence on the inverse side of the relation (usuario) inside the tags
<one-to-many field="direcciones" target-entity="Direccion" mapped-by="idUsuario" />
and it worked, now i got polish the data result of the consult so i can send a cleaner/clearer object to the front-end.

Doctrine - Auto insert rows association table

I have 3 tables : Profile - Permission - ProfilePermissionValue
Profile and Permission are classic entities, and ProfilePermissionValue is an association of a Profile.id, Permission.id, and an extra field representing the value of the permission for the profile.
When I add a Permission, I want a new row being inserted in ProfilePermissionValue for each Profile.
Same on reverse, when I add a new Profile, ... And same on delete by the way.
The question : Is there a way to do it with Doctrine (Symfony 3) functionalities, or I need to code it myself ?
I think you look at the permission <-> profile more strictly than you should. Basically in almost every ACL I worked with there was a assumption - when something is not allowed, it`s disallowed (or when something is not disallowed is allowed which is more dangerous). Which significantly reduce amount of data, you must save.
So when you create your entities like this
<?php
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity()
*/
class Permission
{
// id column
/**
* #ORM\Column(type="string")
* #var string
*/
private $name;
/**
* #return string
*/
public function getName()
{
return $this->name;
}
}
and
<?php
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity()
*/
class User
{
// id column
// name column
/**
* #ORM\ManyToMany(targetEntity=Permission::class)
* #ORM\JoinTable(name="allowed_permissions",
* joinColumns={#ORM\JoinColumn(name="user_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="permission_id", referencedColumnName="id")}
* )
* #var Permission[]|Collection
*/
private $allowedPermissions;
/**
* #return Permission[]
*/
public function getAllowedPermissions()
{
return $this->allowedPermissions->toArray();
}
}
you can simply implement your own class for interface AuthorizationCheckerInterface as
<?php
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
class Authorizator implements AuthorizationCheckerInterface
{
/**
* #param string $name
* #param User $user
* #return bool
*/
public function isGranted($name, $user)
{
foreach ($user->getAllowedPermissions() as $permission) {
if ($permission->getName() === $name) {
return TRUE;
}
}
return FALSE;
}
}
without any needs of having deny permission in your database.

How to Filter Symfony Form Entity Output depending on unrelated relationship

I have three entities like so:
-Available-Teams (Managed by Admins)
-Player-PreConfig (Managed by Admins)
-Player-Self] (Managed by User(Player itself))
Available-Teams:
--> All available teams
Player-PreConfig:
--> Here the Administrators are able to preselect teams in which a player is allowed to play. (First-Filter - Many2Many: Available-Teams<->Player-PreConfig) - Lots of checkboxes in the view.
Player-Self:
--> Here the Player should be able to select the teams (multiple) he would like to play in. But he should not get listed ALL possible Available-Teams, but only the remaining ones.
Classes
/**
* TeamsPlayerBundle\Entity\Teams
*
* #ORM\Table(name="team")
* #ORM\Entity
*/
class Team
{
/**
* #var integer $id
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string $name
*
* #ORM\Column(name="name", type="string", length=255)
*/
private $name;
/**
* #ORM\ManyToMany(targetEntity="PreConfig", mappedBy="teams", cascade={"persist", "remove"})
**/
private $configs;
/**
* #ORM\ManyToMany(targetEntity="Player", mappedBy="teams2show", cascade={"persist"})
**/
private $players;
public function __construct()
{
$this->configs = new ArrayCollection();
$this->players = new ArrayCollection();
}
(... setters and getters)
###################################################
/**
* TeamsPlayerBundle\Entity\PreConfig
*
* #ORM\Table(name="preconfig")
* #ORM\Entity
*/
class PreConfig
{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\ManyToMany(targetEntity="Teams", inversedBy="configs", cascade={"persist", "remove"})
* #ORM\JoinTable(name="preconfig_teams)
**/
private $teams;
public function __construct()
{
$this->teams = new ArrayCollection();
}
(... setters and getters)
####################################################
/**
* TeamsPlayerBundle\Entity\Player
*
*
* #ORM\Table(name="player")
* #ORM\Entity
*/
class Player
{
/**
* #var integer $player_id
*
* #ORM\Column(name="player_id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $player_id;
/**
* #var string $name
* #Assert\NotBlank
*
* #ORM\Column(name="name", type="string", length=64)
*/
private $name
/**
* #ORM\ManyToMany(targetEntity="Team", inversedBy="player", cascade={"persist"})
* #ORM\JoinTable(name="player_team",
* joinColumns={#ORM\JoinColumn(name="player_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="id", referencedColumnName="id")}
* )
**/
private $teams2show;
public function __construct()
{
$this->teams2show = new ArrayCollection();
}
(... setters and getters)
Right now I have this FormType: I try to solve with Query_Builder as suggested by "Viktor77"
namespace TeamsPlayerBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
use Doctrine\ORM\EntityRepository;
use TeamsPlayerBundle\Entity\Player;
class Teams2ShowType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('teams2show', 'entity', array(
'class' => 'TeamsPlayerBundle\Entity\PreConfig',
'query_builder' => function(EntityRepository $er) use ($cid) {
return $er->createQueryBuilder('c')
->add('orderBy', 'c.name ASC')
->innerJoin('c.teams', 'c2')
->where('c2.id = :configId')
->setParameter('configId', $cid);
},
'expanded' => true,
'multiple' => true,
'property_path' => 'teams2show',
'property' => 'name'
))
;
...<br />
For Your Reference: => My first Form looked like this:
class Teams2ShowType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('teams2show', 'entity', array(
'multiple' => true,
'expanded' => true,
The problem was as follows:
If I render the form right now everything works fine, but a huge list of checkboxes gets rendered. The whole entity is presented.
Sure because I have no idea, how to only populate the remaining entities depending on the many-to-many relationship Available-Teams<->Player-PreConfig).
Because obviously, my actual Teams2ShowType has no idea, that only the remaining teams should show up.
I have already tried a lot and read a lot (query_builder, model transformer, etc..), but I could not get it right.
My real example (in the company) has to do something with licensors and partner configuration, but I wanted to present this question in a more comprehensible scenario.
I do not know of any best practices on how to implement this right.
Thank you so much for your help in advance I have already tried to solve that issue more than 3-4days.
Kind regards,
query_builder option is the way to go. Just use Doctrine Query Builder API to get only the entities you need to be rendered in your form and not all of them

Doctrine2 schema tool update fail on many to many relationship

When I need to update my database structure, I do it in PHP using this kind of code :
$tool = new SchemaTool($em);
$classes = array(
$em->getClassMetadata('Entities\Entity1'),
$em->getClassMetadata('Entities\Entity2')
);
$tool->updateSchema($classes);
It works fine until a many to many relation is defined. Consider the following entities as an example :
/**
* Entities\CET\User
*
* #Entity
* #Table(name="tb_user")
*/
class User
{
/**
* #Id
* #Column(type="integer", options={"unsigned"=true})
* #GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ManyToMany(targetEntity="Group", inversedBy="users")
* #JoinTable(name="users_groups")
*/
private $groups;
public function __construct()
{
$this->groups = new ArrayCollection();
}
}
/**
* Entities\CET\Group
*
* #Entity
* #Table(name="tb_group")
*/
class Group
{
/**
* #Id
* #Column(type="integer", options={"unsigned"=true})
* #GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ManyToMany(targetEntity="User", mappedBy="groups")
*/
private $users;
public function __construct()
{
$this->users = new ArrayCollection();
}
}
If my database is empty (no tables) and I update the structure, it works fine. But if tables are already created and I simply want to update them, it breaks and give me the following error :
An exception occurred while executing 'ALTER TABLE tb_group CHANGE id
id INT UNSIGNED AUTO_INCREMENT NOT NULL': SQLSTATE[HY000]: General
error: 1833 Cannot change column 'id': used in a foreign key
constraint 'FK_FF8AB7E0FE54D947' of table 'example.users_groups'
I've tried to manually define join columns and set them to nullable, like this :
/**
* #ManyToMany(targetEntity="Group", inversedBy="users")
* #JoinTable(name="users_groups",
* joinColumns={#JoinColumn(name="user_id", referencedColumnName="id", nullable=true)},
* inverseJoinColumns={#JoinColumn(name="group_id", referencedColumnName="id", nullable=true)}
* )
*/
private $groups;
without much success. Even the simpliest way to define the relation fail to update using Doctrine SchemaTool.
My current workaround is to delete all tables and re-create them, but it's an horrible "solution" on so many levels...