Sonata-admin cannot modify classes with inheritance - php

I am using Symfony2 and I have a hierarchy of classes. The hierarchy is pretty simple, I have a Question (the parent) and many different sub-questions. Using Sonata, I want to be able to create different types of questions, that are sub-questions. To do so, I created a hierarchy of classes as follows :
Hippy\ScavengerHuntBundle\Entity\Question:
type: entity
table: null
inheritanceType: JOINED
discriminatorColumn:
name: subClass
type: string
discriminatorMap:
blurredMultipleChoiceQuestion: BlurredMultipleChoiceQuestion
blurredTextQuestion: BlurredTextQuestion
slidingPuzzleQuestion: SlidingPuzzleQuestion
associationQuestion: AssociationQuestion
trueOrFalseQuestion: TrueOrFalseQuestion
lettersInOrderQuestion: LettersInOrderQuestion
shortTextQuestion: ShortTextQuestion
multipleChoiceQuestion: MultipleChoiceQuestion
sentenceGapQuestion: SentenceGapQuestion
fields:
id:
type: integer
id: true
generator:
strategy: AUTO
title:
type: string
length: 255
position:
type: integer
lifecycleCallbacks: { }
And I'll show you one example of a subclass
Hippy\ScavengerHuntBundle\Entity\LettersInOrderQuestion:
type: entity
table: null
fields:
description:
type: text
lifecycleCallbacks: { }
<?php
namespace Hippy\ScavengerHuntBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* LettersInOrderQuestion
*/
class LettersInOrderQuestion extends Question
{
/**
* #var string
*/
private $description;
/**
* Set description
*
* #param string $description
* #return LettersInOrderQuestion
*/
public function setDescription($description)
{
$this->description = $description;
return $this;
}
/**
* Get description
*
* #return string
*/
public function getDescription()
{
return $this->description;
}
}
At this point, everything seems to be set up properly (the database and the php classes).
Now, I want to integrate this to SonataAdmin, so I added the following in the services
sonata.admin.question:
class: Hippy\ScavengerHuntBundle\Admin\QuestionAdmin
tags:
- { name: sonata.admin, manager_type: orm, group: "Questions", label: "Question" }
arguments:
- ~
- Hippy\ScavengerHuntBundle\Entity\Question
- ~
calls:
- [ setTranslationDomain, [HippyScavengerHuntBundle]]
- [ setSubClasses, [{lettersInOrderQuestion : "Hippy\ScavengerHuntBundle\Entity\LettersInOrderQuestion"}]]
And I created a class QuestionAdmin.php
<?php
// src/Acme/DemoBundle/Admin/PostAdmin.php
namespace Hippy\ScavengerHuntBundle\Admin;
use Sonata\AdminBundle\Admin\Admin;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Form\FormMapper;
use Hippy\ScavengerHuntBundle\Entity\LettersInOderQuestion;
class QuestionAdmin extends Admin
{
// Fields to be shown on create/edit forms
protected function configureFormFields(FormMapper $formMapper)
{
$subject = $this->getSubject();
var_dump($subject);
//exit();
if ($subject instanceof LettersInOrderQuestionAdmin) {
$formMapper->add('description', 'text');
}
}
// Fields to be shown on filter forms
protected function configureDatagridFilters(DatagridMapper $datagridMapper)
{
$datagridMapper
->add('title')
;
}
// Fields to be shown on lists
protected function configureListFields(ListMapper $listMapper)
{
$listMapper
->addIdentifier('title')
;
}
}
At this point, one thing that is cool is that Sonata admin seems to recognize that I'm dealing with subclasses, take a look :
My problem is that when I try to create a lettersInOrderQuestion object, it is not recognized as a lettersInOrderQuestion but only as a Question. See here :
We can see, first via the var_dump and second because the form description is not show, that the object passed is a Question and not a LettersInOrderQuestion, even though the url is
/admin/hippy/scavengerhunt/question/create?subclass=lettersInOrderQuestion
I'm running out of ideas....
Edit1:
In Question AdminClass, in the configureFormFields method, I added
var_dump($this->getSubClasses());
and the result was the following:
array (size=1)
'lettersInOrderQuestion' => string 'Hippy\ScavengerHuntBundle\Entity
ettersInOrderQuestion' (length=56)
Therefore, it looks like there is an error in the parsing of the entity class as the name gets mixed up...

First, there is a typo in your namespace in QuestionAdmin, it should probably be
use Hippy\ScavengerHuntBundle\Entity\LettersInOrderQuestion;
and not (Oder instead of Order"
use Hippy\ScavengerHuntBundle\Entity\LettersInOderQuestion;
Secondly, also in QuestionAdmin, you are mixing the Admin class and the entity class. See here, you have:
if ($subject instanceof LettersInOrderQuestionAdmin) {
it should be, according to your code:
if ($subject instanceof LettersInOrderQuestion) {
Finally, in SonataAdmin, it looks like if you put only one subclass, the class never gets active. You have to put at least two subClasses, if not, the subclass never gets active, see here :
public function hasActiveSubClass()
{
if (count($this->subClasses) > 1 && $this->request) {
return null !== $this->getRequest()->query->get('subclass');
}
return false;
}
An issue has been opened here : https://github.com/sonata-project/SonataAdminBundle/issues/1945

Sonata admin takes as second constructor argument entity class. This class is saved in private variable of parent SonataAdminClass and can not be changed. This class use Model manager to create new instance of this entity class, in your case Question.
This Question object returns admin by method getSubject();
Admin knows nothing about your intention to use entity LettersInOderQuestion.

Related

Doctrine-PHPCR-ODM Event doesn't fire

I have a doctrine-phpcr-odm document named article,I want to slugify a field before updating each article.
The event fires for doctrine-orm entities but dosn't fire for doctrine-phpcr-odm documents!
class ArticlePreUpdateListener
{
public function preUpdate(LifecycleEventArgs $args)
{
var_dump($args);
}
}
article.pre_update.listener:
class: AppBundle\EventListener\ArticlePreUpdateListener
tags:
- { name: doctrine.event_listener, event: preUpdate}
According to Docs, Doctrine-PHPCR-ODM events works the same way as for Doctrine ORM events. The only differences are:
use the tag name doctrine_phpcr.event_listener resp.
doctrine_phpcr.event_subscriber instead of doctrine.event_listener;
expect the argument to be of class
Doctrine\Common\Persistence\Event\LifecycleEventArgs.
`/**
* #Document
*/
class Article
{
[...]
/**
* #PreUpdate
* #PrePersist
*/
public function slugifiyField()
{
$this->yourField = yourSlugifyFunction($this->yourField);
}
}
Then, add a function with a preUpdate annotation (I've added PrePersist to slugify when article is created too)
Edit : According to your comment, I removed HasLifeCycleCallback annotation, but it looks you can use Pre/PostUpdate annotations directly within document entity.

Class XXX is not a valid document or mapped super class

I'm using doctrine Doctrine MongoDB ODM 1.0.3. When trying to update document using doctrine I'm getting the following error:
Class XXX is not a valid document or mapped super class.
I have the following class for the document:
<?php
namespace Documents;
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
/**
* #ODM\Document(collection="posts")
*/
class Posts
{
/** #ODM\Id */
private $id;
/** #ODM\Field(type="string") */
private $title;
/** #ODM\EmbedMany(targetDocument="Comment") */
private $comments = array();
public function setTitle($title)
{
$this->title = $title;
}
public function getTitle()
{
return $this->title;
}
public function addComment($comment)
{
$this->comments[] = $comment;
}
public function getComments()
{
return $this->comments;
}
}
The following code is used to add new document:
$post = new \Documents\Posts();
$post->setTitle( $_POST['title'] );
$dm->persist($post);
$dm->flush();
Later I want to update the added document to add new comment for example. I use the following code:
$comment = new \Documents\Comment($_POST['comment_text']);
$dm->createQueryBuilder('Posts')
->update()
->field('comments')->push($comment)
->field('_id')->equals(new \MongoId($_POST['id']))
->getQuery()
->execute();
but getting the above mentioned error.
As you stated in your own answer you need to provide fully qualified name of class. Just wanted to add that better than to pass the string it is to use static class property like this instead: createQueryBuilder(\Documents\Posts::class); It works much better with IDEs (autocompletion, refactoring etc...)
For people getting this error in PHP 8 and above, check that you are not mixing annotations (e.g. /** #Entity */) and attributes (e.g. #[Entity]), and that you have indicated which method you are using in your config:
mappings:
App:
# pick one:
type: annotation
type: attribute
This always trips me up when I start a new project and the config defaults to annotations when I'm used to using attributes.
In case anyone else have a similar problem, you need to pass fully qualified class name to createQueryBuilder. My document classes are all inside Documents namespace so after passing it like this createQueryBuilder('\Documents\Posts') the problem is solved.
Based on the other varying answers here, it seems this error isn't super precise.
In my case the class was missing the EmbeddedDocument annotation.
namespace Foo;
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
/**
* #ODM\EmbeddedDocument
*/
class Bar { }

Pass parameter to Entity

I am new to symfony. I want to be able to cofigure administrator role name to my application. I need to do something like: (in controller)
if($this->getUser()->isAdmin()) {
//..
}
In User Entity I could define isAdmin as:
function isAdmin()
{
$this->hasRole('ROLE_ADMIN');
}
but that way, ROLE_ADMIN can't be configured. Note that I don't want to pass 'a role name' as param (or default param) to isAdmin function. I want it like i can pass object to User Entity:
public function __construct(AuthConfiguration $config)
{
$this->config = $config;
}
public function isAdmin()
{
return $this->hasRole($this->config->getAdminRoleName());
}
But how can I pass object to user entity since user creation is handled by the repository ?
You can set up custom Doctrine DBAL ENUM Type for roles using this bundle: https://github.com/fre5h/DoctrineEnumBundle
<?php
namespace AppBundle\DBAL\Types;
use Fresh\Bundle\DoctrineEnumBundle\DBAL\Types\AbstractEnumType;
class RoleType extends AbstractEnumType
{
const ROLE_USER = 'ROLE_USER';
const ROLE_ADMIN = 'ROLE_ADMIN';
const ROLE_SUPER_ADMIN = 'ROLE_SUPER_ADMIN';
const ROLE_PROJECT_OWNER = 'ROLE_PROJECT_OWNER';
/**
* #var array Readable choices
* #static
*/
protected static $choices = [
self::ROLE_USER => 'role.user',
self::ROLE_ADMIN => 'role.administrator',
self::ROLE_SUPER_ADMIN => 'role.super_administrator',
self::ROLE_PROJECT_OWNER => 'role.project_owner',
];
}
Register new type in config.yml:
doctrine:
dbal:
mapping_types:
enum: string
types:
RoleType: AppBundle\DBAL\Types\RoleType
Configure your user's role field as ENUM RoleType type:
use Fresh\Bundle\DoctrineEnumBundle\Validator\Constraints as DoctrineAssert;
...
/**
* #DoctrineAssert\Enum(entity="AppBundle\DBAL\Types\RoleType")
* #ORM\Column(name="role", type="RoleType")
*/
protected $role = RoleType::ROLE_USER;
And use it in your entity or repository or anywhere else this way:
use AppBundle\DBAL\Types\RoleType;
...
public function isAdmin()
{
$this->hasRole(RoleType::ROLE_ADMIN);
}
The constructor is only called when you create a new instance of the object with the keyword new. Doctrine does not call the constructor even when it hydrates entities.
You could potentially create your own entity hydrator and call the entity's constructor however I haven't tried this solution. It may not be as maintainable.
I want to provide an alternative which I prefer (you may not).
On all my projects, the architecture is as follow:
Controller <-> Service <-> Repository <-> Entity.
The advantage of this architecture is the use of dependency injection with services.
In your services.yml
services:
my.user:
class: Acme\HelloBundle\Service\MyUserService
arguments:
# First argument
# You could also define another service that returns
# a list of roles.
0:
admin: ROLE_ADMIN
user: ROLE_USER
In your service:
namespace Acme\HelloBundle\Service;
use Symfony\Component\Security\Core\User\UserInterface;
class MyUserService {
protected $roles = array();
public function __constructor($roles)
{
$this->roles = $roles;
}
public function isAdmin(UserInterface $user = null)
{
if ($user === null) {
// return current logged-in user
}
return $user->hasRole($this->roles['admin']);
}
}
In your controller:
// Pass a user
$this->get('my.user')->isAdmin($this->getUser());
// Use current logged-in user
$this->get('my.user')->isAdmin();
It's away from the solution you are looking for but in my opinion it seems more inline with what Symfony2 provides.
Another advantage is that you can extend the definition of an admin.
For example in my project, my user service has a isAdmin() method that has extra logic.

Sonata Admin Bundle: possible to add a child admin object that can have different parents?

I'm using doctrine inheritance mapping to enable various objects to be linked to a comment entity. This is achieved through various concrete "Threads", which have a one-to-many relationship with comments. So taking a 'Story' element as an example, there would be a related 'StoryThread' entity, which can have many comments.
That is all working fine, but I'm having troubles trying to define a CommentAdmin class for the SonataAdminBundle that can be used as a child of the parent entities. For example, I'd want to be able to use routes such as:
/admin/bundle/story/story/1/comment/list
/admin/bundle/media/gallery/1/comment/list
Does anyone have any pointers about how I can go about achieving this? I'd love to post some code extracts but I haven't managed to find any related documentation so don't really know the best place to start.
I've been trying to use the SonataNewsBundle as a reference because they've implemented a similar parent/child admin relationship between posts and comments, but it appears as though this relies on the 'comment' (child) admin class to be hardcoded to know that it belongs to posts, and it also seems as though it needs to have a direct many-to-one relationship with the parent object, whereas mine is through a separate "Thread" entity.
I hope this makes sense! Thanks for any help.
Ok I managed to get this working eventually. I wasn't able to benefit from using the $parentAssociationMapping property of the CommentAdmin class, as the parent entity of a comment is a concrete instance of the Thread entity whereas the parent 'admin' class in this case is a Story (which is linked via the StoryThread). Plus this will need to remain dynamic for when I implement comments on other types of entity.
First of all, I had to configure my StoryAdmin (and any other admin classes that will have CommentAdmin as a child) to call the addChild method:
acme_story.admin.story:
class: Acme\Bundle\StoryBundle\Admin\StoryAdmin
tags:
- { name: sonata.admin, manager_type: orm, group: content, label: Stories }
arguments: [null, Acme\Bundle\StoryBundle\Entity\Story, AcmeStoryBundle:StoryAdmin]
calls:
- [ addChild, [ #acme_comment.admin.comment ] ]
- [ setSecurityContext, [ #security.context ] ]
This allowed me to link to the child admin section from the story admin, in my case from a side menu, like so:
protected function configureSideMenu(MenuItemInterface $menu, $action, Admin $childAdmin = null)
{
// ...other side menu stuff
$menu->addChild(
'comments',
array('uri' => $admin->generateUrl('acme_comment.admin.comment.list', array('id' => $id)))
);
}
Then, in my CommentAdmin class, I had to access the relevant Thread entity based on the parent object (e.g a StoryThread in this case) and set this as a filter parameter. This is essentially what is done automatically using the $parentAssociationMapping property if the parent entity is the same as the parent admin, which it most likely will be if you aren't using inheritance mapping. Here is the required code from CommentAdmin:
/**
* #param \Sonata\AdminBundle\Datagrid\DatagridMapper $filter
*/
protected function configureDatagridFilters(DatagridMapper $filter)
{
$filter->add('thread');
}
/**
* #return array
*/
public function getFilterParameters()
{
$parameters = parent::getFilterParameters();
return array_merge($parameters, array(
'thread' => array('value' => $this->getThread()->getId())
));
}
public function getNewInstance()
{
$comment = parent::getNewInstance();
$comment->setThread($this->getThread());
$comment->setAuthor($this->securityContext->getToken()->getUser());
return $comment;
}
/**
* #return CommentableInterface
*/
protected function getParentObject()
{
return $this->getParent()->getObject($this->getParent()->getRequest()->get('id'));
}
/**
* #return object Thread
*/
protected function getThread()
{
/** #var $threadRepository ThreadRepository */
$threadRepository = $this->em->getRepository($this->getParentObject()->getThreadEntityName());
return $threadRepository->findOneBy(array(
$threadRepository->getObjectColumn() => $this->getParentObject()->getId()
));
}
/**
* #param \Doctrine\ORM\EntityManager $em
*/
public function setEntityManager($em)
{
$this->em = $em;
}
/**
* #param \Symfony\Component\Security\Core\SecurityContextInterface $securityContext
*/
public function setSecurityContext(SecurityContextInterface $securityContext)
{
$this->securityContext = $securityContext;
}
An alternative to your code for direct related entities :
public function getParentAssociationMapping()
{
// we grab our entity manager
$em = $this->modelManager->getEntityManager('acme\Bundle\Entity\acme');
// we get our parent object table name
$className = $em->getClassMetadata(get_class($this->getParent()->getObject($this->getParent()->getRequest()->get('id'))))->getTableName();
// we return our class name ( i lower it because my tables first characted uppercased )
return strtolower( $className );
}
be sure to have your inversedBy variable matching the $className in order to properly work

How to setup table prefix in symfony2

Like in question topic, how can I setup default table prefix in symfony2?
The best if it can be set by default for all entities, but with option to override for individual ones.
Having just figured this out myself, I'd like to shed some light on exactly how to accomplish this.
Symfony 2 & Doctrine 2.1
Note: I use YML for config, so that's what I'll be showing.
Instructions
Open up your bundle's Resources/config/services.yml
Define a table prefix parameter:
Be sure to change mybundle and myprefix_
parameters:
mybundle.db.table_prefix: myprefix_
Add a new service:
services:
mybundle.tblprefix_subscriber:
class: MyBundle\Subscriber\TablePrefixSubscriber
arguments: [%mybundle.db.table_prefix%]
tags:
- { name: doctrine.event_subscriber }
Create MyBundle\Subscriber\TablePrefixSubscriber.php
<?php
namespace MyBundle\Subscriber;
use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
class TablePrefixSubscriber implements \Doctrine\Common\EventSubscriber
{
protected $prefix = '';
public function __construct($prefix)
{
$this->prefix = (string) $prefix;
}
public function getSubscribedEvents()
{
return array('loadClassMetadata');
}
public function loadClassMetadata(LoadClassMetadataEventArgs $args)
{
$classMetadata = $args->getClassMetadata();
if ($classMetadata->isInheritanceTypeSingleTable() && !$classMetadata->isRootEntity()) {
// if we are in an inheritance hierarchy, only apply this once
return;
}
$classMetadata->setTableName($this->prefix . $classMetadata->getTableName());
foreach ($classMetadata->getAssociationMappings() as $fieldName => $mapping) {
if ($mapping['type'] == \Doctrine\ORM\Mapping\ClassMetadataInfo::MANY_TO_MANY
&& array_key_exists('name', $classMetadata->associationMappings[$fieldName]['joinTable']) ) { // Check if "joinTable" exists, it can be null if this field is the reverse side of a ManyToMany relationship
$mappedTableName = $classMetadata->associationMappings[$fieldName]['joinTable']['name'];
$classMetadata->associationMappings[$fieldName]['joinTable']['name'] = $this->prefix . $mappedTableName;
}
}
}
}
Optional step for postgres users: do something similary for sequences
Enjoy
Alternate answer
This is an update taking into account the newer features available in Doctrine2.
Doctrine2 naming strategy
Doctrine2 uses NamingStrategy classes which implement the conversion from a class name to a table name or from a property name to a column name.
The DefaultNamingStrategy just finds the "short class name" (without its namespace) in order to deduce the table name.
The UnderscoreNamingStrategy does the same thing but it also lowercases and "underscorifies" the "short class name".
Your CustomNamingStrategy class could extend either one of the above (as you see fit) and override the classToTableName and joinTableName methods to allow you to specify how the table name should be constructed (with the use of a prefix).
For example my CustomNamingStrategy class extends the UnderscoreNamingStrategy and finds the bundle name based on the namespacing conventions and uses that as a prefix for all tables.
Symfony2 naming strategy
Using the above in Symfony2 requires declaring your CustomNamingStragery class as a service and then referencing it in your config:
doctrine:
# ...
orm:
# ...
#naming_strategy: doctrine.orm.naming_strategy.underscore
naming_strategy: my_bundle.naming_strategy.prefixed_naming_strategy
Pros and cons
Pros:
running one piece of code to do one single task -- your naming strategy class is called directly and its output is used;
clarity of structure -- you're not using events to run code which alter things that have already been built by other code;
better access to all aspects of the naming conventions;
Cons:
zero access to mapping metadata -- you only have the context that was given to you as parameters (this can also be a good thing because it forces convention rather than exception);
needs doctrine 2.3 (not that much of a con now, it might have been in 2011 when this question was asked :-));
Simshaun's answer works fine, but has a problem when you have a single_table inheritance, with associations on the child entity. The first if-statement returns when the entity is not the rootEntity, while this entity might still have associations that have to be prefixed.
I fixed this by adjusting the subscriber to the following:
<?php
namespace MyBundle\Subscriber;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
class TablePrefixSubscriber implements EventSubscriber
{
protected $prefix = '';
/**
* Constructor
*
* #param string $prefix
*/
public function __construct($prefix)
{
$this->prefix = (string) $prefix;
}
/**
* Get subscribed events
*
* #return array
*/
public function getSubscribedEvents()
{
return array('loadClassMetadata');
}
/**
* Load class meta data event
*
* #param LoadClassMetadataEventArgs $args
*
* #return void
*/
public function loadClassMetadata(LoadClassMetadataEventArgs $args)
{
$classMetadata = $args->getClassMetadata();
// Only add the prefixes to our own entities.
if (FALSE !== strpos($classMetadata->namespace, 'Some\Namespace\Part')) {
// Do not re-apply the prefix when the table is already prefixed
if (false === strpos($classMetadata->getTableName(), $this->prefix)) {
$tableName = $this->prefix . $classMetadata->getTableName();
$classMetadata->setPrimaryTable(['name' => $tableName]);
}
foreach ($classMetadata->getAssociationMappings() as $fieldName => $mapping) {
if ($mapping['type'] == ClassMetadataInfo::MANY_TO_MANY && $mapping['isOwningSide'] == true) {
$mappedTableName = $classMetadata->associationMappings[$fieldName]['joinTable']['name'];
// Do not re-apply the prefix when the association is already prefixed
if (false !== strpos($mappedTableName, $this->prefix)) {
continue;
}
$classMetadata->associationMappings[$fieldName]['joinTable']['name'] = $this->prefix . $mappedTableName;
}
}
}
}
}
This has a drawback though;
A not wisely chosen prefix might cause conflicts when it's actually already part of a table name.
E.g. using prefix 'co' when theres a table called 'content' will result in a non-prefixed table, so using an underscore like 'co_' will reduce this risk.
Also, you can use this bundle for the new version of Symfony (4) - DoctrinePrefixBundle
I don't when to implement a solution that involved catching event (performance concern), so I have tried the Alternate Solution but it doesn't work for me.
I was adding the JMSPaymentCoreBundle and wanted to add a prefix on the payment tables.
In this bundle, the definition of the tables are in the Resources\config\doctrine directory (xml format).
I have finally found this solution:
1) copy doctrine directory containing the definitions on the table and paste it in my main bundle
2) modify the name of the tables in the definitions to add your prefix
3) declare it in your config.yml, in the doctrine/orm/entity manager/mapping section (the dir is the directory where you have put the modified definitions):
doctrine:
orm:
...
entity_managers:
default:
mappings:
...
JMSPaymentCoreBundle:
mapping: true
type: xml
dir: "%kernel.root_dir%/Resources/JMSPayment/doctrine"
alias: ~
prefix: JMS\Payment\CoreBundle\Entity
is_bundle: false
tested with Symfony 6 :
Create a class that extends Doctrine's UnderscoreNamingStrategy and handles the prefix :
<?php
# src/Doctrine/PrefixedNamingStrategy.php
namespace App\Doctrine;
use Doctrine\ORM\Mapping\UnderscoreNamingStrategy;
class PrefixedNamingStrategy extends UnderscoreNamingStrategy
{
private const PREFIX = 'sf';
public function classToTableName($className)
{
$underscoreTableName = parent::classToTableName($className);
return self::PREFIX . '_' . $underscoreTableName;
}
}
and configure doctrine to use it :
# config/packages/doctrine.yaml
doctrine:
orm:
naming_strategy: 'App\Doctrine\PrefixedNamingStrategy'
#simshaun answer is good, but there is a problem with Many-to-Many relationships and inheritance.
If you have a parent class User and a child class Employee, and the Employee own a Many-to-Many field $addresses, this field's table will not have a prefix.
That is because of:
if ($classMetadata->isInheritanceTypeSingleTable() && !$classMetadata->isRootEntity()) {
// if we are in an inheritance hierarchy, only apply this once
return;
}
User class (parent)
namespace FooBundle\Bar\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* User
*
* #ORM\Entity()
* #ORM\Table(name="user")
* #ORM\InheritanceType("SINGLE_TABLE")
* #ORM\DiscriminatorColumn(name="type", type="string")
* #ORM\DiscriminatorMap({"user" = "User", "employee" = "\FooBundle\Bar\Entity\Employee"})
*/
class User extends User {
}
Employee class (child)
namespace FooBundle\Bar\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* User
*
* #ORM\Entity()
*/
class Employee extends FooBundle\Bar\Entity\User {
/**
* #var ArrayCollection $addresses
*
* #ORM\ManyToMany(targetEntity="\FooBundle\Bar\Entity\Adress")
* #ORM\JoinTable(name="employee_address",
* joinColumns={#ORM\JoinColumn(name="employee_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="address_id", referencedColumnName="id")}
* )
*/
private $addresses;
}
Address class (relation with Employee)
namespace FooBundle\Bar\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* User
*
* #ORM\Entity()
* #ORM\Table(name="address")
*/
class Address {
}
With the original solution, if you apply pref_ prefixe to this mapping, you will end up with tables :
pref_user
pref_address
employee_address
Solution
A solution can be to modify, in the answer of #simshaun, the point 4 like this:
Create MyBundle\Subscriber\TablePrefixSubscriber.php
<?php
namespace MyBundle\Subscriber;
use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
class TablePrefixSubscriber implements \Doctrine\Common\EventSubscriber
{
protected $prefix = '';
public function __construct($prefix)
{
$this->prefix = (string) $prefix;
}
public function getSubscribedEvents()
{
return array('loadClassMetadata');
}
public function loadClassMetadata(LoadClassMetadataEventArgs $args)
{
$classMetadata = $args->getClassMetadata();
// Put the Many-yo-Many verification before the "inheritance" verification. Else fields of the child entity are not taken into account
foreach($classMetadata->getAssociationMappings() as $fieldName => $mapping) {
if($mapping['type'] == \Doctrine\ORM\Mapping\ClassMetadataInfo::MANY_TO_MANY
&& array_key_exists('name', $classMetadata->associationMappings[$fieldName]['joinTable']) // Check if "joinTable" exists, it can be null if this field is the reverse side of a ManyToMany relationship
&& $mapping['sourceEntity'] == $classMetadata->getName() // If this is not the root entity of an inheritance mapping, but the "child" entity is owning the field, prefix the table.
) {
$mappedTableName = $classMetadata->associationMappings[$fieldName]['joinTable']['name'];
$classMetadata->associationMappings[$fieldName]['joinTable']['name'] = $this->prefix . $mappedTableName;
}
}
if($classMetadata->isInheritanceTypeSingleTable() && !$classMetadata->isRootEntity()) {
// if we are in an inheritance hierarchy, only apply this once
return;
}
$classMetadata->setTableName($this->prefix . $classMetadata->getTableName());
}
}
Here we handle the Many-to-Many relationship before verifying if the class is the child of an inheritance, and we add $mapping['sourceEntity'] == $classMetadata->getName() to add the prefix only one time, on the owning entity of the field.

Categories