I have this simple entity for example:
<?php
namespace App\Entity\Creator;
use App\Repository\Creator\ActivityContactRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: ActivityContactRepository::class)]
#[ORM\Index(columns: ['contact_id'])]
class ActivityContact
{
#[ORM\Id]
#[ORM\ManyToOne(targetEntity: Activity::class, cascade: ['persist'])]
private Activity $activity;
#[ORM\Id]
#[ORM\Column(name: 'contact_id', type: 'integer')]
private int $contactId;
public function __construct(Activity $activity, int $contactId)
{
$this->activity = $activity;
$this->contactId = $contactId;
}
public function getActivity(): Activity
{
return $this->activity;
}
public function setActivity(Activity $activity): void
{
$this->activity = $activity;
}
public function getContactId(): int
{
return $this->contactId;
}
}
When I generate a "diff" migration, doctrine automatically creates a query to add a MariaDB INDEX for the column "activity_id".
In my case, this index is not useful and I would like to remove it, without just deleting it from the migration.
Is there a way to specify to not create this index?
Thanks,
Short answer: No you can't.
An index in activity_id field is being created as you have a ManyToOne relation and a foreign key must be added, so that's why an index is created, to create the foreign key on it, and you cannot undo this behaviour.
You will have then one primary index with activity_id and contact_id and another index for activity_id with a foreign key on it.
Yes, you could use the primary index to hold the foreign key, but doctrine does not use it (as far as I know).
If you want to get rid of this index, you have to remove the ManyToOne relation. You can create a field called $activity_id, and then you will have to manually fetch the activity entity if you want to retrieve it.
Don't worry about the memory used by it, if you have issues with memory, I can assure you that the index created will not be the source of the issues.
Also, if you are using doctrine migrations, don't remove the creation of the index in the migrations file, because it will be generated again and again on each diff you generate.
#TEOL I think the only option that you need is to add an SQL query regarding the drop index in the migration file manually. There is no option available in Doctrine.
Remove attribute #[ORM\Index(...)] above the class name definition. Although you are including that property into an ID of the Entity, the index may be created over the column.
I'm not at home, so i can't test this, but could you try the following : On the class level, add a #ORM\Table and set its indexes to an empty array. Then above your $contactId property, add ORM\Index(columns: ['contact_id'])
<?php
namespace App\Entity\Creator;
use App\Repository\Creator\ActivityContactRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: ActivityContactRepository::class)]
#[ORM\Table(indexes: [])]
class ActivityContact
{
#[ORM\Id]
#[ORM\ManyToOne(targetEntity: Activity::class, cascade: ['persist'])]
private Activity $activity;
#[ORM\Id]
#[ORM\Column(name: 'contact_id', type: 'integer')]
#[ORM\Index(columns: ['contact_id'])]
private int $contactId;
public function __construct(Activity $activity, int $contactId)
{
$this->activity = $activity;
$this->contactId = $contactId;
}
public function getActivity(): Activity
{
return $this->activity;
}
public function setActivity(Activity $activity): void
{
$this->activity = $activity;
}
public function getContactId(): int
{
return $this->contactId;
}
}
Related
I've got an entity called Logs that has a ManyToOne relation to an HourlyRates entity. Both Logs and HourlyRates have date properties. When adding a log with a specific date, an hourlyRate is assigned to it if the log-date fits within the rate's time range. I'm using the Doctrine Extensions Bundle, so the data in each entity can be soft-deleted.
What needs to be done:
After soft-deleting an HourlyRate the related Log has to be updated, so that the nearest existing past HourlyRate takes the place of the deleted one.
I tried to use preSoftDelete, postSoftDelete, preRemove and postRemove methods inside an HourlyRate entity listener. The code was being executed and the setters were working properly, but the database hasn't been updated in any of said cases. An "EntityNotFoundException" was being thrown everytime.
My second approach was to use the preRemove event along with setting the cascade option to "all" by using annotations in the HourlyRate class. As a result, soft-deleting an hourlyRate caused soft-deleting of the related log.
The Log entity:
class Log
{
use SoftDeleteableEntity;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\HourlyRate", inversedBy="logs")
* #ORM\JoinColumn(nullable=false)
*/
private $hourlyRate;
public function setHourlyRate(?HourlyRate $hourlyRate): self
{
$this->hourlyRate = $hourlyRate;
return $this;
}
}
The HourlyRate entity:
class HourlyRate
{
use SoftDeleteableEntity;
//other code
/**
* #ORM\OneToMany(targetEntity="App\Entity\Log", mappedBy="hourlyRate", cascade={"all"})
*/
private $logs;
}
The HourlyRate entity listener:
class HourlyRateEntityListener
{
public function preRemove(HourlyRate $hourlyRate, LifecycleEventArgs $args)
{
$entityManager = $args->getObjectManager();
/** #var HourlyRateRepository $HRrepo */
$HRrepo = $entityManager->getRepository(HourlyRate::class);
foreach ($hourlyRate->getLogs() as $log)
{
$rate = $HRrepo->findHourlyRateByDate($log->getDate(), $log->getUser(), $hourlyRate);
$log->setHourlyRate($rate);
}
}
}
The repository method:
class HourlyRateRepository extends ServiceEntityRepository
{
public function findHourlyRateByDate(?\DateTimeInterface $datetime, User $user, ?HourlyRate $ignore = null): ?HourlyRate
{
$qb = $this->createQueryBuilder('hr')
->where('hr.date <= :hr_date')
->andWhere('hr.user = :user')
->orderBy('hr.date', 'DESC')
->setMaxResults(1)
->setParameters(array('hr_date' => $datetime, 'user' => $user));
//ignore the "deleted" hourlyRate
if($ignore){
$qb->andWhere('hr.id != :ignored')
->setParameter('ignored', $ignore->getId());
}
return $qb->getQuery()
->getOneOrNullResult()
;
}
}
Thank you in advance for any of your help.
EDIT:
Okay so after a whole week of trials and errors i finally managed to achieve the result I wanted.
I removed the One-To-Many relation between the hourlyRates and the logs from the entities, but left the $hourlyRate property inside the Log class. Then I got rid of the HourlyRateEntityListener and the preRemove() method from the LogEntityListener. Instead, I implemented the postLoad() method:
class LogEntityListener
{
public function postLoad(Log $log, LifeCycleEventArgs $args)
{
$entityManager = $args->getObjectManager();
$HRrepo = $entityManager->getRepository(HourlyRate::class);
/** #var HourlyRateRepository $HRrepo */
$rate = $HRrepo->findHourlyRateByDate($log->getDate(), $log->getUser());
$log->setHourlyRate($rate);
}
}
This approach allows me to set the proper hourlyRate for each log without involving the database. Idk if this solution is acceptable though.
So I have this custom Doctrine type
namespace App\Doctrine\Types;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\TextType;
class MyType extends TextType
{
private $prefix='';
public function getName()
{
return 'my_type';
}
public function setPrefix(string $prefix)
{
$this->prefix=$prefix;
}
}
I registerd in in the config/packages/doctrine.yml:
doctrine:
dbal:
types:
my_type: App\Doctrine\Types\MyType
Then in Kernel boot() I'm trying to add some parameters to this type:
public function boot() {
parent::boot();
$myType=Type::getType('my_type');
$myType->setPrefix('abc');
}
This works perfectly the first time I run the app. The prefix is set for the type and can be used through the whole app. However, the second time I get an Exception:
Unknown column type "encrypted_text" requested. Any Doctrine
type that you use has to be registered with
\Doctrine\DBAL\Types\Type::addType(). You can get a list of all the
known types with \Doctrine\DBAL\Types\Type::getTypesMap(). If this
error occurs during database introspection then you might have
forgotten to register all database types for a Doctrine Type. Use
AbstractPlatform#registerDoctrineTypeMapping() or have your custom
types implement Type#getMappedDatabaseTypes(). If the type name is
empty you might have a problem with the cache or forgot some mapping
information.
I then changed boot() to :
public function boot() {
parent::boot();
if (!Type::hasType('my_type')) {
Type::addType('my_type', 'App\Doctrine\Types\MyType');
}
$myType=Type::getType('my_type');
$myType->setPrefix('abc');
}
Now the exception is gone, but the prefix is not set. I know the exceptions gives me information about what to do but I really don't know where to start.
Can anyone point me in the right direction?
For now I fixed it by removing it from config/packages/doctrine.yml so it's not registered there anymore. In Kernel I can now load it:
public function boot() {
parent::boot();
if (!Type::hasType('my_type')) {
Type::addType('my_type', 'App\Doctrine\Types\MyType');
}
$myType = Type::getType('my_type');
$myType->setPrefix('abc');
}
I still can't really understand why this works before building the cache but not once the cache is build. But well, I can continue now.
If someone has a better answer, I'd be more than happy to accept it.
#This is may answer!!!
**IMPORTANTE!! Because in Symfony/PHP (NO Object Oriented Language) don't storaging a state it's need make this form to mapping an ENUM type or any Custom Type similary to ENUM.**
For customize and mapping you types as Enum in DB table, you need to have a higher version Symfony >= 4.4 for to use this Bundle.
Execute it in your project (command prompt)
composer req fresh/doctrine-enum-bundle
Link example --> [https://github.com/fre5h/DoctrineEnumBundle][1] .
I made do this and it's ok for my app.
1
So, copy this class from this link
[https://github.com/fre5h/DoctrineEnumBundle/blob/master/DBAL/Types/AbstractEnumType.php][1]
and storage it in src/DBAL/Types =(IF NOT EXIST, CREATE IT), in your project Symfony => 4.4 o higher version.
#This is your Base class type.
2
Create this Table in Your Database (in my case I'm using MySql DB):
CREATE TABLE players (
id INT AUTO_INCREMENT NOT NULL,
position ENUM('PG', 'SG', 'SF', 'PF', 'C') NOT NULL,
PRIMARY KEY(id)
) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = INNODB
*****************
3
Create an Entity class for mapping table (Players) from your Databae inside your project level directory.
D:SYMFONY\app\myApp
php bin/console doctrine:mapping:import "App\Entity" annotation --path=src/Entity --filter="Players"
4
Create getter and setters for your new Entity:
D:SYMFONY\app\myApp
php bin/console make:entity --regenerate App
5
Generate your CRUD operation for Entty class + frontend.
D:SYMFONY\app\myApp
php bin/console make:entity --regenerate App
6
Create your custom Type class BasketballPositionType
<?php
namespace App\DBAL\Types;
use Fresh\DoctrineEnumBundle\DBAL\Types\AbstractEnumType;
final class BasketballPositionType extends AbstractEnumType
{
public const POINT_GUARD = 'PG';
public const SHOOTING_GUARD = 'SG';
public const SMALL_FORWARD = 'SF';
public const POWER_FORWARD = 'PF';
public const CENTER = 'C';
protected static $choices = [
self::POINT_GUARD => 'Point Guard',
self::SHOOTING_GUARD => 'Shooting Guard',
self::SMALL_FORWARD => 'Small Forward',
self::POWER_FORWARD => 'Power Forward',
self::CENTER => 'Center'
];
}
7
Go inside this path in your Symfony project at
D:SYMFONY\app\myApp\config\packages
and create this class php : doctrime.php.
This class it's necesary for to Register your new Type.
Can you find more information at link for register new type in synfony
[https://symfony.com/doc/current/doctrine/dbal.html][1]
so - REGISTER YOUR NEW TYPE
Copy this fragment of code end paste in doctrime.php class:
<?php
$container->loadFromExtension('doctrine', [
'dbal' => [
'mapping_types' => [
'enum' => 'string', /////////mapping enum to as stringa
],
'types' => [
'BasketballPositionType' => App\DBAL\Types\BasketballPositionType::class,
],
],
]);
So, bindig ENUM type db in String and your new Type is "BasketballPositionType" !!!!
8
Now, use your Custom Type in your Entity Class mapping 'Players'.
<?php
namespace App\Entity;
** very important
use App\DBAL\Types\BasketballPositionType;
use Fresh\DoctrineEnumBundle\Validator\Constraints as DoctrineAssert;
use Doctrine\ORM\Mapping as ORM;
/**
* Players
*
* #ORM\Table(name="players")
* #ORM\Entity
*/
class Players
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="position", type="BasketballPositionType", length=255, nullable=false)
* #DoctrineAssert\Enum(entity="App\DBAL\Types\BasketballPositionType")
*/
private $position;
public function getId(): ?int
{
return $this->id;
}
public function getPosition(): ?string
{
return $this->position;
}
public function setPosition(string $position): self
{
$this->position = $position;
return $this;
}
}
RUN
####################### RUM #######################
Run you Symfony embeded server
[ symfony serve ]
And go later in browser at https://127.0.0.1:8000/players
I'm use CMDER prompt command because it has Git incorporated.
It's very easy!!
[1]: https://symfony.com/doc/current/doctrine/dbal.html
I am new for laravel model unit testing.so please look and suggest me what i am doing wrong.my code is given below.I have 2 models User and UserState.
Model User
public function state()
{
return $this->hasOne('UserState');
}
Model UserState
public function user()
{
return $this->belongsTo('User');
}
now i am writing unit test for UserState. which is given below :
UnitTest UserStateModelTest
public function testUserRelationIsTrue(){
$user = new User();
$user->username = 'testusername';
$user->save();
$this->assertEquals($user->user_id, $user->state->id);
}
during test run by phpunit it generate error
Illuminate\Database\QueryException: SQLSTATE[23000]: Integrity constraint violation:
1452 Cannot add or update a child row: a foreign key constraint fails
If you really want to test a relationship method, you can do it without even saving a model to the database.
You still need to use the RefreshDatabase trait (or the DatabaseMigrations trait) or else the models won't be mapped to any table.
# tests/Unit/ParentTest.php
/**
* Test Parent has HasMany relationship with Child model
* #test
*/
public function has_many_children_with_parent_id_fk()
{
$parent = new Parent;
$foreign_key = 'parent_id';
// Get the relationship object, not the data collection
$relationship = $parent->children();
$related_model = $relationship->getRelated();
// Assert this is a HasMany relationship
$this->assertInstanceOf(HasMany::class, $relationship);
// Assert the related model is Child
$this->assertInstanceOf(Child::class, $related_model);
// Assert the foreign key is the one we specified
// (This can be useful if you do not use the default one)
$this->assertEquals($foreign_key, $relationship->getForeignKeyName());
// Assert the foreign key is a column
// of the database table mapped by the Child model
$this->assertTrue(Schema::hasColumns($related_model->getTable(), array($foreign_key)));
}
# tests/Unit/ChildTest.php
/**
* Test Child has belongsTo relationship with Parent model
* #test
*/
public function belongs_to_parent_with_parent_id_fk()
{
$child = new Child;
$foreign_key = 'parent_id';
// Get the relationship object, not the data collection
$relationship = $child->parent();
$related_model = $relationship->getRelated();
// Assert this is a BelongsTo relationship
$this->assertInstanceOf(BelongsTo::class, $relationship);
// Assert the related model is Parent
$this->assertInstanceOf(Parent::class, $related_model);
// Assert the foreign key is the one we specified
// (This can be useful if you do not use the default one)
$this->assertEquals($foreign_key, $relationship->getForeignKeyName());
// Assert the foreign key is a column
// of the database table mapped by the Child model
$this->assertTrue(Schema::hasColumns($relationship->getParent()->getTable(), array($foreign_key)));
}
This is a bit of a handful but you can make a custom assertion to encapsulate all of this in the TestCase all tests files extend. The following methods suited my needs
# tests/TestCase.php
public function assertHasManyUsing($related_model, $relationship, $foreign_key)
{
$this->assertInstanceOf(HasMany::class, $relationship);
$this->assertInstanceOf($related_model, $relationship->getRelated());
$this->assertEquals($foreign_key, $relationship->getForeignKeyName());
$this->assertTrue(Schema::hasColumns($relationship->getRelated()->getTable(), array($foreign_key)));
}
public function assertBelongsToUsing($related_model, $relationship, $foreign_key)
{
$this->assertInstanceOf(BelongsTo::class, $relationship);
$this->assertInstanceOf($related_model, $relationship->getRelated());
$this->assertEquals($foreign_key, $relationship->getForeignKeyName());
$this->assertTrue(Schema::hasColumns($relationship->getParent()->getTable(), array($foreign_key)));
}
And now, refactoring the tests look like this
# tests/Unit/ParentTest.php
/**
* Test Parent has HasMany relationship with Child model
* #test
*/
public function has_many_children_with_parent_id_fk()
{
$parent = new Parent;
$this->assertHasManyUsing(Child::class, $parent->children(), 'parent_id');
}
# tests/Unit/ChildTest.php
/**
* Test Child has belongsTo relationship with Parent model
* #test
*/
public function belongs_to_parent_with_parent_id_fk()
{
$child = new Child;
$this->assertBelongsToUsing(Parent::class, $child->parent(), 'parent_id');
}
Argument #1 of PHPUnit\Framework\Assert::assertInstanceOf() must be a
class or interface name
tests\TestCase.php:14
> 10▕ use CreatesApplication;
> 11▕
> 12▕ public function assertHasManyUsing($related_model, $relationship, $foreign_key)
> 13▕ {
> ➜ 14▕ $this->assertInstanceOf(HasMany::class, $relationship);
> 15▕ $this->assertInstanceOf($related_model, $relationship->getRelated());
> 16▕ $this->assertEquals($foreign_key, $relationship->getForeignKeyName());
> 17▕ $this->assertTrue(Schema::hasColumns($relationship->getRelated()->getTable(),
> array($foreign_key)));
> 18▕ }
I am testing model relationships in this way
$this->assertTrue($comment->user()->exists());
I'm starting the serious play with doctrine.
Among my current challenges, I have to build an entity which contains an address. Let's take an example.
class Address {
private $country;
private $street;
}
class Person {
private $name;
/**
* #var Address
*/
private $address;
}
What I want to achieve is to have only one table in database, with Address being embedded in Person. Something of the like:
create table Person {
name string(255),
street string(255),
country string(255)
}
I know this is possible with ORMs like Hibernate, and I wish I could have the same behaviour with Doctrine.
How can I have an embedded one-to-one relationship with Doctrine ?
Unfortunately Doctrine's ORM doesn't support embedded objects. The only soultion I found so far is to do the mapping on your own:
class Person {
...
protected $addressName;
protected $addressStreet;
protected $addressCountry;
public function getAddress() {
return new Address($this->addressName, $this->addressStreet, $this->addressCountry);
// or Address::create(...) if you wan't to have a simple constructor
}
public function setAddress(Address $address) {
$this->addressName = $address->getName();
$this->addressStreet = $address->getStreet();
$this->addressCountry = $address->getCountry();
}
...
}
It's quite a lot of repeated code, so you can think about moving it into separate trait.
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.