Extending entities in Symfony2 with Doctrine2 - symfony

I'm having trouble finding a way to appropriately extend an Entity across bundles in Symfony2 using Doctrine2 as the ORM.
Currently there are three methods that I've found to extending entities in Symfony2 using Doctrine2 as the ORM. Mapped Superclass, Single Table Inheritance and Class Table Inheritance. None of these work for what I'm looking to do.
I have two Bundles: UserBundle and BlogBundle. I want to be able to use the UserBundle in projects that do not have the BlogBundle, but the BlogBundle will always be used in projects that have the User Bundle. It's ok if the BlogBundle has dependencies on the UserBundle, but not the other way around.
I have two entities:
BlogBundle\Entity\Post and
UserBundle\Entity\User
Relationship:
There needs to be a One to Many relationship between Users and Blog Posts. This is achieved through a Author_ID property (column) on the BlogBundle\Entity\Post object (table) which is mapped to UserBundle\Entity\User.id
The Problem:
I can call the UserBundle\Entity\User entity directly from within the BlogBundle and achieve what I'm looking for using a Uni-Directional mapping. This does not allow me to access all posts by a user from within a User object. I can access the data via custom queries but this is not as clean as accessing posts by a user through the user object.
What I'd like to do is extend the UserBundle\Entity\User object from within the BlogBundle, and add the methods and properties to this object that establish the One to Many mapping used within the BlogBundle. None of this is persisted, it simply defines the relationship and allows me to logically access all posts created by a user in an application that implements both the BlogBundle and UserBundle by adding needed functionality to the User object within the blog bundle (thus avoiding a dependency from the UserBundle to the BlogBundle).
When I create a BlogBundle\Entity\User object and extend UserBundle\Entity\User I must declare #ORM\Table(name="usertablename"). If I don't, any attempt to access the BlogBundle\Entity\User object will fail to access the database. Since none of the additions in the extended object persist, this works fine across bundles. The issue with this is when I call "php app/console doctrine:schema:update --force", there is a conflict since two entities try to map to & create the same table. I have tried using the ResolveTargetEntityListener feature that was recently implemented but this, along with Mapped Superclas, STI and CTI all force a dependency on the BlogBundle from the UserBundle.
Below are my objects to help illustrate my my setup. They have been abbreviated for clarity. I realize some of the semantics aren't correct but it's intended to communicate the ideas & configuration.
UserBundle\Entity\User
#ORM\Table(name="app_user")
#ORM\Entity
class User implements UserInterface
{
...
}
BlogBundle\Entity\Post
#ORM\Table(name="app_post")
#ORM\Entity
class Post
{
...
#ORM\Column(name="author_id", type="integer")
protected $author_id;
#ORM\ManyToOne(targetEntity="\App\BlogBundle\Entity\User", inversedBy="posts")
#ORM\JoinColumn(name="author_id", referencedColumnName="id")
protected $author;
}
BlogBundle\Entity\User
use App\UserBundle\Entity\User as BaseUser
#ORM\Entity
#ORM\table(name="app_user")
class User extends BaseUser
{
....
#ORM\OneToMany(targetEntity="App\BlogBundle\Entity\Post", mappedBy="author")
protected $posts;
public function __construct()
{
parent::_construct();
$this->posts = new \Doctrine\Common\Collections\ArrayCollection();
}
....
/* Getters & Setters, nothing that defines #ORM\Column, nothing persisted */
}
This works but the problem is that I'm mapping two entities in the project to the same table. The extended object doesn't grab the #ORM\Table(name="app_user") from it's parent so it must be defined in BlogBundle\Entity\User. If not any reference to this object from a controller will not access the database. Since nothing is persisted from the extended object nothing is broken except for when I try to update the database schema from the console.
I can use a unidirectional relationship, but this limits how I can access the data from within a controller.

You can see in this link to know about inheritance: http://docs.doctrine-project.org/en/latest/reference/inheritance-mapping.html#single-table-inheritance
You must declare in UserBundle\Entity\User:
/**
* #Entity
* #InheritanceType("SINGLE_TABLE")
* #DiscriminatorColumn(name="discr", type="string")
* #DiscriminatorMap({"baseuser" = "UserBundle\Entity\User", "blogUser" = "BlogBundle\Entity\User"})
*/
class User implements UserInterface
{
...
}
And BlogBundle\Entity\User
use App\UserBundle\Entity\User as BaseUser;
/**
* #ORM\Entity
*/
class User extends BaseUser
{
....
}
Goodluck!

I think you could find this Bundle interesting:
https://github.com/mmoreram/SimpleDoctrineMapping
It allows you to define by parameters what files are mapping your entities, allowing to override every entity of your generic bundles.
For example:
parameters:
#
# Mapping information
#
test_bundle.entity.user.class: "TestBundle\Entity\User"
test_bundle.entity.user.mapping_file_path: "#TestBundle/Mapping/Class.orm.yml"
test_bundle.entity.user.entity_manager: default
test_bundle.entity.user.enable: true
The only contra I see is that you have to define all the next entities the same way cause you disabled auto_mapping...

Related

Symfony - Get Entity from within another entity

I would like to retrieve a record from another entity (or record from the DB) within a entity.
They there are no relationship between the two entities.
I am using #ORM\HasLifecycleCallbacks() and #ORM\PrePersist so when the main entity is created it will also create another entity (save a record to another table)
The above is working fine, there are no issues with this.
What I am having an issue with is I would like to link that entity with another table but I need to retrieve the object based on the value of the first entity.
Usually I would write a function in the entity repository but I am not calling the entity manager within the entity.
An Entity in Doctrine is an object representation of a concept, with attributes and methods. It is meant to be lightweight, a POPO (plain old php object). It must not know anything about its persistence. Therefore if you see reference to the EntityManager in a model, it probably stinks.
Solutions? You could use an entity listener called on entity creation and then use a service dedicated only to properly compose your object(s), maybe something like a Factory. In this way, your entity stays lightweight, the lifecycle management is satisfied and the entity composing is responsibility only of your service.
Entity manager is accessible in an entity repository. You can legally use it to fetch data from other entities and to compose your business logic. This is what entity repositories are made for: Doctrine Custom Repositories, Symfony Custom Repository Classes.
/**
* #ORM\Entity
*/
class Beta {}
/**
* #ORM\Entity
*/
class Alpha {}
class AlphaRepository extends EntityRepository
{
public function getDataFromAnotherEntity($something)
{
$query = 'select * from MyBundle\Entity\Alpha alpha where alpha.id = :something';
return $this->getEntityManager()
->createQuery($query)
->setParameter('something', $something)
->getResult();
}
}
In Symfony 3.1 you can use the entityManager to set a reference. This is still lightweight as it does not instance a complete Doctrine Record.
Example: I have an entity Status which has some states, and it's referenced in another entity. On create i use this method inside EventSubscriber:
public function preAction(LifecycleEventArgs $args)
{
$entity = $args->getObject();
$entityManager = $args->getObjectManager();
if (method_exists($entity, 'setStatus')) {
if ($entity->getStatus() === null) {
$entity->setStatus($entityManager->getReference('AppBundle\Entity\Status', Status::STATUS_REGULAR));
}
}
}

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.

One-to-many relationship: how to set default entity-value?

I have a one-to-many relationship Cart-SendingMethod. I would like to set a default SendingMethod for new Carts. So I have tried this:
<?php
/**
* #ORM\ManyToOne(targetEntity="MetodoEnvio", inversedBy="metodoEnvios")
* #ORM\JoinColumn(name="metodo_envio_id", referencedColumnName="id")
**/
private $metodoEnvio = 1;
but doesn't work... I get:
Impossible to access an attribute ("id") on a integer variable ("1") when I call Cart.SendingMethod.id from a view file
So how to set a default SendingMethod for new Products?
I could do it in the controller, but I would like to know if it is possible from the entity Product.
Note: I didn't know exactly if this is a symfony or doctrine question.
You don't want to introduce dependencies into your entity.
The obvious and cleaner way to do it would be to create a CartFactory service, and inject that into any controller (or other class) that needs to create carts. Inject your EntityManager and other dependencies into the factory. That way you DRY up your cart-initialization code, and avoid bulking up your controller.
Just set the property's default value inside the constructor like this:
public function __construct(..)
{
$this->property = new OtherObject();
}

How to handle SQL triggers in Symfony2?

I have a User entity in my Symfony2/Doctrine2 webapp. This user has an attribute last_updated to identify the latest time, anything has changed. I set this attribute to NOT NULL in my database. So far, so good.
I would consider it to be good practice to create a SQL trigger in the database, that sets this last_updated to NOW() on every INSERT or UPDATE. So you don't have to care about this in your application. So that's what I did, I implemented this trigger in my database.
But if I now create a user in my app
$user = new User();
$em = $this->getDoctrine()->getManager();
$em->persist($user);
$em->flush();
I get an error message by Symfony:
An exception occurred while executing 'INSERT INTO User (username, ..., last_updated) VALUES (?, ..., ?)'
with params ["johndoe", ..., null]:
SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'last_updated' cannot be null
The problem is clear: Symfony is trying to fire an INSERT-statement to the database with the parameter null for last_updated, which is not allowed - as this attribute may not be null.
I could quickly think of two workarounds:
One workaround would be to take the last_updated field out of the entity description. Then Symfony would not try to pass anything to the database for this column, and the trigger would set the appropriate value. But I don't think this is a good way, because as soon as I would try to update the db schema (doctrine:schema:update --force) I would loose my last_updated-column.
Another workaround: Simply do $user->setLastUpdated(new \DateTime()) before I persist() and flush(). But this would minimize the advantage of using a trigger on my database to avoid having to care about it in my application.
Is there any way to let Symfony/Doctrine know that there is a trigger running on my database? If not, (how) can I hook into Symfony/Doctrine to implement a proper workaround?
To quote a response to this question from a google group:
Database side code (such as Triggers and Functions) tend to break the benefits of developing software using an ORM like Propel or Doctrine as one of the biggest advantages of using ORM's is to be database agnostic. By having database side Triggers and Functions you are tying yourself to the database and therefore gain little to no benefit using an ORM. -GarethMc
https://groups.google.com/forum/#!topic/symfony-users/MH_ML9Dy0Rw
For this it is best to use the Life Cycle Callbacks as Faery suggests. One simple function will handle updating that field so that you dont have to worry about it if you decide to change databases in the future.
//In Your Entity File EX: SomeClass.php
/**
* #ORM\Entity
* #ORM\HasLifecycleCallbacks()
*/
class SomeClass
{
....
/**
* #ORM\PrePersist()
* #ORM\PreUpdate()
*/
public function prePersistPreUpdate()
{
$this->last_modified = new \DateTime();
}
}
See also references for lifecycle callbacks
Symfony reference
Doctrine reference
In your case you would add the lifecycle call back function and annotation to your User entity class. SomeClass is simply an example class showing that lifecycle callbacks are good for more than just your User entity.
Another (easier and more generalized) option would be to use the Timestampable Doctrine extension by Gedmo. In this way, you could simply annotate your entity fields to be timestamped on create or on update.
Example:
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
class MyEntity
{
...
/**
* #var \DateTime $lastUpdated
*
* #Gedmo\Timestampable(on="update")
* #ORM\Column(name="last_updated", type="datetime")
*/
private $lastUpdated;
...
}
https://packagist.org/packages/gedmo/doctrine-extensions

Doctrine extend Entity

I'm using Symfony with Doctrine.
To resolve my problem I want to:
or When I'm extending Entity class I want the doctrine to ignore parent's class #entity annotation (to see it as #MappedSuperclass)
or (this one is more preferable) When I'm extending Entity class add to child class something like #MappedChildclass just to know that this class is an Entity, but actual implementation and mappings in the parent class
Lets see concrete problem:
I have 3 bundles:
AppBridgeBundle
UserBundle
ProfileBundle
UserBundle and UserProfile must be decoupled. AppBridgeBundle is a bridge between 2 bundles, it will couple both.
UserBundle has UserEntity:
/**
* #ORM\Entity
**/
class UserEntity {
/**
* #var \Acme\UserBundle\Interfaces\ProfileData
* #ORM\ManyToOne(targetEntity="Acme\UserBundle\Interfaces\ProfileData",
cascade={"persist"})
* )
*/
private $profileData;
// ...
}
UserBundle has its own interface ProfileData (decoupled, we only need to inject implementation of this interface).
ProfileBundle has ProfileEntity:
/**
* #ORM\Entity
**/
class ProfileEntity implements Acme\ProfileEntity\Interfaces\Profile {
// properties ...
}
ProfileBundle has its own interface Profile (decoupled).
Basically ProfileData and Profile interfaces are the same.
Now AppBridgeBundle introduces Adapter of Profile and ProfileData interfaces to adapt UserBundle with ProfileBundle.
class ProfileAdapter extends \Acme\ProfileBundle\Entity\ProfileEntity
implements \Acme\UserBundle\Interfaces\ProfileData,
\Acme\ProfileBundle\Interfaces\Profile {
}
Next, we inject our interface implementation in the config.yml app configuration:
orm:
resolve_target_entities:
Acme\UserBundle\Interfaces\ProfileData: Acme\AppBridgeBundle\Entity\ProfileAdapter
Now, the problem is that when I update doctrine schema it throws me an error that Acme\AppBridgeBundle\Entity\ProfileAdapter is not Entity.
If I mark ProfileAdapter with #Entity then it will create 2 separate tables - I don't need it
If I mark ProfileAdapter with #Entity with the same name as ProfileEntity - #Table('profileentity') then it throws me an error that table profile already exists
If I mark ProfileEntity with #ORM\MappedSuperclass and remove #Entity annotation from it - I'll loose default implementation of the ProfileEntity class (so it cant work without bridge anymore).
if I mark ProfileEntity with #InheritanceType("SINGLE_TABLE") it will add to the tables unnecessary discriminator field to my table.
Any suggestions?
I think the trick is to use the dispatched doctrine "loadClassMetadata" event.
You can track this event in a subscriber which is aware of ProfileEntity inheritance, and inject the "#Entity" or the "#MappedSuperclass" annotation.
You can see an implementation here : https://github.com/victoire/CmsBundle/blob/master/EventSubscriber/LoadMetadataSubscriber.php
In this example, I dynamically inject enabled widgets to the discriminatorMap of my Widget entity.
I let you adapt this code to your needs.
Had the same problem, what I did using your example:
move \Acme\ProfileBundle\Entity\ProfileEntity to \Acme\ProfileBundle\Model\ProfileEntity
redeclare ProfileEntity class to abstract but leave all the ORM instructions intact
redeclare ProfileAdapter to extend new ProfileEntity model
mark ProfileAdapter as #ORM\Entity only, all remaining #ORM* instructions leave in the model class
Pros:
no additional tables
no additional fields
can work without the bridge
Cons:
it is necessary to create entity extending abstract model for each new Bundle installation

Resources