Map a discriminator column to a field with Doctrine 2 - doctrine-orm

In my project I have several class table inheritances like this:
namespace MyProject\Model;
/**
* #Entity
* #InheritanceType("JOINED")
* #DiscriminatorColumn(name="discr", type="string")
* #DiscriminatorMap({"person" = "Person", "employee" = "Employee"})
*/
class Person
{
// ...
}
/** #Entity */
class Employee extends Person
{
// ...
}
I have a method which converts entities to arrays based on the fields which have public getters. The problem here is that I lose the inheritance information in my array because the discriminator value isn't stored in a field.
So what I tried was the following, hoping doctrine would automatically set $disc:
class Person
{
// can I automatically populate this field with 'person' or 'employee'?
protected $discr;
public function getDiscr() { return $this->discr; }
public function setDiscr($disc) { $this->discr; }
// ...
}
Is there a way to make this work in doctrine? Or would I need to read the class metadata in my entity-to-array method?

Sadly, there is no documented way to map the discr column to an entity. That's because the discr column is really part of the database and not the entity.
However, it's quite common to just put the discr value directly in your class definition. It's not going to change and you will always get the same class for the same value anyways.
class Person
{
protected $discr = 'person';
class Employee extends Person
{
protected $discr = 'employee';

Here's a small example of what I have in one of my ZF2 projects (using Doctrine MongoDB ODM):
// an instance of your entity
$entity = ...;
/** #var \Doctrine\ODM\MongoDB\DocumentManager $documentManager */
$documentManager = $serviceManager->get('DocumentManager');
/** #var \Doctrine\ODM\MongoDB\Mapping\ClassMetadataFactory $factory */
$factory = $documentManager->getMetadataFactory()
/** #var \Doctrine\ODM\MongoDB\Mapping\ClassMetadata $metadata */
$metadata = $factory->getMetadataFor(get_class($object));
if ($metadata->hasDiscriminator()) {
// assuming $data is result of the previous extraction
$data[$metadata->discriminatorField] = $metadata->discriminatorValue;
}
What I have done is I've implemented a custom interface DiscriminatorAwareInterface and I only apply the checks to classes that implement it (in your case it would be the class that all "discriminated" classes extend.
As a result I end up with code that looks like this:
// add value of the discrinimator field to entities that support it
if ($object instanceof DiscriminatorAwareInterface) {
/** #var \Doctrine\ODM\MongoDB\Mapping\ClassMetadata $metadata */
$metadata = $factory->getMetadataFor(get_class($object));
if ($metadata->hasDiscriminator()) {
$data[$metadata->discriminatorField] = $metadata->discriminatorValue;
}
}
I'm pretty sure it will be the same if you use the standard ORM, except instead of a document manager you will have entity manager.

Just got this problem and solved it without defining the discriminator as a real member:
abstract class MyEntity {
const TYPE_FOO = 'foo';
const TYPE_BAR = 'bar';
const TYPE_BUZ = 'buz';
...
/**
* #return string
*/
public function getMyDiscriminator()
{
$myDiscriminator = null;
switch (get_class($this)) {
case MyEntityFoo::class:
$myDiscriminator = self::TYPE_FOO;
break;
case MyEntityBar::class:
$myDiscriminator = self::TYPE_BAR;
break;
case MyEntityBuz::class:
$myDiscriminator = self::TYPE_BUZ;
break;
}
return $myDiscriminator;
}
...
}
class MyEntityFoo extends MyEntity {}
class MyEntityBar extends MyEntity {}
class MyEntityBuz extends MyEntity {}

You can use the following solution:
`$`$metadata = \Doctrine\ORM\Mapping\ClassMetadata((string)$entityName);
print_r($metadata->discriminatorValue);`

Related

Filtering a Collection using Criteria targeting an Embeddable object

Let's say you have a class Title, and the title is translated in multiple languages using TitleTranslation classes.
To indicate which language the title is translated in, each translation has a Locale value object.
For readability, I am attempting to provide the Title class with a getTitle(Locale $locale) method, returning the correct translation.
The easy way to do this would be to loop over all translations, and check each translation's locale.
However, I would like to accomplish this using Criteria, so only a single TitleTranslation will be fetched and hydrated.
To illustrate the case, a simplified version of the classes I'm working with:
Title:
/** #ORM\Entity #ORM\Table */
class Title
{
/**
* #ORM\OneToMany(targetEntity="TitleTranslation", mappedBy="element")
*/
private $translations;
}
TitleTranslation:
/** #ORM\Entity #ORM\Table */
class TitleTranslation
{
/**
* #ORM\ManyToOne(targetEntity="Title", inversedBy="translations")
*/
private $title;
/**
* #ORM\Column(type="string")
*/
private $translation;
/**
* #ORM\Embedded(class="Locale")
*/
private $locale;
public function getTranslation() : string
{
return $this->translation;
}
}
Locale:
/** #Embeddable */
class Locale
{
/** #ORM\Column(type="string")
private $locale;
public function __toString()
{
return $this->locale;
}
}
I have made the following attempts, all of which are unsuccessful:
public function getTitle(Locale $locale)
{
$localeCriteria = Criteria::create()->where(Criteria::expr()->eq('locale', $locale));
/** #var TitleTranslation | bool $translation */
$translation = $this->translations->matching($translationCriteria)->first();
return $translation ? $translation->getTranslation() : null;
}
This approach fails with an ORMException "Unrecognized field: locale", which seems normal, as Embeddables should be queried as "locale.locale" (field in containing class.field in VO).
However, using this notation:
$localeCriteria = Criteria::create()->where(Criteria::expr()->eq('locale.locale', $locale));
Fails with Undefined property: TitleTranslation::$locale.locale
Am I missing something, or is this approach simply not possible?

Doctrine: can't removeElement from inverse side of the relation

I have a ManyToMany relation (an offer can have many banners, and a banner can belong to many offers). I don't understand why this works:
$banner->getOffers()->removeElement($this->offer);
But this doesn't:
$this->offer->getBanners()->removeElement($banner);
These are my entities:
Class Banner
{
/**
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\Offer", inversedBy="banners")
*/
private $offers;
public function __construct()
{
$this->offers = new ArrayCollection();
}
public function getOffers(): Collection
{
return $this->offers;
}
}
--
Class Offer
{
/**
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\Banner", mappedBy="offers")
* #ORM\JoinColumn
*/
private $banners;
public function __construct()
{
$this->banners = new ArrayCollection();
}
public function getBanners(): Collection
{
return $this->banners;
}
}
the removeElement() method returns true:
I tried to persist $this->offer and even $banner, but nothing changed, the row isn't deleted from the banner_offer table.
I'm on Doctrine 2.7.1, Symfony 3.1.7
What am I doing wrong?

How do I order by a property that isn't a DB column using Doctrine?

When defining a relationship, there is a property on the related model (not a DB column), but I would like to sort by it (in the #OrderBy annotation).
I have a base model that is extended using single table inheritance. The property in question is basically an order property that is specified in each child class, but is not saved to the DB.
(I don't want to add an order column to the DB table, since the ordering depends purely on which child class the discriminator is mapped to. There is already a unique constraint so that each child class can be used no more than once in the relationship.)
Here's a really simplified version of my code...
Base entity:
/**
* #ORM\Entity
* #ORM\Table(name="base")
* #ORM\InheritanceType("SINGLE_TABLE")
* #ORM\DiscriminatorColumn(name="class_name", type="string")
*
* #ORM\DiscriminatorMap({
* "Base" = "Models\Base",
* "ChildA" = "Models\ChildB",
* "ChildB" = "Models\ChildA"
* })
**/
class Base
{
/** #ORM\Column(type="string") **/
protected $class_name;
/**
* #ORM\ManyToOne(targetEntity="Related", inversedBy="collection")
**/
protected $related;
// this is just a plain ol' property (not in the DB)
protected $order;
public function getClassName()
{
return $this->class_name;
}
}
Children:
/**
* #ORM\Entity
* #ORM\Table(name="child_a")
**/
class ChildA extends Base
{
$order = 1;
}
/**
* #ORM\Entity
* #ORM\Table(name="child_b")
**/
class ChildB extends Base
{
$order = 2;
}
Related entity:
/**
* #ORM\Entity
* #ORM\Table(name="related")
**/
class Related
{
/**
* #ORM\OneToMany(targetEntity="Base", mappedBy="related")
* #ORM\OrderBy({"order"="ASC"})
**/
protected $collection;
public function getCollection()
{
$em = App::make('Doctrine\ORM\EntityManagerInterface');
// map each Base instance to the appropriate child class
return $this->collection->map(function ($base) use ($em) {
$class_name = $base->getClassName();
return $em->find($class_name, $base->getId());
});
}
}
Is it possible to use the order property for ordering the collection relationship? (Ordering based on class_name using a switch-like construct would also be valid, but I haven't found any way to do that either, and it would be harder to maintain.)
Thanks in advance!
The directive beginning with ORM is very much telling Doctrine you're doing referencing a property that has a relationship with a table field. You can't use ORM directives on fields that don't exist. Doctrine annotations: OrderBy
You would have to implement this in a function, best in the model itself (within your getCollection() function), or if you're using a framework like Symfony place it in a function of the repository class for this entity. You'd have to use PHP sorting functions to do this. SQL/DQL won't work either because the property isn't related to a field in the table.

ZF2 Doctrine Entity findAll

Being able to use Doctrine speeds up a lot of things however it feels somewhat clunky to me having to set / use the entity manager in all of my controllers. I would prefer to have all of the database logic in 1 specific module. Perhaps I'm just thinking about this the wrong way, and someone can point me in the right direction.
Currently I have my Entity which functions just fine and I can do insertions into the database fine with the following
namespace Manage\Controller;
use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
class ViewController extends AbstractActionController {
public function somethingAction(){
$objectManager = $this->getServiceLocator()->get('Doctrine\ORM\EntityManager');
$user = new \Manage\Entity\User();
$user->setname('foo');
$user->settitle('bar');
$objectManager->persist($user);
$objectManager->flush();
}
}
However whenever I want to select something from the database I have to make sure to add
use Doctrine\ORM\EntityManager;
And then the following list of Controller functions...
/**
* #var EntityManager
*/
protected $entityManager;
/**
* Sets the EntityManager
*
* #param EntityManager $em
* #access protected
* #return PostController
*/
protected function setEntityManager(EntityManager $em) {
$this->entityManager = $em;
return $this;
}
/**
* Returns the EntityManager
*
* Fetches the EntityManager from ServiceLocator if it has not been initiated
* and then returns it
*
* #access protected
* #return EntityManager
*/
protected function getEntityManager() {
if (null === $this->entityManager) {
$this->setEntityManager($this->getServiceLocator()->get('Doctrine\ORM\EntityManager'));
}
return $this->entityManager;
}
Once I have added all of that I can now do a query in my getsomethingAction like so...
public function getsomethingAction() {
$repository = $this->getEntityManager()->getRepository('Manage\Entity\User');
$list = $repository->findAll();
var_dump($list);
return new ViewModel();
}
To me that feels very clunky... I can do an insert without needing all the extra functions but I cannot do a select? Is it possible to extend the Entity class in order to get the find / findAll etc functions that is provided by calling $repository = $this->getEntityManager()->getRepository('Manage\Entity\User'); directly inside the entity?
By that I mean I would prefer to be able to run the find directly on the entity as I would when I set the data... like below:
public function getsomethingAction(){
$list = new \Manage\Entity\User();
$l = $list->findAll();
var_dump($l);
return new ViewModel();
}
Ok so my main objective so far has been to move the complex logic out of the controllers into a re-usable model. So with this example answer I'm creating an interface where the complex logic would live however it also allows me to still use the model in a controller to get data from the database... here is the Model...
namespace Manage\Model;
use Doctrine\ORM\EntityManager;
class ApiInterface {
/**
* #var EntityManager
*/
protected $entityManager;
protected $sl;
/**
* Sets the EntityManager
*
* #param EntityManager $em
* #access protected
* #return PostController
*/
protected function setEntityManager(EntityManager $em) {
$this->entityManager = $em;
return $this;
}
/**
* Returns the EntityManager
*
* Fetches the EntityManager from ServiceLocator if it has not been initiated
* and then returns it
*
* #access protected
* #return EntityManager
*/
protected function getEntityManager() {
if (null === $this->entityManager) {
$this->setEntityManager($this->sl->get('Doctrine\ORM\EntityManager'));
}
return $this->entityManager;
}
public function __construct($ServiceLocator) {
$this->sl = $ServiceLocator;
}
public function get() {
$repository = $this->getEntityManager()->getRepository('Manage\Entity\ApiList');
return $repository;
}
public function set() {
return new \Manage\Entity\ApiList();
}
public function save($data) {
$objectManager = $this->sl->get('Doctrine\ORM\EntityManager');
$objectManager->persist($data);
$objectManager->flush();
}
public function doComplexLogic($foo,$bar){
// Can now use both set() and get() to inspect/modify/add data
}
}
So now inside my controller I can do something that gets some basic data from the table like:
public function getapiAction() {
$api = new \Manage\Model\ApiInterface($this->getServiceLocator());
var_dump($api->get()->findAll());
return new ViewModel();
}
And to quickly set data from a controller I can do:
public function setapiAction() {
$apiInterface = new \Manage\Model\ApiInterface($this->getServiceLocator());
$api= $apiInterface->set();
$user->setfoo('blah');
$user->setbar('moo');
$apiInterface->save($api);
return new ViewModel();
}
And it also allows me to run complex logic from the controller by taking the complexity out of the controller like so...
public function complexAction(){
$foo = $this->params()->fromQuery();
$bar = $this->params()->fromPost();
$apiInterface = new \Manage\Model\ApiInterface($this->getServiceLocator());
$apiInterface->doComplexLogic($foo, $bar);
}
Please let me know in comments if this answer would be the proper way to do things, I realize it's very simple and generic but I wanted to keep it that way so others can understand what / why and if this is a good approach / not etc.

Doctrine 2 Can't Seem to Remove Many to Many Relationships

I have the following setup "Many Users can have Many Projects (Collaborators)"
/**
* #Entity #HasLifeCycleCallbacks
* #Table(name="projects")
*/
class Project implements \Zend_Acl_Resource_Interface {
/**
* #ManyToMany(targetEntity="User", mappedBy="projects")
* #OrderBy({"displayName" = "ASC", "username" = "ASC"})
*/
protected $collaborators;
..
}
/**
* #Entity
* #Table(name="users")
*/
class User implements \Zend_Acl_Role_Interface {
/**
* #ManyToMany(targetEntity="Project", inversedBy="collaborators")
*/
protected $projects;
...
}
I tried to remove a collaborator using the following
$user = Application_DAO_User::findById($this->_getParam('userid'));
$proj = Application_DAO_Project::getProjectById($this->_getParam('id'));
Application_DAO_Project::removeCollaborator($proj, $user); // <---
// Application_DAO_User
public static function findById($id) {
return self::getStaticEm()->find('Application\Models\User', $id);
}
// Application_DAO_Project
public static function getProjectById($id) {
return self::getStaticEm()->find('Application\Models\Project', $id);
}
public static function removeCollaborator(Project $proj, User $collaborator) { // <---
$proj->getCollaborators()->remove($collaborator);
$collaborator->getProjects()->remove($proj);
self::getStaticEm()->flush();
}
And there isn't any errors but the database stays the same ...
This may be well over due but was just experiencing the same problem myself... According to the doctrine 2 documents, the function ArrayCollection->remove($i) is for removing by array index.
What you are after is:
getCollaborators()->removeElement($collaborator);
I went round in circles trying to figure this out until I realised that for this to work:
getCollaborators()->removeElement($collaborator);
$collaborator would have to be the actual object from the collaborators ArrayCollection. That is, if you pass in a new Collaborator object with the same parameters it won't remove it. That's because ArrayCollection uses array_search to look for the object you want to remove.
Hope that saves someone else a few hours...