the best way of update entity through a service in idempotent case? - php

suppose I have the entity:
class UserEntity
{
/**
* #var integer
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* #var string
* #ORM\Column(type="string")
*/
private $name;
/**
* #var string
* #ORM\Column(type="string")
*/
private $email;
// and contruct, getters and setters..
}
and in the respective service:
class UserService extends BaseService
{
public function update($id, $data)
{
try {
$user= $this->fetch($id);
if (! $user instanceof UserEntity) {
// throw respective exception;
}
$user->setName($data['name']);
$user->setEmail($data['email']);
$this->entityManager->flush($user);
return $user;
} catch (Exception $e) {
throw $e;
}
}
}
if there is a user such that:
{
id: 1,
name: jhon,
email: jhon#domain.com
}
and the data provided to the service is:
$id = 1;
$data = [
'name' => jhon,
'email => jhon#domain,com
]
then, what is the best way of avoid the innecesary query to database in these cases?
since it is unnecessary to invoke the flush method.
or Doctrine internally is responsible for not doing the query?

According to the current docs:
The flush operation applies to a managed entity with the following semantics:
The entity itself is synchronized to the database using a SQL UPDATE
statement, only if at least one persistent field has changed.
No SQL
updates are executed if the entity did not change.
When entities become managed (according to one of their ORMInvalidArgumentExceptions):
An entity is managed if its fetched from the database or registered as new through EntityManager#persist
So yeah, Doctrine will make the decision for you.

Related

Doctrine: How to create entity-related tables on demand? (if i want to keep one SQL schema source)

Is there a way to tell Doctrine the name of a number of entities and it creates their related tables (incl. foreign keys etc.)?
My scenario:
I want to have annotations at my Doctrine entities as the only source for my database schema. Which means, that for instance for tests, i don't want to maintain a copy of these information in a SQL file or something.
To be clear, i mean annotations in entity classes like the following:
<?php
namespace App\Entity;
/**
* #ORM\Entity(repositoryClass="App\Repository\UserRepository")
* #UniqueEntity(fields={"email"}, message="There is already an account with this email")
*
* #ORM\Table(
* uniqueConstraints={
* #ORM\UniqueConstraint(name="email", columns={"email"})
* }
* )
*/
class User
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=180, nullable=false)
*/
private $email;
// ...
}
What i would like to do:
In my tests i would like to create the table for, lets say User, like:
<?php
namespace App\Test;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
class SomeTestCase extends KernelTestCase
{
public function setUp()
{
// ...
$this->entityManager = $kernel->getContainer()
->get('doctrine')
->getManager();
}
public function test1()
{
// Is there a function available which has this functionality?
$this->entityManager->createTableForEntity('App\Entity\User'); // <---------
// ...
}
}
Is that possible? If not, even creating all tables at once is fine for me.
Is there another way to achieve it?
I use the following to create all the tables in my tests:
use Doctrine\ORM\Tools\SchemaTool;
$metadatas = $this->entityManager->getMetadataFactory()->getAllMetadata();
$schemaTool = new SchemaTool($this->entityManager);
$schemaTool->updateSchema($metadatas);
There is a method getMetadataFactory() on the MetadataFactory class so I guess the following should work as well if you want to create just one table.
$metadata = $this->entityManager->getMetadataFactory()->getMetadataFor('App\Entity\User');
$schemaTool = new SchemaTool($this->entityManager);
$schemaTool->updateSchema($metadata);

Symfony OneToMany updates instead of insert

I am a beginner in Symfony.
I have a strange problem in my form.
I have 2 entities : Proposal_Lsi and Lsi_Beams. One proposal can have multiple beams, but a beam can only have one proposal. I figured I should use a OneToMany/ManyToOne relation, and that my owning side is the beam one, and inverse side is proposal.
I followed the official guide at https://symfony.com/doc/3.1/form/form_collections.html about Form Collections.
Everything renders just fine, I can submit a new proposal with multiple beams, and all is correctly stored in the database.
The problem occurs whenever I try to add new beams to my proposal : the systems overwrites (update query) existing beams (starting by the first one in the database) instead of adding new ones (insert query).
What am I missing ?
Here's some of my code, if that can help.
Proposal Class:
class Proposal_lsi{
/**
* #ORM\OneToOne(targetEntity="Emir2Bundle\Entity\Proposal", inversedBy="proposal_lsi")
* #ORM\JoinColumn(name="proposal", referencedColumnName="id")
* #ORM\Id
*/
private $proposal;
/**
* #ORM\OneToMany(targetEntity="Emir2Bundle\Entity\Lsi_beams", mappedBy="proposal_lsi")
*/
private $lsi_beams;
...
/**
* Add lsiBeam
*
* #param \Emir2Bundle\Entity\Lsi_beams $lsiBeam
* #return Proposal_lsi
*/
public function addLsiBeam(\Emir2Bundle\Entity\Lsi_beams $lsiBeam)
{
$lsiBeam->setProposalLsi($this);
$this->lsi_beams[] = $lsiBeam;
return $this;
}
}
Beams Class:
class Lsi_beams{
/**
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="Emir2Bundle\Entity\Proposal_lsi", inversedBy="lsi_beams", cascade={"persist"})
* #ORM\JoinColumn(name="proposal_lsi", referencedColumnName="proposal", nullable=false)
*/
private $proposal_lsi;
...
}
And the form in the controller :
$form = $this->createFormBuilder($proposallsi)
->setAction($this->generateUrl('lsi_submission', array('id' => $id)))
->setMethod('POST')
->add('lsi_beams', CollectionType::class, array(
'entry_type' => LsiBeamsType::class,
'allow_add' => true,
'allow_delete' => true,
'prototype' => true,
'by_reference' => false
)
)
...
What am I doing wrong ? Let me know if you need more code.
Thanks for any reply !
Notes:
use Doctrine ArrayCollection to better keep track of collections
put cascade={"persist"} at the inverse side of association (where you have mappedBy)
Keep entity names singular (e.g. Lsi_beam instead of Lsi_beams)
Keep your naming strategy clear and strait. Don't use undescores in your class & property names (e.g. use $lsiBeams instead of $lsi_beams)
ProposalLsi
use Doctrine\Common\Collections\ArrayCollection;
class ProposalLsi
{
/**
* #ORM\OneToMany(targetEntity="LsiBeam", mappedBy="proposalLsi", cascade={"persist"})
*/
private $lsiBeams;
public function __construct()
{
$this->lsiBeams = new ArrayCollection();
}
public function addLsiBeam(LsiBeams $lsiBeam)
{
if ($this->lsiBeams->contains($lsiBeam)) {
return;
} else {
$lsiBeam->setProposalLsi($this);
$this->lsiBeams->add($lsiBeam);
}
return $this;
}
public function removeLsiBeam(LsiBeams $lsiBeam)
{
if (!$this->lsiBeams->contains($lsiBeam)) {
return;
} else {
$lsiBeam->setProposalLsi(null);
$this->lsiBeams->removeElement($lsiBeam);
}
return $this;
}
}
LsiBeam
class LsiBeam
{
/**
* #ORM\ManyToOne(targetEntity="ProposalLsi", inversedBy="lsiBeams")
*/
private $proposalLsi;
public function setProposalLsi(?ProposalLsi $proposalLsi)
{
$this->proposalLsi = $proposalLsi;
}
}

What is the best way to set/update association lookup field inside Doctrine entity

I have the main entity
/**
* #ORM\Entity()
*/
class Document
{
/**
* #var int
* #ORM\Id()
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var DocumentStatus
* #ORM\ManyToOne(targetEntity="DocumentStatus")
*/
private $status;
/**
* #var string
* #ORM\Column(type="text")
*/
private $text;
}
and the lookup "enum" entity (seeding on application deploy)
/**
* #ORM\Entity(repositoryClass="DocumentStatusRepository");
*/
class DocumentStatus
{
const DRAFT = 'draft';
const PENDING = 'pending';
const APPROVED = 'approved';
const DECLINED = 'declined';
/**
* #var int Surrogate primary key
* #ORM\Id()
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string Natural primary key (name for developers)
* #ORM\Column(type="string", unique=true)
*/
private $key;
/**
* #var string Short name for users
* #ORM\Column(type="string", unique=true)
*/
private $name;
/**
* #var string Full decription for users
* #ORM\Column(type="string", nullable=true, unique=true)
*/
private $description;
}
with simple repository
class DocumentStatusRepository extends EntityRepository
{
public function findOneByKey($key)
{
return parent::findOneBy(['key' => $key]);
}
}
I want to encapsulate domain logic of document lifecycle by intoducing methods like
public function __construct($text)
{
$this->text = $text;
$this->status = $something->getByKey(DocumentStatus::DRAFT);
}
public function approve()
{
try {
$this->doSomeDomainActions();
$this->status = $something->getByKey(DocumentSatus::DRAFT);
} catch (SomeDomainException($e)) {
throw new DocumentApproveException($e);
}
}
...
or
public function __construct($text)
{
$this->text = $text;
$this->status = $something->getDraftDocumentStatus()
}
public function approve()
{
$this->status = $something->getApprovedDocumentStatus()
}
...
without public setters. Also I want keep Document loose coupling and testable.
I see the next ways:
Permanent inject DocumentStatusRepository (or service that encapsulates it) into every instance by constructor on entity creation and by using public setter in postLoad subscriber
Permanent inject DocumentStatusRepository by static Document method on application bootstrap or on loadClassMetadata
Temporary inject DocumentStatusRepository in constructor and methods like Document::approve
Use setStatus() method with complex logic based on $status->key value
Encapsulate document domain logic in some DocumentManager and use Document entity like simple data storgae or DTO :(
Are there other ways? Which way is easier and more convenient to use in the long term?
Using generated Identities for Document.
Now you generate the identity on the side of the database. So you save Document from the domain perspective in inconsistent state. Entity/Aggregate should be identified, if it has no id it shouldn't exists.
If you really want to keep to database serials, add method to the repository which will generate id for you.
Better way is to use uuid generator for example ramsey/uuid.
And inject the id to the constructor.
DocumentStatus as Value Object
Why Document Status is Entity? It does look like a simple Value Object.
Then you can use Embeddable annotation. So it will leave within same table in the database, no need for doing inner joins.
DocumentStatus gets behaviour for example ->draftDocumentStatus(), which returns NEW DocumentStatus with draft status, so you can switch the old instance one with new one. ORM will do the rest.
DocumentStatusRepository
If you really want to keep DocumentStatus as entity, which in my opinion is wrong you shouldn't have DocumentStatusRepository.
Document is your aggregate root and the only entrance to the DocumentStatus, should be by aggregate root.
So you will have DocumentRepository only, which will be responsible for rebuilding the whole aggregate and saving it.
Also you should change the mapping.
It should have FETCH=EAGER type, so it will retrieve DocumentStatus with Document together.
Secondly you should do mapping with CASCADE=ALL and ORPHANREMOVAL=TRUE.
Otherwise, if you remove Document, DocumentStatus will stay in the database.

Is there a better way to handle the Doctrine proxy object

I have two classes linked with a one-to-one relation.
class Client {
...
/**
* #ORM\OneToOne(targetEntity="ClientInfo")
* #ORM\JoinColumn(name="id", referencedColumnName="client_id")
*/
private $info;
...
public function doSomething() {
if (!$this->getInfo() instanceof ClientInfo) {
return false;
}
return $this->getInfo()->doSomething();
}
...
}
class ClientInfo {
...
/**
* #ORM\Id
* #ORM\OneToOne(targetEntity="Client")
* #ORM\JoinColumn(name="client_id", referencedColumnName="id")
*/
private $client;
...
public function doSomething() {
return 'something';
}
...
}
Those classes are loaded with database content with Doctrine. It is working perfectly when there is data in the database. But if there is not ClientInfo data, I have a \Doctrine\ORM\EntityNotFoundException raised.
So I changed the doSomething() method to take this into account.
public function doSomething() {
if (!$this->getInfo() instanceof ClientInfo) {
return false;
}
try {
return $this->getInfo()->doSomething();
} catch (\Doctrine\ORM\EntityNotFoundException $e) {
return false;
}
}
But it does not feel right to me since it is tied with Doctrine. I am trying to modify my unit tests to add a mock of the proxy object but it does not feel right either.
Is there a better way of doing that?
EDIT 1
I followed Nico Kaag suggestion but it does not change anything.
My constructor in my Client class look like this:
public function __construct() {
$this->info = new ClientInfo();
}
If I do a var_dump of $this->info after retrieving my object with Doctrine, this is what I get.
object(Proxies\__CG__\MyBundle\Entity\ClientInfo)[444]
public '__initializer__' =>
object(Closure)[461]
public '__cloner__' =>
object(Closure)[462]
public '__isInitialized__' => boolean false
private 'client' (MyBundle\Entity\ClientInfo) => string '21055' (length=5)
...
EDIT 2
I finally changed what I have done. I removed the try..catch block and change the query to retrieve objects from database. Now I force the query to retrieve the ClientInfo object at the same time as the Client object.
This way, I can trust my test and if I forget to query both objects simultaneously, I will have an exception to remind it to me.
See I have made classes for you.
use Doctrine\ORM\Mapping AS ORM;
/**
* #ORM\Entity
*/
class client
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\OneToOne(targetEntity="Entities\client_info", inversedBy="client")
* #ORM\JoinColumn(name="client_info_id", referencedColumnName="id", unique=true)
*/
private $clientInfo;
}
use Doctrine\ORM\Mapping AS ORM;
/**
* #ORM\Entity
*/
class client_info
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\OneToOne(targetEntity="Entities\client", mappedBy="clientInfo")
*/
private $client;
}
Try this, you will not get such issue.
Also I have used bi-directional relation with cardinality one-to-one, parent connection 0:1*- (parent optional), please see the diagram.
Suggetion : Use ORM designer tool for designing and extracting entity classes.

Doctrine doesn't load associations from session correctly

I'm having this behavior with Doctrine 2.1 where I'm looking for a nice 'workaround'. The problem is as follows:
I have a user Entity:
/**
* #Entity(repositoryClass="Application\Entity\Repository\UserRepository")
* #HasLifecycleCallbacks
*/
class User extends AbstractEntity
{
/**
*
* #var integer
*
* #Column(type="integer",nullable=false)
* #Id
* #GeneratedValue(strategy="IDENTITY")
*/
protected $id;
/**
*
* #var \DateTime
* #Column(type="datetime",nullable=false)
*/
protected $insertDate;
/**
*
* #var string
* #Column(type="string", nullable=false)
*/
protected $username;
/**
*
* #ManyToOne(targetEntity="UserGroup", cascade={"merge"})
*/
protected $userGroup;
}
And a usergroup entity:
/**
* #Entity
*/
class UserGroup extends AbstractEntity
{
/**
*
* #var integer
*
* #Column(type="integer",nullable=false)
* #Id
* #GeneratedValue(strategy="IDENTITY")
*/
protected $id;
/**
*
* #var string
* #Column(type="string",nullable=false)
*/
protected $name;
}
If I instantiate a user object (doing this with Zend_Auth) and Zend_Auth puts it automatically the session.
The problem is however, that is I pull it back from the session at a next page then the data in the user class is perfectly loaded but not in the userGroup association. If I add cascade={"merge"} into the annotation in the user object the userGroup object IS loaded but the data is empty. If you dump something like:
$user->userGroup->name
You will get NULL back. The problem is no data of the usergroup entity is accesed before the user object is saved in the session so a empty initialized object will be returned. If I do something like:
echo $user->userGroup->name;
Before I store the user object in the session all data of the assocication userGroup is succesfully saved and won't return NULL on the next page if I try to access the $user->userGroup->name variable.
Is there a simple way to fix this? Can I manually load the userGroup object/association with a lifecycle callback #onLoad in the user class maybe? Any suggestions?
Your problem is a combination of what mjh_ca answered and a problem with your AbstractEntity implementation.
Since you show that you access entity fields in this fashion:
$user->userGroup->name;
I assume your AbstractEntity base class is using __get() and __set() magic methods instead of proper getters and setters:
function getUserGroup()
{
return $this->userGroup;
}
function setUserGroup(UserGroup $userGroup)
{
$this->userGroup = $userGroup;
}
You are essentially breaking lazy loading:
"... whenever you access a public property of a proxy object that hasn’t been initialized yet the return value will be null. Doctrine cannot hook into this process and magically make the entity lazy load."
Source: Doctrine Best Practices: Don't Use Public Properties on Entities
You should instead be accessing fields this way:
$user->getUserGroup()->getName();
The second part of your problem is exactly as mjh_ca wrote - Zend_Auth detaches your entity from the entity manager when it serializes it for storage in the session. Setting cascade={"merge"} on your association will not work because it is the actual entity that is detached. You have to merge the deserialized User entity into the entity manager.
$detachedIdentity = Zend_Auth::getInstance()->getIdentity();
$identity = $em->merge($detachedIdentity);
The question, is how to do this cleanly. You could look into implementing a __wakeup() magic method for your User entity, but that is also against doctrine best practices...
Source: Implementing Wakeup or Clone
Since we are talking about Zend_Auth, you could extend Zend_Auth and override the getIdentity() function so that it is entity aware.
use Doctrine\ORM\EntityManager,
Doctrine\ORM\UnitOfWork;
class My_Auth extends \Zend_Auth
{
protected $_entityManager;
/**
* override otherwise self::$_instance
* will still create an instance of Zend_Auth
*/
public static function getInstance()
{
if (null === self::$_instance) {
self::$_instance = new self();
}
return self::$_instance;
}
public function getEntityManager()
{
return $this->_entityManager;
}
public function setEntityManager(EntityManager $entityManager)
{
$this->_entityManager = $entityManager;
}
public function getIdentity()
{
$storage = $this->getStorage();
if ($storage->isEmpty()) {
return null;
}
$identity = $storage->read();
$em = $this->getEntityManager();
if(UnitOfWork::STATE_DETACHED === $em->getUnitOfWork()->getEntityState($identity))
{
$identity = $em->merge($identity);
}
return $identity;
}
}
And than add an _init function to your Bootstrap:
public function _initAuth()
{
$this->bootstrap('doctrine');
$em = $this->getResource('doctrine')->getEntityManager();
$auth = My_Auth::getInstance();
$auth->setEntityManager($em);
}
At this point calling $user->getUserGroup()->getName(); should work as intended.
When you store the entity to a session (via Zend_Auth or otherwise), the object is serialized and no longer maintained by Doctrine when subsequently retrieved and unserialized. Try merging the entity back into the EntityManager. See http://www.doctrine-project.org/docs/orm/2.1/en/reference/working-with-objects.html

Categories