Deserialize and persist relationships with JMS Serializer - symfony

I'm trying to get the following working:
I've got an entity like:
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as JMS;
/**
* Contact
*
* #ORM\Table()
* #ORM\Entity()
*/
class Contact
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=255)
*/
private $name;
/**
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\ServiceClient", inversedBy="contacts")
* #ORM\JoinColumn(name="service_client", referencedColumnName="service_client")
*
* #JMS\Type("AppBundle\Entity\ServiceClient")
* #JMS\SerializedName("serviceClient")
*/
private $serviceClient;
}
I'm sending the following JSON over an HTTP request (Post, it's a new Contact, no ID):
{
"name": "Lorem Ipsum",
"serviceClient": {"service_client": "ipsum"}
}
What I expect is for the JMS Serializer to parse that relationship, and leting me persist the Contact object like this:
<?php
$contact = $this->get('serializer')->deserialize(
$request->getContent(),
Contact::class, 'json'
);
$this->em->persist($contact);
$this->em->flush();
In fact I got that working (I swear it was working) but now it's giving me the follwing error:
A new entity was found through the relationship
'AppBundle\Entity\Contact#serviceClient' that was not configured to
cascade persist operations for entity:
AppBundle\Entity\ServiceClient#000000006fafb93e00007f122bd10320. 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\"}). If you
cannot find out which entity causes the problem implement
'AppBundle\Entity\ServiceClient#__toString()' to get a clue."
So it's tryign to persist the entity... a thing I do not want since the entity already exists. I just want Doctrine to put the reference, the foreign key.
Edit: It seems it's the constructor, if I set it to the doctrine_object_constructor it works like magic, the thing I do not understand is why it stop working in the first place.
Can anyone share any ideas or a cleaner way to do what I did?
jms_serializer.object_constructor:
alias: jms_serializer.doctrine_object_constructor
public: false

This problem happens when Doctrine cannot map your relationship to an existing record in the database, so it will try to create a new one with the data from the JSON object.
In your case, the JSON object: {"service_client": "ipsum"} cannot be mapped to an existing ServiceClient instance.
It's because the default JMS object constructor call the unserialize function (will be the one from your Entity if you defined this method) to construct the object, which mean this object will always be treated by Doctrine as new (has never been persisted).
By using doctrine_object_constructor, JMS will get the object from Doctrine. The object came from Doctrine not only have the attributes and methods you define in your entity, but also meta-data about whether it's an existing one, it's corresponding row from the database ( so Doctrine can detect update made on the record later and handle it), therefore Doctrine are able to avoid incorrect persisting.

Doctrine will try to persist the Contact with a reference of a ServiceClient entity given in the deserialization. In the entity definition at the level of the manyToOne definition you need to add :
#ORM\ManyToOne(targetEntity="AppBundle\Entity\ServiceClient", inversedBy="contacts", cascade={"persist"})

Related

Doctrine throw error: "A new entity was found through the relationship" after removing entity

Let's say I have two entities, Project and User with relation.
Project.php
/**
* #var User
*
* #ORM\ManyToOne(targetEntity="User")
* #ORM\JoinColumn(onDelete="SET NULL")
*/
private $creator;
When I remove the User entity, the doctrine leaves the User object(without ID) in the Project entity. In a normal situation, this is fine but I am using DomainEvents. In this scenario, after removing the User entity, DomainEvent triggers saving some data in the DB and secondary saving data(after removing) throw this error. This happens because of now in the Project entity we have the detached(from the EM) User object without ID.
I thought about a listener, that will remove empty objects in the entity after removing, but I am not sure that is a good variant
What is the best variant for solving this error?
The onDelete option doesn't apply a cascade removing.
If you want to do so I think you should have to add the cascade={"remove"} option to the ManyToOne.
Try as following :
/**
* #var User
*
* #ORM\ManyToOne(targetEntity="User", cascade={"remove"})
* #ORM\JoinColumn(onDelete="SET NULL")
*/
private $creator;
Removing entity in doctrine

Doctrine 2.5. OneToOne relationship sharing primary key

In my app, when users sign up I have to send them an email with a validation key, as usually happens on most websites, I'm trying to do this with Doctrine but I can’t get it to work when I try to persist() the user.
First of all, I think the correct way in this case is to use a OneToOne unidirectional relationship, but I don’t know if it would be better to use a bidirectional one. I've tried both and I always get an error.
I have read these two questions carefully:
One to one relationship on two tables sharing primary key
Doctrine one-to-one unidirectional
As well as this part of the documentation. When I validate the schema (php bin/console doctrine:schema:validate) everything is fine.
class Usuario {
/**
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
// ...
}
class ClaveVal {
/**
* #ORM\Id
* #ORM\OneToOne(targetEntity="Usuario")
*/
private $usuario;
/**
* #ORM\Column(name="clave", type="string", length=20, nullable=false)
* #Assert\NotBlank()
*/
private $clave;
// ...
}
Which is quite similar to this.
Now, I'm trying to persist() a new Usuario and a new ClaveVal for this usuario like this:
$usuario = new Usuario();
// Add usuario attributes
$claveVal = new ClaveVal();
$claveVal->setUsuario($usuario);
$claveVal->setClave(‘123456’);
$em->persist($usuario);
$em->persist($claveVal);
But I get this error:
The given entity of type 'AppBundle\Entity\ClaveVal'
(AppBundle\Entity\ClaveVal#000000007020f28f0000000031d5c8c6) has no
identity/no id values set. It cannot be added to the identity map
I know why this happens. This works perfectly:
$em->persist($usuario);
$em->flush();
$em->persist($claveVal);
$em->flush();
But I don't want to do that because I want it to be a unit of work using flush() only once.
Besides, as the author of the post I linked above says, it should be Doctrine's job to flush() at the right moment to get the id.
So, how can I achieve this using flush() only once (and without using transactions or listeners, I'm sure there is an easier way to do this)? Would it be better to use a bidirectional OneToOne relationship? As I said, I tried it too but I got the same error.
Thanks in advance.

Entity associated with non-entity

I have an interface SupplierInterface with 2 implementations: B2BSupplier (a Doctrine entity), RetailSupplier (a static object).
<?php
namespace MyBundle\Model;
interface SupplierInterface {
const B2B = 'B2B';
const RETAIL = 'Retail';
/**
* #return string
*/
public function getSupplierType();
/**
* #return string
*/
public function __toString();
}
Another entity, Supply has a many-to-one relationship with a Supplier. Normally this isn't problematic. But because RetailSupplier is not a Doctrine entity, I'm a bit flummoxed about how to proceed.
Supply looks like this:
<?php
namespace MyBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Validator\Constraints as Assert;
use Gedmo\Blameable\Traits\BlameableEntity;
use Gedmo\Timestampable\Traits\TimestampableEntity;
/**
* Supply
*
* #ORM\Table(name="cir_supply")
* #ORM\Entity()
*/
class Supply
{
use BlameableEntity;
use TimestampableEntity;
/**
* #var int
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\ManyToOne(targetEntity="B2BSupplier")
* #ORM\JoinColumn(name="supplier_id", referencedColumnName="id", nullable=true)
*/
protected $supplier; // <-- PROBLEM, since supplier could be B2BSupplier entity, or it could be vanilla object RetailSupplier
/**
* #ORM\ManyToOne(targetEntity="Chemical", inversedBy="supplies")
* #ORM\JoinColumn(name="chemical_id", referencedColumnName="id", nullable=false)
*/
protected $chemical;
/**
* #ORM\Column(name="external_id", type="string")
*/
protected $externalId;
//getters and setters ...
}
How do I specify a Doctrine relationship when that relationship might not always be valid?
From my experience I'm 99% sure you can't do what you want in your current setup. That being said, there are a few workarounds I can think of. Also before I go into the workarounds. You should think if you really want OneToOne relation on 'supplier' or will ManyToOne work better. OneToOne has some Lazy loading issues and also Workaround 3 work better with ManyToOne.
Workaround 1:
Remove the relation and make the supplier filed contain the id, without having a relation defined.
Extend SupplierRepository 'find' method to handle the cases where id is
2.1 'null' there is no relation in witch case it returns RetailSupplier
2.2 call parent::find for all other cases
2.3 Optional: if null relations are required change 2.1 to use '0' instead of null (adds con 3)
Pros:
fast to achieve from your current setup
keep database foreign key (if step 2.3 is ignored)
Cons:
hidden behavior of the 'find' method
you loose the your doctrine relation
not scalable for other types of Suppliers
source of the information is split between the app and the database
if step 2.3 is required, you loose database foraign key ('0' will not be a foraign key)
Workaround 2:
Modify getSupplier to return RetailSupplier if $this->supplier is null
Modify setSupplier to set null if $supplier is instance of RetailSupplyer
Optinal: Change the first 2 steps to handle '0' as RetailSupplyer and 'null' as no relation
Pros:
fast to achieve from your current setup
keep database foreign key (if step 3 is ignored)
keep doctrine relation
Cons:
hidden behavior of the setter and getter
not scalable for other types of Suppliers
if step 3 is required, you loose database foraign key ('0' will not be a foraign key)
source of the information is split between the app and the database
Workaround 3 (doctrine inheritance mapping):
Create an abstract (called Supplier) this will be inherited by RetailSupplyer and B2BSupplier
Add inheritance metadata to Supplier abstract something like this
Create an entity for RetailSupplyer and a database table with one single line to start (the first RetailSupplier)
Change your database to match your inheritance mapping settings (for more info http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/inheritance-mapping.html)
Change your relation to ManyToOne on $supplier and make it to point to Supplier
Pros:
source of the information is only the database
no hidden behavior in your code
scalable for other types of suppliers and other more retail suppliers
Cons:
harder to achieve from your current setup (database changes, new doctrine setup, possibly some refactor)
pros/cons: Depending on the selected inheritance type you can have full relation path in your database (with foraign key), or you can have no relations. This is up to you ;) after you read the documentation for inheritance mapping.
PS: If I had to choose i will go with Workaround 3. It is hardest to achieve, but solid do it.
Hope this helps and happy coding
Alexandru Cosoi

Symfony2 & FOSRestBundle: Getting UUID packed in a BINARY(16) field from MySQL

I am facing a weird problem relating to UUIDs.
I have developed a REST API using Symfony2+FOSRestBundle+JMSSerializer. As I need to update some tables from two sources I thought of using UUID as primary key for one entity.
I did a doctrine:mapping:import to generate entities in my Symfony project. Everything correct. I ended up with the following entity (only exposing the key field and generated getter for simplicity):
<?php
namespace Stardigita\TgaAPIBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* TgaBookings
*
* #ORM\Table(name="tga_bookings", indexes={[...]})
* #ORM\Entity
*/
class TgaBookings
{
/**
* #var string
*
* #ORM\Column(name="book_cd_booking_UUID", type="blob", length=16, nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $bookCdBookingUuid;
/**
* Get bookCdBookingUuid
*
* #return string
*/
public function getBookCdBookingUuid()
{
return $this->bookCdBookingUuid;
}
...
No setter was generated. I can still do it myself and I will, as I will need to know the key beforehand.
The data for this field is correctly stored in the table as a BINARY(16). When I recover the data calling the REST GET method, I get this:
[
{
"book_cd_booking_uuid": "Resource id #1244",
"book_cd_booking": 8,
....
My question is: how can I get the actual data from the field?
I suppose something has to be done in the field getter, but I tried some solutions without any success.
Thanks.
UPDATE:
I've managed to get the actual data logged, modifying the getBookCdBookingUuid method this way:
/**
* Get bookCdBookingUuid
*
* #return string
*/
public function getBookCdBookingUuid()
{
return bin2hex($this->bookCdBookingUuid);
}
and changed the type to "guid" in the property annotation:
/**
* #var string
*
* #ORM\Column(name="book_cd_booking_UUID", type="guid", length=16, nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $bookCdBookingUuid;
I have represented the hex UUID correctly in the log before returning the results in the controller:
[2014-11-03 19:52:07] app.ERROR: 1046684e5f6711e4a09f00089bce936a [] []
But still getting an exception relating UTF invalid characters:
request.CRITICAL: Uncaught PHP Exception RuntimeException: "Your data could not be encoded because it contains invalid UTF8 characters." at /var/www/tga_api/vendor/jms/serializer/src/JMS/Serializer/JsonSerializationVisitor.php line 36 {"exception":"[object] (RuntimeException: Your data could not be encoded because it contains invalid UTF8 characters. at /var/www/tga_api/vendor/jms/serializer/src/JMS/Serializer/JsonSerializationVisitor.php:36)"} []
Also I got no response from the service. A 500 error is returned.
Please, I need to solve this issue. Any ideas are welcome.
Thanks.
GeneratedValue
I notice you're using the annotation #ORM\GeneratedValue(strategy="IDENTITY") for the UUID property. IDENTITY means the database should/will use auto-increments, which shouldn't be done when using UUIDs. Please change it to #ORM\GeneratedValue(strategy="NONE") or remove it completely.
Conversion
The string form of a UUID (like 01234567-89ab-cdef-0123-456789abcdef) should be converted to binary when it's persisted in the database, and converted back when fetched from the database.
The easiest way to do this is to introduce a custom type. See here for an example.
Bugs
Doctrine (even master/2.5) has some issues with using UUIDs in associations. I'm attempting to fix these issues in PR #1178.
If you need UUIDs in associations and can't wait till it's fixed, then use regular integer ids and have the UUID is a separate column.

Doctrine2 doesn't load Collection until a method is called

When does Doctrine2 loads the ArrayCollection?
Until I call a method, like count or getValues, I have no data
Here is my case. I have a Delegation entity with OneToMany (bidirectional) relation to a Promotion Entity, like this:
Promotion.php
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
*/
class Promotion
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\ManyToOne(targetEntity="Delegation", inversedBy="promotions", cascade={"persist"})
* #ORM\JoinColumn(name="delegation_id", referencedColumnName="id")
*/
protected $delegation;
}
Delegation.php
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
*/
class Delegation
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\OneToMany(targetEntity="Promotion", mappedBy="delegation", cascade={"all"}, orphanRemoval=true)
*/
public $promotions;
public function __construct() {
$this->promotions = new \Doctrine\Common\Collections\ArrayCollection();
}
}
Now I do something like the following (with a given delegation)
$promotion = new Promotion();
$promotion = new Promotion();
$promotion->setDelegation($delegation);
$delegation->addPromotion($promotion);
$em->persist($promotion);
$em->flush();
Looking for the relation into the database is ok. I have my promotion row with the delegation_id set correctly.
And now my problem comes: if I ask for $delegation->getPromotions() I get an empty PersistenCollection, but if I ask for a method of the collection, like $delegation->getPromotions()->count(), everything is ok from here. I get the number correctly. Asking now for $delegation->getPromotions() after that I get the PersistenCollection correctly as well.Why is this happening? When does Doctrine2 loads the Collection?
Example:
$delegation = $em->getRepository('Bundle:Delegation')->findOneById(1);
var_dump($delegation->getPromotions()); //empty
var_dump($delegation->getPromotions()->count()); //1
var_dump($delegation->getPromotions()); //collection with 1 promotion
I could ask directly for promotions->getValues(), and get it ok, but I'd like to know what is happening and how to fix it.
As flu explains here Doctrine2 uses Proxy classes for lazy loading almost everywhere. But acessing $delegation->getPromotions() should automatically invoke the corresponding fetch. A var_dump get an empty collection, but using it- in a foreach statement, for example- it is working ok.
Calling $delegation->getPromotions() only retrieves the un-initialized Doctrine\ORM\PersistentCollection object. That object is not part of the proxy (if the loaded entity is a proxy).
Please refer to the API of Doctrine\ORM\PersistentCollection to see how this works.
Basically, the collection itself is again a proxy (value holder in this case) of a real wrapped ArrayCollection that remains empty until any method on the PersistentCollection is called. Also, the ORM tries to optimize cases where your collection is marked as EXTRA_LAZY so that it is not loaded even when you apply some particular operations to it (like removing or adding an item).

Resources