Doctrine2, pass Id or Object? - php

I do not understad why with some Entity objects I can set the Id and for others objects I get an error and says me that the Id can't be null and I have to pass an object instead.
e.g.:
$log = new Log();
$log->setTypeId(1);
$log->setUserId(1);
$entityManager->persist($log);
$entityManager->flush();
If I try the code above I get error that says: Integrity constraint violation: 1048 Column 'user_id' cannot be null. And I have to first create the Type Object and de User object and the pass them:
$log->setType($TypeObject)
$log->setUser($UserObject)
But for other entity objects I have no problem assigning the value directly, why is that?
This is my Entity Log:
<?php
/**
* #Entity
* #Table(name="log")
* #HasLifecycleCallbacks
*/
class Log
{
/**
* #var type
* #Id
* #Column(type="integer")
* #GeneratedValue
*/
protected $id;
/**
*
* #var type
* #Column(type="integer")
*/
protected $user_id;
/**
*
* #var type
* #Column(type="integer")
*/
protected $type_id;
/**
*
* #var type
* #Column(type="datetime")
*/
protected $created;
/**
*
* #var type
* #ManyToOne(targetEntity="User", inversedBy="logs")
*/
protected $user;
/**
*
* #ManyToOne(targetEntity="Type", inversedBy="logs")
*/
protected $type;
public function getId()
{
return $this->id;
}
public function getUserId()
{
return $this->user_id;
}
public function getTypeId()
{
return $this->type_id;
}
public function getCreated()
{
return $this->created;
}
public function setUserId($userId)
{
$this->user_id = $userId;
}
public function setTypeId($typeId)
{
$this->type_id = $typeId;
}
public function setCreated($created)
{
$this->created = $created;
}
public function setUser($user)
{
$this->user = $user;
}
public function setType($type)
{
$this->type = $type;
}
/**
* #PrePersist
*/
public function prePersist()
{
$this->setCreated(new DateTime());
}
}
?>

The existing answer never did sit well with me. There are many valid scenarios where loading an object just to define the relationship while already having the FK handy just does not make any sense at all.
A better solution is to use Doctrine's EntityManager's getRefrence method.
Reference Proxies...
The method EntityManager#getReference($entityName, $identifier) lets
you obtain a reference to an entity for which the identifier is known,
without loading that entity from the database. This is useful, for
example, as a performance enhancement, when you want to establish an
association to an entity for which you have the identifier. You could
simply do this:
<?php
// $em instanceof EntityManager, $cart instanceof MyProject\Model\Cart
// $itemId comes from somewhere, probably a request parameter
$item = $em->getReference(\MyProject\Model\Item::class, $itemId);
$cart->addItem($item);
Maybe this was not available when this question was first posted - I don't know.

EDIT
I found this statement on the website of Doctrine2. It's a best practice that you might want to follow when coding your models.
Doctrine2 Best Practices
25.9. Don’t map foreign keys to fields in an entity
Foreign keys have no meaning whatsoever in an object model. Foreign keys are how a relational database establishes relationships. Your object model establishes relationships through object references. Thus mapping foreign keys to object fields heavily leaks details of the relational model into the object model, something you really should not do
EDIT
Doctrine does the mapping from your objects to their respective Ids.
What you've done here is a bit redundant.
You've essentially told doctrine the same thing twice.
You've told it that it has a 'user_id' column AND that it also has a User object, which are the same thing. But doctrine can already guess that this relationship will have a user_id column based on the fact that the log class has a user object inside.
You should simply do the following instead
<?php
/**
* #Entity
* #Table(name="log")
* #HasLifecycleCallbacks
*/
class Log
{
/**
* #var type
* #Id
* #Column(type="integer")
* #GeneratedValue
*/
protected $id;
/**
*
* #var type
* #Column(type="datetime")
*/
protected $created;
/**
*
* #var type
* #ManyToOne(targetEntity="User", inversedBy="logs")
*/
protected $user;
/**
*
* #ManyToOne(targetEntity="Type", inversedBy="logs")
*/
protected $type;
public function getId()
{
return $this->id;
}
public function getCreated()
{
return $this->created;
}
public function setCreated($created)
{
$this->created = $created;
}
public function setUser($user)
{
$this->user = $user;
}
public function setType($type)
{
$this->type = $type;
}
/**
* #PrePersist
*/
public function prePersist()
{
$this->setCreated(new DateTime());
}
}
Doctrine will worry about the user_id and type_id on it's own. You don't have to worry about it. This way you get to work with full fledged objects, making it easier to program, instead of having to worry about id's. Doctrine will handle that.
If ALL you have is an id, because that's what you're using on the front end, then just fetch the object associated with that id using the Entitymanager.
$user = $em->getEntity( 'User', $idFromWeb );
$log = new Log();
$log->setUser( $user );

Related

Symfony Doctrine2 manyToMany relationship not removed - Specific to SQLite

I have several classes using a Taggable trait to set up a tag system common to several doctrine entities (Project, Note, ...).
The relationship between these entities and these tags is a ManyToMany relationship that I can not make multi-directional.
My problem: When I delete a Project entity, it is removed from the project table, but the relationships in the project_tag table between this project and the tags are not deleted. Then, if I create a new Project entity, an exception is thrown.
An exception exists while executing 'INSERT INTO project_tag (project_id, tag_id) VALUES (?,?)' With params [2, 4]:
SQLSTATE [23000]: Integrity constraint violation: 19 UNIQUE constraint failed: project_tag.project_id, project_tag.tag_id
Entities :
Tag
/**
* Tag
*
* #ORM\Table(name="tag")
* #ORM\Entity(repositoryClass="AppBundle\Repository\TagRepository")
*/
class Tag
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=255, unique=true)
*/
private $name;
/**
* #ORM\Column(name="last_use_at", type="datetime", nullable=false)
* #var \DateTime
*/
private $lastUseAt;
public function __construct()
{
$this->lastUseAt = new \DateTime();
}
public function __toString()
{
return $this->name;
}
/**
* Get id
*
* #return int
*/
public function getId()
{
return $this->id;
}
/**
* Set name
*
* #param string $name
*
* #return Tag
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name
*
* #return string
*/
public function getName(): string
{
return $this->name;
}
/**
* #return \DateTime
*/
public function getLastUseAt(): \DateTime
{
return $this->lastUseAt;
}
/**
* #param \DateTime $lastUseAt
*/
public function setLastUseAt(\DateTime $lastUseAt)
{
$this->lastUseAt = $lastUseAt;
}
}
Taggable
trait Taggable
{
/**
* #var ArrayCollection
*
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\Tag", cascade={"persist"})
*/
protected $tags;
/**
* Add tag
*
* #param Tag $tag
*
* #return $this
*/
public function addTag(Tag $tag)
{
$tag->setLastUseAt(new \DateTime());
$this->tags[] = $tag;
return $this;
}
/**
* Remove tag
*
* #param Tag $tag
*/
public function removeTag(Tag $tag)
{
$this->tags->removeElement($tag);
}
/**
* Get tags
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getTags()
{
return $this->tags;
}
}
Project
/**
* Project
*
* #ORM\Table(name="project")
* #ORM\Entity(repositoryClass="AppBundle\Repository\ProjectRepository")
*/
class Project
{
use Taggable;
}
Note
class Note
{
use Taggable;
}
Is this the only solution or is my annotation incomplete / incorrect?
I tried with JoinColumns, JoinTable and onDelete = "cascade" but nothing works.
In the meantime, I dodged the problem with this instruction placed before the suppresion.
$project->getTags()->clear();
Full code of the action in the controller :
/**
* #Route("/project/{id}/delete", name="project_delete")
*/
public function deleteAction($id) {
$em = $this->getDoctrine()->getManager();
$project = $em->getRepository('AppBundle:Project')->find($id);
if(!$project) {
return $this->redirectToRoute('index');
}
$project->getTags()->clear();
$em->remove($project);
$em->flush();
return $this->redirectToRoute('index');
}
I think I found a better solution: you can set the PRAGMA within Doctrine configuration. Like:
doctrine:
dbal:
# configure these for your database server
driver: 'pdo_sqlite'
#server_version: '5.7'
#charset: utf8mb4
#default_table_options:
#charset: utf8mb4
#collate: utf8mb4_unicode_ci
url: '%env(resolve:DATABASE_URL)%'
options:
'PRAGMA foreign_keys': 'ON'
I just tried it on my Symfony 4 application, re-created the database and tested using DB Browser for SQLite and it works as I expected.
Hope this helps
I managed to fix the problem. Here's my solution working for SQLite conections.
Create an eventListener listening on the kernel.request event :
namespace AppBundle\EventListener;
use Doctrine\Bundle\DoctrineBundle\Registry;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
class RequestListener
{
/**
* #var Registry
*/
private $doctrine;
public function __construct(Registry $doctrine)
{
$this->doctrine = $doctrine;
}
public function onKernelRequest(GetResponseEvent $event)
{
$this->doctrine->getConnection()->exec('PRAGMA foreign_keys = ON');
}
}
Service declaration
app.event_listener.request_listener:
class: AppBundle\EventListener\RequestListener
arguments:
- '#doctrine'
tags:
- { name: kernel.event_listener, event: kernel.request }
I think the problem is that you have your trait Taggable set as the owning side of the ManyToMany relationship but your are deleting the inverse side and expecting something to happen as a result. Doctrine will only check the owning side of the relationship in order to persist any changes. See here for docs on this.
You can solve by making the Taggable the inverse side of each of your relationships, or by manually telling doctrine to delete the owning side.
The first solution will probably not work for you since you won't (easily) specify multiple inverse sides. (Are you sure a trait is the right way to go for this??)
The second solution is easy. In your entities like Project for your deleteTag($tag) function, call a delete function on the owning side (e.g., deleteProject($project). You will have to create if one does not exist.
class Project
{
use Taggable;
public function deleteTag($tag)
{
$this->tags->removeElement($tag);
// persist on the owning side
$tag->deleteProject($this);
}
}
EDIT:
After seeing full code, it looks like you are deleting correctly. Now you need to tell doctrine to carry that through. See this post for full details, but basically you can change your trait to this:
trait Taggable
{
/**
* #var ArrayCollection
*
* #ORM\ManyToMany(
* targetEntity="AppBundle\Entity\Tag",
* cascade={"persist"},
* onDelete="CASCADE"
* )
*/
protected $tags;
// ...
}

Doctrine trying to persist Owner

I've setup Doctrine and Symfony-forms independent of the Symfony Framework (as I don't need most of it).
The issue I'm having is, when trying to persist a new "Audit" which has an "Type" doctrine seems to want to persist the owning side of the relationship (Type).
For example as Audit may have a type of Vehicle Service.
// -- Model/Audit.php --
/**
* #var \Model\Type
*
* #ORM\ManyToOne(targetEntity="Model\Audit\Type", inversedBy="audits")
* #ORM\JoinColumn(name="type_id", referencedColumnName="id", nullable=true)
*/
private $type;
/**
* Set type
*
* #param \Model\Type $type
* #return Audit
*/
public function setType(\Model\Type $type)
{
$this->type = $type;
return $this;
}
And then in the inverse side:
/**
* #ORM\OneToMany(targetEntity="Model\Audit", mappedBy="type")
* #var type */
private $audits;
public function __construct() {
$this->audits = new \Doctrine\Common\Collections\ArrayCollection();
}
Persistance code looks as follows:
$data = $form->getData();
$entityManager->persist($data);
$entityManager->flush();
And finally the form class is:
class AuditType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('name')
->add('type', 'entity', array(
'class' => "Model\Type"
));
}
All looks (to me at least) exactly the same as in all the documentations both Doctrine and Symfony sides but I'm getting this error:
A new entity was found through the relationship 'Model\Audit#type'
that was not configured to cascade persist operations for entity:
Vehicle Service. To solve this issue: Either explicitly call
EntityManager#persist() on this unknown entity or configure cascade
persist this association in the mapping for example
#ManyToOne(..,cascade={"persist"})."
Which is really frustrating as I don't want to persist the Type side, I just want to put (in most basic terms) the id of 3 into the type_id column. Yet Doctrine seems to think I want to create a new "Type" which I certainly do not. They already exist.
Using $entityManager->merge($audit); works in part, it allows the inital Audit and its FK's to be saved. However it caused any embedded forms to become ignored.
I think you need set
/**
* #ORM\OneToMany(targetEntity="Model\Audit", mappedBy="type")
* #var type
*/
private $audits;
public function __construct() {
$this->audits = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* #return ArrayCollection
*/
public function getAudits()
{
return $this->audits;
}
/**
* #param Audit $audit
*/
public function addAudits(Audit $audit)
{
$this->audits->add($audit);
$audit->setTyoe($this);
}
and in Type Audit.model
// -- Model/Audit.php --
/**
* #var \Model\Type
*
* #ORM\ManyToOne(targetEntity="Model\Audit\Type", inversedBy="audits")
* #ORM\JoinColumn(name="type_id", referencedColumnName="id", nullable=true)
*/
private $type;
/**
* Set type
*
* #param \Model\Type $type
* #return Audit
*/
public function setType(\Model\Type $type)
{
$this->type = $type;
}

Doctrine2 bulk import try to work with another entity

I'm working on a members import batch (with insertions and updates) for a big project with a lot of entities such as Member, Client, Group, ....
After reading the chapter related to bulk imports in Doctrine doc, I've implemented this code :
$batchSize = 20;
$i = 0;
foreach ($entities as $entity)
{
$this->getEntityManager()->persist($entity);
if (($i % $batchSize) === 0)
{
$this->getEntityManager()->flush();
$this->getEntityManager()->clear();
}
}
$this->getEntityManager()->flush();
$this->getEntityManager()->clear();
Now, when I want to bulk handle an array of Member entities, Doctrine try to insert null data into a completely other table related to the Group entity and an exception is thrown An exception occurred while executing 'INSERT INTO groups ...
There are not any relations between Member and Group ...
Any idea about this weird behavior ?
EDIT
Short mapping details :
/**
* #ORM\Entity
* #ORM\Table(name="members")
*/
class Member
{
// some properties ...
/**
* #ORM\ManyToOne(targetEntity="Client", inversedBy="members", cascade={"persist", "merge"})
* #ORM\JoinColumn(name="client_id", referencedColumnName="id", onDelete="CASCADE")
*/
protected $client;
/**
* #return Client
*/
public function getClient()
{
return $this->client;
}
/**
* #param Client $client
*
* #return $this
*/
public function setClient(Client $client)
{
$this->client = $client;
return $this;
}
}
/**
* #ORM\Entity
* #ORM\Table(name="clients")
*/
class Client
{
/**
* #ORM\OneToMany(targetEntity="Member", mappedBy="client", cascade={"persist", "remove", "merge"}, fetch="EXTRA_LAZY")
*/
protected $members;
/**
* #ORM\ManyToOne(targetEntity="Group", inversedBy="clients", cascade={"persist", "merge"})
* #ORM\JoinColumn(name="clients_id", referencedColumnName="id", onDelete="SET NULL")
*/
protected $group;
public function __construct()
{
$this->members = new ArrayCollection();
}
/**
* #return ArrayCollection
*/
public function getMembers()
{
return $this->members;
}
/**
* #param $members
*
* #return $this
*/
public function setMembers($members)
{
$this->members = new ArrayCollection();
return $this->addMembers($members);
}
/**
* #param $members
*
* #return $this
*/
public function addMembers($members)
{
foreach ($members as $member)
{
$this->addMember($member);
}
return $this;
}
/**
* #param Member $member
*
* #return $this
*/
public function addMember(Member $member)
{
$this->members->add($member);
$member->setClient($this);
return $this;
}
/**
* #param Member $member
*
* #return $this
*/
public function removeMember(Member $member)
{
if ($this->members->contains($member))
{
$this->members->removeElement($member);
}
return $this;
}
/**
* #param $members
*
* #return $this
*/
public function removeMembers($members)
{
foreach ($members as $member)
{
$this->removeMember($member);
}
return $this;
}
/**
* #param Group $group
*
* #return $this
*/
public function setGroup(Group $group = null)
{
$this->group = $group;
return $this;
}
/**
* #return Group
*/
public function getGroup()
{
return $this->group;
}
}
/**
* #ORM\Entity
* #ORM\Table(name="groups")
*/
class Group
{
/**
* #ORM\OneToMany(targetEntity="Client", mappedBy="group")
*/
protected $clients;
public function __construct()
{
$this->clients = new ArrayCollection();
}
/**
* #return ArrayCollection
*/
public function getClients()
{
return $this->clients;
}
/**
* #param $clients
*
* #return $this
*/
public function setClients($clients)
{
$this->clients = new ArrayCollection();
return $this->addClients($clients);
}
/**
* #param $clients
*
* #return $this
*/
public function addClients($clients)
{
foreach ($clients as $client)
{
$this->addClient($client);
}
return $this;
}
/**
* #param Client $client
*
* #return $this
*/
public function addClient(Client $client)
{
if (!$this->clients->contains($client))
{
$this->clients->add($client);
$client->setGroup($this);
}
return $this;
}
/**
* #param $clients
*
* #return $this
*/
public function removeClients($clients)
{
foreach ($clients as $client)
{
$this->removeClient($client);
}
return $this;
}
/**
* #param Client $client
*
* #return $this
*/
public function removeClient(Client $client)
{
if ($this->clients->contains($client))
{
$this->clients->removeElement($client);
$client->setGroup(null);
}
return $this;
}
}
And the error is type of :
An exception occurred while executing 'INSERT INTO groups ... SQLSTATE[23502]: Not null violation: 7 ERROR: null value in column "label" violates not-null constraint
DETAIL: Failing row contains (60, null, f, null, f, null, null).
EDIT2
This is the table creation description (using postgresql) :
CREATE TABLE groups (
id integer NOT NULL,
tempref character varying(255) DEFAULT NULL::character varying,
prorated_basis boolean NOT NULL,
fixed_price_amount double precision,
is_indexed boolean,
pricing_grid pricing[],
label character varying(255) NOT NULL
);
CREATE SEQUENCE groups
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE groups_id_seq OWNED BY groups.id;
ALTER TABLE ONLY pricing_groups ALTER COLUMN id SET DEFAULT nextval('groups_id_seq'::regclass);
ALTER TABLE ONLY groups
ADD CONSTRAINT groups_pkey PRIMARY KEY (id);
I can describe what is causing the error, but only guess why it is caused and give some hints on what to look for when debuging this.
As you described, you are updating members, that are part of a client, that in turn is part of a group. As you specified on the relations by cascade=persist, clients and groups are saved as well when persisting a member. That means, groups are either updated or created when inserting members. In your case, you are creating a new group by this mechanism. Yet this group does not have the label property set, resulting in a NULL value in the database, which is not allowed by the scheme.
As you said, this error is already occuring during the best batch. One of the first 20 members you update implicity creates a new group with no label. To find out which one it is I'd suggest using a debugger and inspecet each member before persistence to see what the group of this member is part of, and if it exists in the database. If it does not exist (by ID), you should investigate why this group does not the required label set.
If all groups actually do exist in the database already, things do get a bit more tricky and this depends on how the members you are updating are loaded. Are they fetched from the EntityManager (managed state) or are they loaded from some different source (e.g. serialized) and hence in a unmanaged state? If they are unmanaged, they will become manage upon peristence, and by specification of the relation cascade=merge, client, and group, will become managed as well. There is an important thing to know here though, merge will return a new (managed) entity which is then persisted (see the accepted answer here). As this is a new object, there might be the chance that this object is not fully initialized and can contain undefined values (which then would translate to NULL).
So when loading the member data from a different source than the EntityManager, you might have to connect them with the EntityManager first to avoid this problem.
Debugging the last one is quite difficult and you'd need to step into the UnitOfWork->doPersist method to see how each individual entity is actual persisted.

Are Doctrine-generated association properties always of type ArrayCollection?

I am currently trying to filter a Doctrine-generated association property by using Criteria as described in the Doctrine manual: Filtering Collections
Here is some sample code: A person who owns several cars. For the sake of simplicity we save the date when the person bought and sold the car in the car entity instead of creating an association entity.
class Car
{
/**
* #ORM\ManyToOne(targetEntity="Person", inversedBy="cars")
* #ORM\JoinColumn(name="owner_id", referencedColumnName="id", nullable=false)
*/
private $owner;
/**
* #ORM\Column(name="bought", type="date")
*/
private $bought;
/**
* #ORM\Column(name="sold", type="date")
*/
private $sold;
}
class Person
{
/**
* #ORM\OneToMany(targetEntity="Car", mappedBy="owner")
*/
private $cars;
public function __construct
{
$this->cars = new ArrayCollection();
}
/**
* Get cars
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getCars()
{
return $this->cars;
}
public function getCarsOwnedAtDate(\DateTime $date)
{
$allCars = $this->getCars();
$criteria = Criteria::create()->where(
Criteria::expr()->andX(
Criteria::expr()->lte("bought", $date),
Criteria::expr()->gte("sold", $date)
)
);
return $allCars->matching($criteria);
}
}
The documentation for getCars says that it returns a Collection. Note that this documentation is auto-generated by Doctrine.
So when I call matching on Collection in my getCarsOwnedAtDate method my IDE issues a warning that Collection does not have a matching method. Turns out, it is right.
The object returned by getCars is an ArrayCollection and has a matching method so the code runs fine.
My question is: Can I rely on the fact that methods auto-generated by Doctrine will always return an ArrayCollection? Or will the above code fail in a certain case? If it does always return ArrayCollection, then why does the PhpDoc comment not mention this class explicitly?

Doctrine 2 Collection not connected or filled

I have a doctrine setup where i cant use the many side of collections.
The objects used:
User.php:
class User extends \App\Entity
{
/**
* #Id #Column(type="integer")
* #GeneratedValue
*/
private $id;
/**
* #ManyToOne(targetEntity="Company", inversedBy="users")
*/
private $company;
public function getCompany()
{
return $this->company;
}
public function setCompany($company)
{
$this->company = $company;
}
}
Company.php:
class Company extends \App\Entity
{
/**
* #Id #Column(type="integer", nullable=false)
* #GeneratedValue
*/
private $id;
/**
* #OneToMany(targetEntity="User", mappedBy="company")
*/
private $users;
public function __construct()
{
$this->users = new \Doctrine\Common\Collections\ArrayCollection();
}
public function getUsers()
{
return $this->users;
}
}
When i create the relation seems to be working. Get no errors. Then i tried the following:
$company = $this->em->getRepository('App\Entity\Company')->findOneByName("Walmart");
$user = $this->em->getRepository('App\Entity\User')->findOneByName("Niek");
$user->setCompany($company);
$company2 = $this->em->getRepository('App\Entity\Company')->findOneByName("Ford");
$user2 = $this->em->getRepository('App\Entity\User')->findOneByName("Henk");
$company2->getUsers()->add($user2);
$this->em->flush();
When i inspect the database for the first user the company is set. Relation is there. The seconds does not persists. and when i do this:
print_r(\Doctrine\Common\Util\Debug::dump($company->getUsers(),$doctrineDepth));
print_r(\Doctrine\Common\Util\Debug::dump($company->getUsers2(),$doctrineDepth));
i get 2 empty arrays.
So it seems that the array isnt connected. It only behaves like this on OneToMany ore ManyToOne relationships. Got one ManyToMany and that one works perfect in the same project
Any ideas?
You still need to set both sides of the relationship with a one-to-many or a many-to-one relationship:
$company2 = $this->em->getRepository('App\Entity\Company')->findOneByName("Ford");
$user2 = $this->em->getRepository('App\Entity\User')->findOneByName("Henk");
$company2->getUsers()->add($user2);
$user2->setCompany($company2);
$this->em->flush();
The reason your many-to-many works is because you don't need to set both sides of the relationship.

Categories