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...
Related
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
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;
The title may not be so clear so I'll explain in detail here (I must miss something obvious but I can't figure out what).
I'm using the Vich uploader bundle to store pictures in my project. I have two entity linked with an unidirectional one to one relation, the first is the owner and contain the annotation pointing to the second entity containing the file.
This is the code part from the first entity :
...
/**
* #ORM\OneToOne(targetEntity="Cartong\MyBundle\Entity\Mysql\EntityContainingTheFile")
* #ORM\JoinColumn(name="photo_id", referencedColumnName="id")
*/
private $photo;
...
And the one containing the file :
/**
* #ORM\Entity
* #ORM\HasLifecycleCallbacks()
* #Vich\Uploadable
*/
class EntityContainingTheFile extends FileUpload
{
/**
* #var UploadedFile
* #Vich\UploadableField(mapping="my_pictures", fileNameProperty="filename")
*/
protected $file;
/**
* #return UploadedFile
*/
public function getFile()
{
return parent::getFile();
}
/**
* #param UploadedFile $file
*/
public function setFile(File $file)
{
return parent::setFile($file);
}
}
The FileUpload code is here too. It basically containing the file description (I'm using other entity that are extending this class) :
/**
* #ORM\MappedSuperclass
* #Vich\Uploadable
*/
class FileUpload
{
/**
* #var integer : stock the unique id of the file
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string : stock the format of the file
*
* #ORM\Column(name="format", type="string", length=255)
*/
private $format;
/**
* var string : stock the original name of the file
*
* #ORM\Column(name="alt", type="string", length=255)
*/
private $alt;
/**
* #var integer : stock the size of the file (ko)
*
* #ORM\Column(name="size", type="integer")
*/
private $size;
/**
* #var \DateTime
*
* #ORM\Column(name="updated_at", type="datetime", nullable=true)
*/
private $updatedAt;
/**
* #var string $filename
*
* #ORM\Column(name="filename", type="string", length=255)
*/
protected $filename;
protected $file;
The file upload is working well, everything is stored at the right place in my project and the DB. The problem occur when I try to retrieve what I just store trough the first entity.
This is the kind of code I have in my controller :
$repo = $this->container->get('doctrine')->getRepository('CartongMSFBundle:MyFirstEntity');
$test = $repo->find($theEntityWithAFile);
The object returned containing all the expected information except the photo, where all the fields are null.
So if I'm trying to get the specific file trough a findById in the "file" repo it's working but when I'm trying to get it trough my first entity it's not.
Any idea ? (maybe a mistake in the annotations ?)
It seems like a typical doctrine hydration issue. In case of associations, doctrine by default doesn't load from database associated entities, until it is needed (e.g. you call $myFirstEntity->getPhoto()->getFormat()). This is called lazy loading.
If you want your associated entity to be loaded along with your first entity you should set doctrine fetch option to EAGER:
/**
* #ORM\OneToOne(targetEntity="EntityContainingTheFile", fetch="EAGER")
* #ORM\JoinColumn(name="photo_id", referencedColumnName="id")
*/
private $photo;
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.
I have an API developed in Symfony2 but when i send a request to it the response returns with 204Mb for only 40 rows... This is the code:
$em = $this->getDoctrine()->getManager();
$themes = $em->getRepository("KlickpagesAdminBundle:Theme")->findAll();
return $themes;
Im use FOSRestBundle to serialize and returns the json.
How i can resolve this?
Aa #Cerad said it is very like because of relations to other entities and lazy loading going in circles
For a quick test exclude all fields from the serilazition, except few scalar ones like so:
use JMS\Serializer\Annotation\Expose;
use JMS\Serializer\Annotation\ExclusionPolicy;
/**
* Group
*
* #ExclusionPolicy("all")
*/
class Group implements GroupInterface
{
/**
* #Expose
* #var integer
*/
private $id;
/**
* #Expose
* #var string
*/
private $title;
/**
* Relation to privilegesis not explicitly exposed.
* #var Privilege[]
*/
private $privileges;
/**
* Relation to Users not explicitly exposed.
* #var User[]
*/
private $users;
...
The important parts are exclusionStrategy and expose antations.
If this will help, you got for sure a circles serialization of your annotations and the right solution is to define serialization groups, lets say like this:
/**
* #Expose
* #Groups({"groupDetail", "userAuthenticate"})
*
* #var Privilege[]
*/
private $privileges;
/**
* #Expose
* #Groups({"groupDetail"})
*
* #var User[]|ArrayCollection
*/
private $users;
You can then define by which group should be the response serialized on your controller or programatically.
// controllerAction
/*
* #Annotations\View(serializerGroups={"Default","groupDetail"})
*/
public function getGroupAction($groupId) { ... }
// programatically
...
/** #var $context SerializationContext */
$context = SerializationContext::create();
$serializationGroups = ['Default', 'GroupDetail'];
$context->setGroups($serializationGroups);
$view = $this->view($collection, 200);
$view->setSerializationContext($context);
return $this->handleView($view);
...
Resources: http://jmsyst.com/libs/serializer/master/cookbook/exclusion_strategies