Subclass Doctrine 2 entity - php

I am trying to subclass a Doctrine 2 entity to add a bunch of "helper" functions I'd like to use.
For example, this is my entity:
namespace Project\Entity;
class Product
{
private $name;
private $idProductCategory;
}
Mapping is done via XML files.
Then, I try to extend it:
namespace Project\Entity;
class ProductJSON extends Product {
public function toJSON() {
/* ... */
}
}
When I try to use this object in Doctrine:
$a = $entityManager->getRepository('\Project\Entity\ProductJSON');
I get the "No mapping file found named Project.Entity.ProductJSON.dcm.xml" error.
Which is perfectly right, because I do not want any additional mapping.
I've extensively read Doctrine docs and about Mapped Super Classes (Doctrine: extending entity class), but as far as I understand that is for extending Entities in a DB-sense.
I don't care about the database/mapping, I just want to extend entities PHP-wise to use the objects seamlessy in my application.
How to achieve this goal?

You do not need a subclass to add additional methods. Just add the methods to your entity class - Doctrine will just ignore them because there is no mapping information attached to them.
E.g.
class Product
{
private $name;
private $idProductCategory;
/* ... */
public function toJSON() {
/* ... */
}
}

Related

Symfony multiple entity managers with one repository

I have a symfony/api-platform application which connects to 2 different databases configured in doctrine.yaml. I want to notice that both databases store the same entities (but with different actions on each), hence we duplicated all the entity classes and created a repository for each, in order for migrations and api-platform actions to work. Since those enities share a lot of common repository functionallity, what I have done so far to remove code
duplication is:
Create entity manager decorators
class AEntityManager extends EntityManagerDecorator{}
class BEntityManager extends EntityManagerDecorator{}
config/services.yaml
App\EntityManager\AEntityManager:
decorates: doctrine.orm.a_entity_manager
App\EntityManager\BEntityManager:
decorates: doctrine.orm.b_entity_manager
App\Repository\Main\AResourceRepository:
arguments:
- '#App\EntityManager\AEntityManager'
App\Repository\ProductPending\BResourceRepository:
arguments:
- '#App\EntityManager\BEntityManager'
Create a base class (for each entity) to share code between the 2 repositories
class RepositoryBase extends ServiceEntityRepository
{
public function __construct(EntityManagerInterface $em, string $class) {...}
// common methods
}
class ARepository extends RepositoryBase
{
public function __construct(EntityManagerInterface $em)
{
parent::__construct($em, A::class);
}
}
class BRepository extends RepositoryBase
{
public function __construct(EntityManagerInterface $em)
{
parent::__construct($em, B::class);
}
}
And entities
/**
#Orm\Entity(repositoryClass=ARepository::class)
*/
class A {
string $prop;
}
/**
#Orm\Entity(repositoryClass=BRepository::class)
*/
class B {
string $prop;
}
Note that all actions for the second database occur in endpoints prefixed with (let's say) /api/b/...
I would like to know if there is a way to eliminate the different repository classes and define the same repository across the 2 different entities. What I have in mind is to change the object which is used to inject the EntityManagerInterface constructor parameter based on the url of the request, but I haven't found something specific about it and I don't know if that's even possible.

Missing argument 1 for method with argument being a service injection in Symfony 3

I have such service linked to one method in my entity:
entity_form:
class: AppBundle\Entity\EntityForm
calls:
- [setDoctrine, ['#doctrine']]
The argument injects (or at least should) doctrine into my entity so that I can get stuff from the db inside methods.
I set it for the getter because I had to omit setting the argument in the __construct because in code the class is never really "constructed"
The entity itself looks like this:
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="entity_form")
*/
class EntityForm
{
protected $id;
protected $url;
protected $name;
protected $className;
protected $description;
private $doctrine;
public function getId()
{
return $this->id;
}
/* _All Getters An Setters Here.._ */
private function setDoctrine($doctrine)
{
return $doctrine;
}
public function createEntity($id)
{
$this->setDoctrine();
$entityPath = sprintf('AppBundle:%s:%s', __NAMESPACE__, $this->getClassName());
var_dump($this->doctrine);
exit;
//var_dump is temponary and only for testing the doctrine
$entity = $this->doctrine->getRepository($entityPath)->find($id);
return $entity;
}
}
This entity stores information about forms on the website. With this I would like to nicely and quickly create entity of class for which the form is made, from given Id.
Unfortunately
Running such code gives me this error:
Warning: Missing argument 1 for
AppBundle\Entity\EntityForm::setDoctrine(), called in
C:\PHP\Repos\centaur\src\AppBundle\Entity\EntityForm.php on line 139
and defined
What could be the reason for this error to happen? How can I fix it?
Any help would be amazing
The argument injects (or at least should) doctrine into my entity so
that I can get stuff from the db inside methods.
No.
You defined entity_form service that is nothing more than AppBundle\Entity\EntityForm with setter-injected doctrine service.
So your EntityForm with injected service is available in container, it won't be injected into every created EntityForm.
Looks like you should read more about responsibilities. What you want to achieve in createEntity method should be a part of application service layer, not a model itself.
Model is not aware of application/infrastructure concepts, so injecting ORM is not ok.
About mentioned error, considering only service definition and that call:
$this->setDoctrine();
You want to call setter without required argument, and probably missed that your service definition already did setter-injection.
As I mentioned before, entity is basically not a service, so move that part into specific class and inject database-related service there using standard DI.

Map subclass as its extended parent

I have created the following abstract class, which use single table inheritance and maps subclasses on the DiscriminatorColumn model.
/**
* #Entity
* #Table(name="entity")
* #InheritanceType("SINGLE_TABLE")
* #DiscriminatorColumn(name="model", type="string")
* #DiscriminatorMap({
* "green" = "model\GreenEntity",
* "blue" = "model\BlueEntity"
* })
*/
abstract class AbstractEntity
{
/** #Id #Column(type="string") */
protected $entity_id;
}
Let's say I extend the abstract class AbstractEntity by some classes:
class GreenEntity extends AbstractEntity {}
class BlueEntity extends AbstractEntity {}
And extend these by some more subclasses
class GreenEntityChildOne extends GreenEntity {}
class GreenEntityChildTwo extends GreenEntity {}
class BlueEntityChildOne extends BlueEntity {}
class BlueEntityChildTwo extends BlueEntity {}
Now, for example, when I instantiate GreenEntityChildOne and persist it to the database, it will throw an exception that I don't have a mapping for it.
What I'm trying to do is get GreenEntityChildOne to be mapped as GreenEntity (or rather, every class which extends a class below AbstractEntity to be mapped as the class which extends the upper abstract class).
Is this at all possible?
It's not possible with pure annotations
Yes, the mapping you are trying to achieve is possible. However, not with pure annotations. The important thing is that Doctrine needs to know all sub classes at runtime. If you do not want to state them explicitly in the annotations of the mapped superclass, you will need to dynamically provide them.
Doctrine event system to the rescue
There is a great blog post on dynamic mapping with Doctrine, which explains how you can use Doctrine event listeners to programmatically change the loaded ClassMetadata.
To dynamically add subclasses to the discriminator map you can implement a Doctrine event listener like the following:
class DynamicDiscriminatorMapSubscriber implements EventSubscriber
{
public function getSubscribedEvents()
{
return array(Events::loadClassMetadata);
}
public function loadClassMetadata(LoadClassMetadataEventArgs $eventArgs)
{
$metadata = $eventArgs->getClassMetadata();
$metadata->addDiscriminatorMapClass("GreenEntityChildOne", GreenEntityChildOne::class);
}
}
Register your subscriber
Now you only need to register the event subscriber with Doctrine. Ideally, you inject the classes you want to add based on your configuration to the event subscriber.
// create dynamic subscriber based on your config which contains the classes to be mapped
$subscriber = new DynamicDiscriminatorMapSubscriber($config);
$entityManager->getEventManager()->addEventSubscriber($subscriber);
Further reading
Also, have a look at the PHP mapping section in the Doctrine manual and the more informative API docs for the ClassMetadataBuilder.
Answer is possibly on the Doctrine Docs:
"All entity classes that is part of the mapped entity hierarchy (including the topmost class) should be specified in the #DiscriminatorMap"
http://doctrine-orm.readthedocs.org/en/latest/reference/inheritance-mapping.html
You've only specified GreenEntity and BlueEntity.
I don't know what I'm talking about. This is the first thing I've ever read about Doctrine...

Inheritance on doctrine's embeddables

Is it possible to use inheritance on value objects embedded in doctrine entities?
The situation I'm thinking about is:
I have an entity that has and embedded value object. That value object has the following hierarchy:
class myEntity {
/** #Embedded(class = "baseValueObject") */
private $value_object;
...
}
class baseValueObject {...}
class valueObject1 extends baseValueObject{...}
class valueObject2 extends baseValueObject2{...}
If I define my entity to have baseValueObject as an embeddable, nothing happens when I use the schema-tool to update my db schema, so I guess that's not the way to do it.
Another option that I'm thinking about is to use single-table inheritance on the entity to create a child entity that use one of the value objects, and another child entity for the other one. Like this:
class myEntity {
/** #Embedded(class = "baseValueObject") */
private $value_object;
...
}
class myEntityA extends myEntity {
/** #Embedded(class = "valueObject1") */
private $value_object;
...
}
class myEntityB extends myEntity {
/** #Embedded(class = "valueObject2") */
private $value_object;
...
}
class baseValueObject {...}
class valueObject1 extends baseValueObject{...}
class valueObject2 extends baseValueObject2{...}
What's the proper approach? Is it even possible to do it this way?
If you want to extend one embeddable from another you need to set the parents properties as protected not private.
https://github.com/doctrine/doctrine2/issues/4097
If you want to use your Value Object in field then you should define new type in doctrine http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/cookbook/custom-mapping-types.html
If you want inherit properties from base then you should use #MappedSuperclass annotation http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/inheritance-mapping.html
You should use #Embeddable when you want split entity by specific properties by creating specific entities. So you can't use Value Object as target. According to documentation (I'm not able to share third link).

Silex + Doctrine ORM doesn't fire events when set on #MappedSuperclass

I am using Silex with Doctrine ORM, everything was working properly but I got a problem that I can't figure out.
I have an Entity News in the namespace Lpdq\Model\Entity which extends another class News in the namespace Lpdq\Model which contains some methods and pre/post event methods for doctrine.
My entity News
<?php
namespace Lpdq\Model\Entity;
/**
* News
*
* #Table(name="news")
* #Entity(repositoryClass="Lpdq\Model\Entity\Repository\News")
*/
class News extends Lpdq\Model\News{
/*some properties/methods*/
}
My super class News
<?php
namespace Lpdq\Model;
/**
* News
*
* #MappedSuperclass
* #HasLifecycleCallbacks
*/
class News{
/**
* #PrePersist
*/
public function prePersist()
{
$this->setCreated(new \DateTime());
$this->setUpdated(new \DateTime());
}
/**
* #PreUpdate
*/
public function preUpdate()
{
$this->setUpdated(new \DateTime());
}
/*...some methods...*/
}
In my controller, I just instance my entity and persist/flush it
<?php
namespace Lpdq\Controller;
use Silex\Application;
use Symfony\Component\HttpFoundation\Request;
use Lpdq\Model\Entity\News;
class NewsController {
public function addAction( Request $request, Application $app) {
$news = new News();
$news->setTitle('test');
/*...*/
$app['orm.em']->persist($news);
$app['orm.em']->flush();
/*...*/
}
}
My problem is that my prePersist/preUpdate methods are not called when I persist my entity.
(So I get an error because my properties created and updated are null)
If I set my entity News as HasLifecycleCallbacks and put the same prePersist/Update method, they are triggered.
While I am here, I am wondering if the way I extends my entities to put pre/post and other methods are a good or bad practice?
If you have multiple entities which need same set of methods then having a base class news makes sense, if only one entity is extending the class news then it's a overkill and you can put the code in your entity class itself.
The general pattern is if you have multiple entities and all for them have created and updated field then you should create a base class and all such entities should extend it.
You need to have the annotation HasLifecycleCallbacks to enable Lifecycle callbacks. If the lifecycle events are applicable for all entities you are extending from base class then you should put in the annotation in base class otherwise put it in individual classes.
You have Lpdq\Model\Entity\News extending Lpdq\Model\News which is at least confusing.
You're also only showing partial implementation- make sure that setTitle() actually updates tracked model properties for Doctrine to identify the instance as dirty. Otherwise the flush events won't be called.

Categories