Get Entity subobject id without joining or new sql request - symfony

Example I have a Entity User which have a Contact. How I get the Contact ID without joining the contact table.
$user = $entityManager->userRepository->findUserById(1);
$contactId = $user->getContact()->getId();
when `getContact() is called the whole contact is loaded from the DB. How I can avoid this SQL request without add a join in findUserById. I only need to contactId which is in my users table, but there is no simple function like $user->getContactId().

Look at this post Getting only ID from entity relations without fetching whole object in Doctrine. I tested a simple example in a ManyToOne relation and the related object was not loaded.

Your situation sounds perfect for the Doctrine Second Level Cache
http://doctrine-orm.readthedocs.org/projects/doctrine-orm/en/latest/reference/second-level-cache.html
With this, the first query will be gotten from DB but every subsequent query will be from the second level cache, plus if you change the data using doctrine then the cache is automatically updated. For this reason, I suggest you set a very long time for the cache my personal favourite is 1 month approx 2592000 seconds.
Enable it in your user entity like so
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use FOS\UserBundle\Model\User as BaseUser;
use Doctrine\Common\Collections\ArrayCollection;
/**
* #ORM\Entity(repositoryClass="AppBundle\Repository\UserRepository")
* #ORM\Cache(usage="NONSTRICT_READ_WRITE", region="users_region")
* #ORM\Table(
* name="site_users",
* indexes={
* #ORM\Index(name="username_index", columns={"username"}),
* #ORM\Index(name="email_index", columns={"email"}),
* #ORM\Index(name="enabled_index", columns={"enabled"}),
* #ORM\Index(name="last_login_index", columns={"last_login"})
* }
*)
*/
class User extends BaseUser
{
/**
* #ORM\Id
* #ORM\Column(name="id", type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
......
Then in your config.yml like so
orm:
auto_generate_proxy_classes: "%kernel.debug%"
entity_managers:
default:
connection: default
auto_mapping: true
naming_strategy: doctrine.orm.naming_strategy.underscore
second_level_cache:
enabled: true
# region_lock_lifetime: 60
region_lifetime: 300
log_enabled: %kernel.debug%
region_cache_driver: apc
regions:
users_region:
region_lifetime: 2592000
region_cache_driver: apc
............
More information here
http://symfony.com/doc/master/bundles/DoctrineBundle/configuration.html
Note this method also ensure that the popular check for an authenticated user $user = $this->getUser(); does not hit your DB as well.

I suggest you to change fetch mode in the specific query, as described here in the doc.
So you can describe your query as follow:
// Supposing this method in the repository class
public function findUserById($idUser)
{
$qb = $this->createQueryBuilder('u')
->where("u.id = :idUser")
->setParameter("idUser", $idUser);
$query = $qb->getQuery();
// Describe here all the entity and the association name that you want to fetch eager
$query->setFetchMode("YourBundle\\Entity\\Contract", "contract", ClassMetadata::FETCH_EAGER);
...
return $qb->->getResult();
}
NB:
Changing the fetch mode during a query is only possible for one-to-one
and many-to-one relations.
Hope this help

Related

Deserialize and persist relationships with JMS Serializer

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"})

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 - Share Entity Between Bundles with different relationships

How do you share an entity between multiple bundles with different relationships?
For example both the ZooAnimalBundle and FarmAnimalBundle need a User Entity. A third Bundle AccountUserBundle has the User Entity.
In both the Zoo and Farm AnimalBundles I create a User Entity like so:
use Account\UserBundle\Entity\User as BaseUser;
class User extends BaseUser
{
}
I then have a Hospital entity in Zoo:
class Hospital {
/**
* #ORM\ManyToMany(targetEntity="Zoo\AnaimalBundle\Entity\User")
* #ORM\JoinTable(name="users_zoo_animals")
*/
protected $users;
And a Room entity in Farm:
class Room {
/**
* #ORM\ManyToMany(targetEntity="Farm\AnaimalBundle\Entity\User")
* #ORM\JoinTable(name="users_farm_animals")
*/
protected $users;
Everything works so far in that I can call Zoo.Room->getUsers() or Farm.Hospital->getUsers()
However the problem is I'm not sure on how to set up the inverse relationship in their respective User entities.
If for example I update the FarmAnimal User Entity and run doctrine:generate:entities
/**
* #ORM\Entity
*/
class User extends BaseUser
{
/**
* #ORM\ManyToMany(targetEntity="Room", mappedBy="users", cascade={"persist"})
*/
protected $rooms;
}
It will copy the protected $properties from BaseUser and create all the set and get methods which is not what I want. What is the correct way of setting up these relationships?
Update
If you don't setup the inverse relationship, how would you select all users where hospital.id = 1
$qb = $this->getEntityManager()->createQueryBuilder()
->select(
'u'
)
->from('Account\UserBundle\Entity\User','u')
->leftJoin('u.hospitals', 'h')
->andWhere('h.id = :hospital_id')
->setParameter('hospital_id',$hospital_id);
This gives the error:
Class Account\UserBundle\Entity\User has no association named hospitals
I know I could select from hospital and join user because that relationship does exist but I need to select users because I am using them with Doctrine\ORM\Tools\Pagination\Paginator
The query would be
$qb = $this->createQueryBuilder('a')
->select(
'h', 'u'
)
->leftJoin('h.users', 'u')
The problem with this is Paginator only sees one result Hospital because the Users are attached to it.
You can define abstract entity dependencies and implement them with other bundles.
First, each of the bundles depending on a user entity should define a User interface. For example:
namespace Foo\BarBundle\Entity;
interface UserInterface
{
public function getId();
public function getEmail();
// other getters
}
Then, in each entity depending on the user, define the relationship, e.g.:
namespace Foo\BarBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* #ORM\Entity
*/
class Something
{
/**
* #ORM\ManyToOne(targetEntity="UserInterface")
* #Assert\NotNull
*/
protected $User;
// add other fields as required
}
Now you need to register the User entity as an implementation of the UserInterfaces:
namespace Foo\UserBundle\Entity;
use Foo\BarBundle\Entity\UserInterface as BarUserInterface;
use Foo\FoobarBundle\Entity\UserInterface as FoobarUserInterface;
/**
* #ORM\Entity
*/
class User implements BarUserInterface, FoobarUserInterface
{
// implement the demanded methods
}
Then add the following to app/config/config.yml:
doctrine:
orm:
resolve_target_entities:
Foo\BarBundle\Entity\UserInterface: Foo\UserBundle\Entity\User
Foo\FooarBundle\Entity\UserInterface: Foo\UserBundle\Entity\User
(Heads up: there will usually already be a doctrine.orm node which you'll have to extend.)
This is not a perfect solution, because you cannot really say which fields the user entity should have. On the other hand, it's strictly OOP, as you don't have to know about internals of the User implementation – you just need it to return the right values.
Creating multiple definitions of the account is the wrong way to do it, unless you want to create 3 seperate user tables (even then it's better not to do it this way).
Really you want your other entities to map to the your user entity in the account bundle.
I.e.,
class Hospital {
/**
* #ORM\ManyToMany(targetEntity="Zoo\AccountBundle\Entity\User")
*/
protected $users;
Now, there is no need to create the inverse relationship. In fact, this is a bad practice since you have a bi-directional dependency. Users don't know about hospitals, but hospital knows about it's users. Now, any bundle can map to the user entity and reuse it.

Doctrine2 reinitializes a related entity (one to one) when saving a main entity

The context is a Symfony2 project (2.0.23) using Doctrine2.
I have a Candidate entity with one to one relation Website. When creating a new candidate I set the Website entity like this:
Candidate.php:
<?php
use MyProject\Bundle\CoreBundle\Entity\Website;
public function initialize(Website $website)
{
$this->setWebsite($website);
The one to one relation is declared like this:
<?php
/**
* #var Website $website
*
* #ORM\OneToOne(targetEntity="Website")
* #ORM\JoinColumn(name="Website", referencedColumnName="Code")
*/
private $website;
Everything works fine locally. But on out testing and production servers, when creating a new candidate with a given Website, sometimes the associated entity is updated with default values just when persisting and flushing the main Candidate entity, here is the MySQL log:
/*!*/;
# at 11400207
#130628 9:26:32 server id 1 end_log_pos 11400399 Query thread_id=53611133 exec_time=0 error_code=0
SET TIMESTAMP=1372404392/*!*/;
UPDATE website SET Name = NULL, bEnabled = NULL ... WHERE Id = 2 AND Code = 2000
I persist with the method of the FOSUserBundle:
<?php
/**
* Updates a user.
*
* #param UserInterface $user
* #param Boolean $andFlush Whether to flush the changes (default true)
*/
public function updateUser(UserInterface $user, $andFlush = true)
{
$this->updateCanonicalFields($user);
$this->updatePassword($user);
$this->em->persist($user);
if ($andFlush) {
$this->em->flush();
}
}
I don't understand why. What is really weird is that only happens sometimes and it is quiet unpredictable.
Any suggestion or hint would be welcome... Thanks.
PS: The doctrine metadata cache was deactivated.
Edit1: Added persist, note that this is called through a FormHandler service.
Well it seems it was related to the main entity that had 2 #ORM\Id annotations. As it is an old database, it is not "Doctrine" friendly. There is a PK an auto-increment field, but the joins are made on a second column. So I removed the #ORM\Id annotation for the PK field.

type of users SonataUserBundle + FOSUserBundle + SonataAdminBundle

I'm trying to create 3 users in my project:
Cliente : Who will access front-end and have fields 'name', 'CPF', 'adress'.
Vendedor : Who will register Offers in the site and have fields 'phone', 'CNPJ'
Admin : Who will administer all Clientes, Vendedores, Offers, etc...
So.. I installed 3 bundles for that: SonataUserBundle + FosUserBundle + SonataAdminBundle
I followed the entire tutorial of each one. But I don't know how can I create each type of this users.
I am using ApplicationSonataUserBundle which generate entities User and Group.
Here is my code:
namespace Sete\UserBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Application\Sonata\UserBundle\Entity\User as BaseUser;
/**
* Cliente
*
* #ORM\Table(name="cliente")
* #ORM\Entity
*
*/
class Cliente extends BaseUser
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
...another fields...
}
and Vendedor:
namespace Sete\UserBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Application\Sonata\UserBundle\Entity\User as BaseUser;
/**
* Vendedor
*
* #ORM\Table(name="vendedor")
* #ORM\Entity
*/
class Vendedor extends BaseUser
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
...another fields...
}
AppKernel.php
...
new Sonata\UserBundle\SonataUserBundle('FOSUserBundle'),
new Sonata\EasyExtendsBundle\SonataEasyExtendsBundle(),
new Application\Sonata\UserBundle\ApplicationSonataUserBundle()
And config.yml
...
fos_user:
db_driver: orm # can be orm or odm
firewall_name: main
user_class: Application\Sonata\UserBundle\Entity\User
group:
group_class: Application\Sonata\UserBundle\Entity\Group
sonata_user:
manager_type: orm # can be orm or mongodb
...
This way my entities Cliente and Vendedor have no association with groups. I try to add $groups relationship, but not work. So, when I try to admin this entities I got error:
An exception occurred while executing 'SELECT count(DISTINCT c0_.id)
AS sclr0 FROM cliente c0_ LEFT JOIN fos_user_user_group f3_ ON f2_.id
= f3_.user_id LEFT JOIN fos_user_group f1_ ON f1_.id = f3_.group_id':
SQLSTATE[42S22]: Column not found: 1054 Unknown column 'f2_.id' in 'on
clause'
Is that the best pratice to create types of users? Or, Instead of extends ApplicationUserBundle:User, create Cliente and Vendedor entities (without extends ApplicationUserBundle:User), and then create relationship with User (puting fields $cliente and $vendedor inside User entity and making a relationship) ?
Sorry about the english.
I trying to do this all week. Follow many tutorials but not got the answer.
Thx all.
One way to go about having multiple user types is to use Doctrine inheritance mapping.
However, I would recommend using single user type together with role security handler. You should install SonataUserBundle with Easy Extends (installation instructions), so that you can leave SonataUserBundle unmodified. You should add all needed fields to the App\UserBundle\Entity\User and create three user Groups: Clientes, Vendedors and Admins. In your main config.yml, add two missing roles (there already is ROLE_ADMIN):
ROLE_SUPER_ADMIN: [<...>, ROLE_CLIENT, ROLE_VENDOR]
Assign those three user Groups appropriate permissions that you created.
Now, in User, make custom fieds nullable and in you UserAdmin show only the fieds user type can see using security rigths. Example:
if ($this->isGranted('ROLE_CLIENT')) {
$formMapper->add('cpf');
}

Resources