Default value in Doctrine - php

How do I set a default value in Doctrine 2?

<?php
/**
* #Entity
*/
class myEntity {
/**
* #var string
*
* #ORM\Column(name="myColumn", type="integer", options={"default" : 0})
*/
private $myColumn;
...
}
Note that this uses SQL DEFAULT, which is not supported for some fields like BLOB and TEXT.

Database default values are not "portably" supported. The only way to use database default values is through the columnDefinition mapping attribute where you specify the SQL snippet (DEFAULT cause inclusive) for the column the field is mapped to.
You can use:
<?php
/**
* #Entity
*/
class myEntity {
/**
* #var string
*
* #Column(name="myColumn", type="string", length="50")
*/
private $myColumn = 'myDefaultValue';
...
}
PHP-level default values are preferred as these are also properly available on newly created and persisted objects (Doctrine will not go back to the database after persisting a new object to get the default values).

Set up a constructor in your entity and set the default value there.

Use:
options={"default":"foo bar"}
and not:
options={"default"="foo bar"}
For instance:
/**
* #ORM\Column(name="foo", type="smallint", options={"default":0})
*/
private $foo

Update
One more reason why read the documentation for Symfony will never go out of trend. There is a simple solution for my specific case and is to set the field type option empty_data to a default value.
Again, this solution is only for the scenario where an empty input in a form sets the DB field to null.
Background
None of the previous answers helped me with my specific scenario but I found a solution.
I had a form field that needed to behave as follow:
Not required, could be left blank. (Used 'required' => false)
If left blank, it should default to a given value. For better user experience, I did not set the default value on the input field but rather used the html attribute 'placeholder' since it is less obtrusive.
I then tried all the recommendations given in here. Let me list them:
Set a default value when for the entity property:
<?php
/**
* #Entity
*/
class myEntity {
/**
* #var string
*
* #Column(name="myColumn", type="string", length="50")
*/
private $myColumn = 'myDefaultValue';
...
}
Use the options annotation:
#ORM\Column(name="foo", options={"default":"foo bar"})
Set the default value on the constructor:
/**
* #Entity
*/
class myEntity {
...
public function __construct()
{
$this->myColumn = 'myDefaultValue';
}
...
}
None of it worked and all because of how Symfony uses your Entity class.
IMPORTANT
Symfony form fields override default values set on the Entity class.
Meaning, your schema for your DB can have a default value defined but if you leave a non-required field empty when submitting your form, the form->handleRequest() inside your form->isValid() method will override those default values on your Entity class and set them to the input field values. If the input field values are blank, then it will set the Entity property to null.
http://symfony.com/doc/current/book/forms.html#handling-form-submissions
My Workaround
Set the default value on your controller after form->handleRequest() inside your form->isValid() method:
...
if ($myEntity->getMyColumn() === null) {
$myEntity->setMyColumn('myDefaultValue');
}
...
Not a beautiful solution but it works. I could probably make a validation group but there may be people that see this issue as a data transformation rather than data validation, I leave it to you to decide.
Override Setter (Does Not Work)
I also tried to override the Entity setter this way:
...
/**
* Set myColumn
*
* #param string $myColumn
*
* #return myEntity
*/
public function setMyColumn($myColumn)
{
$this->myColumn = ($myColumn === null || $myColumn === '') ? 'myDefaultValue' : $myColumn;
return $this;
}
...
This, even though it looks cleaner, it doesn't work. The reason being that the evil form->handleRequest() method does not use the Model's setter methods to update the data (dig into form->setData() for more details).

Here is how to do it in PHP 8 using attributes.
#[ORM\Column(type: 'boolean', nullable: false, options: ['default' => 0])]
#[Assert\NotNull()]
private bool $isFavorite = false;

The workaround I used was a LifeCycleCallback. Still waiting to see if there is any more "native" method, for instance #Column(type="string", default="hello default value").
/**
* #Entity #Table(name="posts") #HasLifeCycleCallbacks
*/
class Post implements Node, \Zend_Acl_Resource_Interface {
...
/**
* #PrePersist
*/
function onPrePersist() {
// set default date
$this->dtPosted = date('Y-m-d H:m:s');
}

You can do it using xml as well:
<field name="acmeOne" type="string" column="acmeOne" length="36">
<options>
<option name="comment">Your SQL field comment goes here.</option>
<option name="default">Default Value</option>
</options>
</field>

Here is how I solved it for myself. Below is an Entity example with default value for MySQL. However, this also requires the setup of a constructor in your entity, and for you to set the default value there.
Entity\Example:
type: entity
table: example
fields:
id:
type: integer
id: true
generator:
strategy: AUTO
label:
type: string
columnDefinition: varchar(255) NOT NULL DEFAULT 'default_value' COMMENT 'This is column comment'

None of this worked for me. I found some documentation on doctrine's site that says to set the value directly to set a default value.
https://www.doctrine-project.org/projects/doctrine-orm/en/2.6/reference/faq.html#how-can-i-add-default-values-to-a-column
private $default = 0;
This inserted the value I wanted.

Works for me on a mysql database also:
Entity\Entity_name:
type: entity
table: table_name
fields:
field_name:
type: integer
nullable: true
options:
default: 1

Adding to #romanb brilliant answer.
This adds a little overhead in migration, because you obviously cannot create a field with not null constraint and with no default value.
// this up() migration is autogenerated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() != "postgresql");
//lets add property without not null contraint
$this->addSql("ALTER TABLE tablename ADD property BOOLEAN");
//get the default value for property
$object = new Object();
$defaultValue = $menuItem->getProperty() ? "true":"false";
$this->addSql("UPDATE tablename SET property = {$defaultValue}");
//not you can add constraint
$this->addSql("ALTER TABLE tablename ALTER property SET NOT NULL");
With this answer, I encourage you to think why do you need the default value in the database in the first place? And usually it is to allow creating objects with not null constraint.

If you use yaml definition for your entity,
the following works for me on a postgresql database:
Entity\Entity_name:
type: entity
table: table_name
fields:
field_name:
type: boolean
nullable: false
options:
default: false

While setting the value in the constructor would work, using the Doctrine Lifecycle events might be a better solution.
By leveraging the prePersist Lifecycle Event, you could set your default value on your entity only on initial persist.

I struggled with the same problem. I wanted to have the default value from the database into the entities (automatically). Guess what, I did it :)
<?php
/**
* Created by JetBrains PhpStorm.
* User: Steffen
* Date: 27-6-13
* Time: 15:36
* To change this template use File | Settings | File Templates.
*/
require_once 'bootstrap.php';
$em->getConfiguration()->setMetadataDriverImpl(
new \Doctrine\ORM\Mapping\Driver\DatabaseDriver(
$em->getConnection()->getSchemaManager()
)
);
$driver = new \Doctrine\ORM\Mapping\Driver\DatabaseDriver($em->getConnection()->getSchemaManager());
$driver->setNamespace('Models\\');
$em->getConfiguration()->setMetadataDriverImpl($driver);
$cmf = new \Doctrine\ORM\Tools\DisconnectedClassMetadataFactory();
$cmf->setEntityManager($em);
$metadata = $cmf->getAllMetadata();
// Little hack to have default values for your entities...
foreach ($metadata as $k => $t)
{
foreach ($t->getFieldNames() as $fieldName)
{
$correctFieldName = \Doctrine\Common\Util\Inflector::tableize($fieldName);
$columns = $tan = $em->getConnection()->getSchemaManager()->listTableColumns($t->getTableName());
foreach ($columns as $column)
{
if ($column->getName() == $correctFieldName)
{
// We skip DateTime, because this needs to be a DateTime object.
if ($column->getType() != 'DateTime')
{
$metadata[$k]->fieldMappings[$fieldName]['default'] = $column->getDefault();
}
break;
}
}
}
}
// GENERATE PHP ENTITIES!
$entityGenerator = new \Doctrine\ORM\Tools\EntityGenerator();
$entityGenerator->setGenerateAnnotations(true);
$entityGenerator->setGenerateStubMethods(true);
$entityGenerator->setRegenerateEntityIfExists(true);
$entityGenerator->setUpdateEntityIfExists(false);
$entityGenerator->generate($metadata, __DIR__);
echo "Entities created";

Be careful when setting default values on property definition! Do it in constructor instead, to keep it problem-free. If you define it on property definition, then persist the object to the database, then make a partial load, then not loaded properties will again have the default value. That is dangerous if you want to persist the object again.

Related

Set doctrine entity boolean field to 0 instead of null

Im trying to persist an doctrine entity with a boolean field where the values are 0 or 1.
When the property is set to true, it save it as '1' in database.
But when its 'false' or '0', it save it as NULL on database.
How can I fix this to only save only as 1 or 0 ?
The annotation for the property I use is like following:
#ORM\Column(name="substitute", type="boolean", nullable=true)
When I set nullable to false, I cant persist it because it still want to set to null.
Thanks
When I persist it, the field value is 0
Attempt 1
#ORM\Column(name="substitute", type="boolean", options={"default":"0"}))
error: Can't save null
Attempt 2
#ORM\Column(name="substitute", type="boolean", nullable= true, options={"default":"0"}))
Doesn"t work, it still save null in base
Info 1
The actually insert query is trying to insert 0. But I got this error "ORA-01400: cannot insert NULL into (\"MYBASE\".\"MYTABLE\".\"SUBSTITUTE\")"
Info 2
Same append with another entity
class TestEntity
{
/**
* #ORM\Column(name="test_entity_id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(name="substitute", type="boolean")
*/
private $isSubstitute = false;
}
Persisting
$test = new TestEntity();
$test->setIsSubstitute(false);
$em->persist($test);
Result
request.CRITICAL: Uncaught PHP Exception Doctrine\DBAL\Exception\NotNullConstraintViolationException: "An exception occurred while executing 'INSERT INTO TestEntity (test_entity_id, substitute) VALUES (?, ?)' with params [7, 0]: SQLSTATE[HY000]: General error: 1400 OCIStmtExecute: ORA-01400: cannot insert NULL into ("MYBASE"."TESTENTITY"."SUBSTITUTE") (ext\pdo_oci\oci_statement.c:148)"\n (ext\\pdo_oci\\oci_statement.c:148) at PATH\\vendor\\doctrine\\dbal\\lib\\Doctrine\\DBAL\\Driver\\PDOStatement.php:91)"} []
Info 3
Inserting manually works using oci or oci8 driver
sql> INSERT INTO TestEntity (test_entity_id, substitute) VALUES (13, 0)
[2017-04-06 11:21:15] 1 row affected in 62ms
Just set the SQL Default to 0 (Edit: You need to update the schema after that change):
/**
* #ORM\Column(type="boolean", options={"default":"0"})
*/
protected $isActive;
Also you could initialize the property by default:
/**
* #ORM\Column(type="boolean", options={"default":"0"})
*/
protected $isActive = false;
Nullable shouldn't matter as long as the value is set to either true/false.
If you really set the property to false before saving and it still saves it as null in the DB then something else is going on.
Change your driver from oci to oci8 in your paramters.yml file:
database_driver: oci8
That should do it. Use the Underground PHP and Oracle Manual for installing OCI8.
I think #Cerad's suggestion is correct, can you try:
/**
* #ORM\Column(name="substitute", type="boolean")
*/
protected $substitute = false;
Let us know the result.
I just replicated your case, and I managed to successfully save into the db 1 for true, and 0 for false.
Example:
//Entity
Person: id, name, isMajor(boolean field)
//IMPORTANT: I setted the boolean field inside __construct() method, and let it be, by default, false (0). This means you don't need anymore to have that options={"default":"0"}.
//...
/**
* #var bool
*
* #ORM\Column(name="isMajor", type="boolean", nullable=true)
*/
private $isMajor;
public function __construct()
{
$this->isMajor = false;
}
//Created CRUD against the Entity
//When saving (either using the default actions provided by the CRUD, or by setting the values inside another controller's action):
//AppBundle/Controller/DefaultController.php
/**
* #Route("/new-person")
*/
public function createAction()
{
$person = new Person();
$person->setName('name');
$person->setIsMajor(true); // this saves 1 in the table
$person->setIsMajor(false); // this saves 0 in the table
$em = $this->getDoctrine()->getManager();
$em->persist($person);
$em->flush();
return $this->redirectToRoute('person_index');
}
I hope I did understood well your problem.
You can create fake object using $em->getReference(EntityName::class, "AnyValue");
Happy coding.

Symfony2 Doctrine2 autoincrement field with custom id generator

I'm facing a problem with a custom id generator, auto-incrementation and doctrine-migrations in Symfony (2.7) with Doctrine (2.5) and MySQL.
When I use the custom generator, the id field does not get the AUTO_INCREMENT status in the migrations:diff output.
When I add the autoincrement manually in the yaml then all referenced table-columns also get the AUTO_INCREMENT.
How can I use auto increment on my primary entity with a custom generator without affecting the references?
The entity (sites.orm.yml):
type: entity
table: sites
id:
id:
type: integer
column: id
generator:
strategy: CUSTOM
customIdGenerator:
class: AssignableIdentityGenerator
The generator:
class AssignableIdentityGenerator extends AbstractIdGenerator {
public function generate(EntityManager $em, $entity) {
return $entity->getId() ? : (int) $em->getConnection()->lastInsertId();
}
public function isPostInsertGenerator() {
return true;
}
}
Migrations:diff output keeps being this (removes the AUTO_INCREMENT):
ALTER TABLE sites CHANGE id id INT NOT NULL;
If I add
options:
autoincrement: true
I don't get an ALTER for the sites table, which is correct because it keeps the AUTO_INCREMENT, but instead I get
ALTER TABLE other_table CHANGE site_id site_id INT AUTO_INCREMENT NOT NULL;
on a referencing table, which is not correct.
Doctrine natively suports the AUTO_INCREMENT with its own IdentityGenerator and I think I found the spot in Doctrine\ORM\Tools\SchemaTool::gatherColumn where this takes effect, but it's deep in a private method:
if ($class->isIdGeneratorIdentity() && $class->getIdentifierFieldNames() == array($mapping['fieldName'])) {
$options['autoincrement'] = true;
}
Unfortunately Doctrine is not very friendly to extensions (many private methods/properties or classes being instantiated instead of using injectable services).
Would anyone have a solution to this problem?
Or maybe even a whole different idea on how to get an id field that is both auto-incrementable and manually settable.
Would anyone have a solution to this problem? Or maybe even a whole different idea on how to get an id field that is both auto-incrementable and manually settable.
I got this solution for you:
First: You need to set up your ID strategy to "AUTO"
/**
* #var int
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
Second: If you want to set up your ID manually, you need to change the metadata (before flush) of your class. There you can change your IdGeneratorType
/**
* #param EntityManager $em
* #param mixed $object
*/
protected function changeIdGeneratorType(EntityManager $em, $object)
{
if ($object->getId() !== null) {
$metadata = $em->getClassMetadata(get_class($object));
$metadata->setIdGeneratorType($metadata::GENERATOR_TYPE_NONE);
}
}
$metadata::GENERATOR_TYPE_NONE
With this GeneratorType you will be able to set your ID's manually.
Now you will be able to both auto-incrementable and manually settable ID's

Symfony Doctrine One to Many does not insert foreign key

I am having annoying problems with persisting an entity with one or more OneToMany-Childs.
I have a "Buchung" entity which can have multiple "Einsatztage" (could be translated to an event with many days)
In the "Buchung entity I have
/**
* #param \Doctrine\Common\Collections\Collection $property
* #ORM\OneToMany(targetEntity="Einsatztag", mappedBy="buchung", cascade={"all"})
*/
private $einsatztage;
$einsatztage is set to an ArrayCollection() in the __constructor().
Then there is the "Einsatztag" Entity which has a $Buchung_id variable to reference the "Buchung"
/**
* #ORM\ManyToOne(targetEntity="Buchung", inversedBy="einsatztage", cascade={"all"})
* #ORM\JoinColumn(name="buchung_id", referencedColumnName="id")
*/
private $Buchung_id;
Now If I try to persist an object to the database the foreign key of the "Einsatztag" Table is always left empty.
$buchung = new Buchung();
$buchung->setEvent( $r->request->get("event_basis"));
$buchung->setStartDate(new \DateTime($r->request->get("date_from")));
$buchung->setEndDate(new \DateTime($r->request->get("date_to")));
$von = $r->request->get("einsatz_von");
$bis = $r->request->get("einsatz_bis");
$i = 0;
foreach($von as $tag){
$einsatztag = new Einsatztag();
$einsatztag->setNum($i);
$einsatztag->setVon($von[$i]);
$einsatztag->setBis($bis[$i]);
$buchung->addEinsatztage($einsatztag);
$i++;
}
$em = $this->getDoctrine()->getManager();
$em->persist($buchung);
foreach($buchung->getEinsatztage() as $e){
$em->persist($e);
}
$em->flush();
Firstly, you have to understand that Doctrine and Symfony does not work with id's within your entities.In Einsatztag entity, your property should not be called $Buchung_id since it's an instance of buchung and not an id you will find out there.
Moreover, in your loop, you add the Einsatztag to Buchung. But do you process the reverse set ?
I do it this way to always reverse the set/add of entities.
Einsatztag
public function setBuchung(Buchung $pBuchung, $recurs = true){
$this->buchung = $pBuchung;
if($recurs){
$buchung->addEinsatztag($this, false);
}
}
Buchung
public function addEinsatztag(Einsatztag $pEinsatztag, $recurs = true){
$this->einsatztages[] = $pEinsatztag;
if($recurs){
$pEinsatztag->setBuchung($this, false);
}
}
Then, when you will call
$buchung->addEinsatztag($einsatztag);
Or
$einsatztag->set($buchung);
The relation will be set on both side making your FK to be set. Take care of this, you'll have some behavior like double entries if you do not use them properly.
SImplier , you can use default getter/setters and call them on both sides of your relation, using what you already have, like following:
$einsatztag->set($buchung);
$buchung->addEinsatztag($einsatztag);
Hope it helped ;)
First of all, don't use _id properties in your code. Let it be $buchung. If you want it in the database, do it in the annotation. And this also the reason, why it's not working. Your are mapping to buchung, but your property is $Buchung_id
<?php
/** #ORM\Entity **/
class Buchung
{
// ...
/**
* #ORM\OneToMany(targetEntity="Einsatztag", mappedBy="buchung")
**/
private $einsatztage;
// ...
}
/** #ORM\Entity **/
class Einsatztag
{
// ...
/**
* #ORM\ManyToOne(targetEntity="Product", inversedBy="einsatztage")
* #JoinColumn(name="buchung_id", referencedColumnName="id")
**/
private $buchung;
// ...
}
You don't have to write the #JoinColumn, because <propertyname>_id would the default column name.
I'm going to ignore the naming issue and add a fix to the actual problem.
You need to have in the adder method a call to set the owner.
//Buchung entity
public function addEinsatztage($einsatztag)
{
$this->einsatztags->add($einsatztag);
$ein->setBuchung($this);
}
And to have this adder called when the form is submitted you need to add to the form collection field the by_reference property set to false.
Here is the documentation:
Similarly, if you're using the CollectionType field where your underlying collection data is an object (like with Doctrine's ArrayCollection), then by_reference must be set to false if you need the adder and remover (e.g. addAuthor() and removeAuthor()) to be called.
http://symfony.com/doc/current/reference/forms/types/collection.html#by-reference

Doctrine : Default value on joinColumn field

New to symfony2 and Doctrine.
How can I set a default value to the field foo_id (which is a reference on Foo table) to point on the ID 1 of the Foo table (which exists in all cases) ?
Me\NavigationBundle\Entity\PublicText:
type: entity
table: public_text
id:
id:
type: integer
id: true
generator:
strategy: AUTO
fields:
title:
type: string
length: '255'
nullable: false
content:
type: string
length: '2000'
nullable: false
manyToOne:
foo:
#How to set a default value???
targetEntity: \Me\NavigationBundle\Entity\Foo
joinColumn:
name: foo_id
referencedColumnName: id
nullable: false
lifecycleCallbacks: { }
I tried a lot of things without success :
Set default value to ID 1 in the constructor of Foo
Perform a request to retrieve the Foo object of ID 1 in the Me entity (could works, but bad practice)
Look up columnDefinition at Doctrine Column Annotations Reference¶
Pros: You can set up your own column definition
Cons: doctrine orm:schema-tool used by Doctrine (if you use it), gets confused and always reports columns that have a custom columnDefinition as changed. As it it will always tell you to run the change command for your column definition, as documented here:
SchemaTool will not detect changes on the column correctly anymore if you use "columnDefinition".
Example
/**
* #ManyToOne(targetEntity="YourType")
* #JoinColumn(
* name="your_type_id",
* referencedColumnName="id",
* nullable=false,
* columnDefinition="INT NOT NULL DEFAULT 1"
* )
*/
private $yourType;
Please note that using columnDefinition alone will work for generating migrations but will break the ORM context and potentially cause FK integrity issues. You will still need to add the object association to the ORM for persisting entities. See warnings from Ocramius
Example:
(new PublicText())
->getFoo(); //null - expected object(Foo)#1 (1) { ["id"] => int(1) }
I have seen many ways to achieve this in doctrine 2.
Constructor
In general, the quickest way and what most users do is require an association in the constructor.
public function __construct(Foo $foo)
{
$this->foo = $foo;
}
Then you can use getReference to retrieve it in your controller without needing to query the database. See http://doctrine-orm.readthedocs.org/en/latest/reference/advanced-configuration.html#reference-proxies
$foo = $em->getReference('app:Foo', 1);
$text = new \Path\To\Entity\PublicText($foo);
$em->persist($text);
$em->flush();
LifeCycleEvents
My preferred way Another method to set the default value with most ManyToOne relationships is to utilize the LifeCycleEvents http://doctrine-orm.readthedocs.org/en/latest/reference/events.html
Though it does have some caveats to be aware of. So be sure to RTM before implementing into production environments. In this case it should work fine, but I don't know your entire mapping structure.
use Doctrine\ORM\Event\LifecycleEventArgs;
/**
* #Entity
* #HasLifeCycleEvents
*/
class PublicText
{
// ...
/**
* #PrePersist
* #param \Doctrine\ORM\Event\LifecycleEventArgs $event
*/
public function onPrePersist(LifecycleEventArgs $event)
{
if (false === empty($this->foo)) {
return;
}
$this->foo = $event->getEntityManager()->getReference('app:Foo', 1);
}
}
Then in your controller.
$text = new \Path\To\Entity\PublicText;
$em->persist($text); //retrieve and set foo if not set in the Entity Event.
$em->flush();
Repository Method
Another option within your Entity is to just set the property's value using a Repository.
Define Repository Class in Entity
/**
* #Entity(repositoryClass="PublicTextRepo")
*/
class PublicText
{
// ...
}
Repository
use Doctrine\ORM\EntityRepository;
class PublicTextRepo extends EntityRepository
{
public function create()
{
$text = new \Path\To\Entity\PublicText;
$foo = $this->_em->getReference('app:Foo', 1);
$text->setFoo($foo );
return $text;
}
}
Then in your controller you can then do
$text = $em->getRepository('app:PublicText')->create();
$em->persist($text);
$em->flush();
Discriminator Map
Though not always viable depending on the use case. One of the ways I go about defining a default value of an Entity is creating a DiscriminatorMap with single table inheritance. http://doctrine-orm.readthedocs.org/en/latest/reference/inheritance-mapping.html#single-table-inheritance.
This way when the object is created the default value is automatically set in the database, and locks the object as that type. The issue is that the resulting value is not an object like it is in the other methods above.
To get the object's discriminator static value, you can use constants within the objects you define.
/**
* #Entity
* #InheritanceType("SINGLE_TABLE")
* #Table(name="user")
* #DiscriminatorColumn(name="type", type="string")
* #DiscriminatorMap({Person::TYPE="Person", Employee::TYPE="Employee"})
*/
class Person
{
const TYPE = 'person';
/**
* #return string [person|employee]
*/
public function getType()
{
return $this::TYPE;
}
// ...
}
/**
* #Entity
*/
class Employee extends Person
{
const TYPE = 'employee';
// ...
}
Then all you need to do in your controller is.
$employee = new \Path\To\Entity\Employee;
$em->persist($employee); //inserts with `user.type` as `employee`
$em->flush();
echo $employee->getType(); //employee
With annotation you can use : options={"default" = YourValue} on a #ORM\Column, So in yaml I think you can add
options:
default: yourValue
I'm not sure, i gie you an idea...

How to avoid duplicate entries in a many-to-many relationship with Doctrine?

I'm using an embed Symfony form to add and remove Tag entities right from the article editor. Article is the owning side on the association:
class Article
{
/**
* #ManyToMany(targetEntity="Tags", inversedBy="articles", cascade={"persist"})
*/
private $tags;
public function addTag(Tag $tags)
{
if (!$this->tags->contains($tags)) // It is always true.
$this->tags[] = $tags;
}
}
The condition doesn't help here, as it is always true, and if it wasn't, no new tags would be persisted to the database at all. Here is the Tag entity:
class Tag
{
/**
* #Column(unique=true)
*/
private $name
/**
* #ManyToMany(targetEntity="Articles", mappedBy="tags")
*/
private $articles;
public function addArticle(Article $articles)
{
$this->articles[] = $articles;
}
}
I've set $name to unique, because I want to use the same tag every time I enter the same name in the form. But it doesn't work this way, and I get the exception:
Integrity constraint violation: 1062 Duplicate entry
What do I need to change to use article_tag, the default join table when submitting a tag name, that's already in the Tag table?
I have been battling with a similar issue for months and finally found a solution that seems to be working very well in my application. It's a complex application with quite a few many-to-many associations and I need to handle them with maximum efficiency.
The solution is explained in part here: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/faq.html#why-do-i-get-exceptions-about-unique-constraint-failures-during-em-flush
You were already halfway there with your code:
public function addTag(Tag $tags)
{
if (!$this->tags->contains($tags)) // It is always true.
$this->tags[] = $tags;
}
Basically what I have added to this is to set indexedBy="name" and fetch="EXTRA_LAZY" on the owning side of the relationship, which in your case is Article entity (you may need to scroll the code block horizontally to see the addition):
class Article
{
/**
* #ManyToMany(targetEntity="Tags", inversedBy="articles", cascade={"persist"}, indexedBy="name" fetch="EXTRA_LAZY")
*/
private $tags;
You can read up about the fetch="EXTRA_LAZY" option here.
You can read up about indexBy="name" option here.
Next, I modified my versions of your addTag() method as follows:
public function addTag(Tag $tags)
{
// Check for an existing entity in the DB based on the given
// entity's PRIMARY KEY property value
if ($this->tags->contains($tags)) {
return $this; // or just return;
}
// This prevents adding duplicates of new tags that aren't in the
// DB already.
$tagKey = $tag->getName() ?? $tag->getHash();
$this->tags[$tagKey] = $tags;
}
NOTE: The ?? null coalesce operator requires PHP7+.
By setting the fetch strategy for tags to EXTRA_LAZY the following statement causes Doctrine to perform a SQL query to check if a Tag with the same name exists in the DB (see the related EXTRA_LAZY link above for more):
$this->tags->contains($tags)
NOTE: This can only return true if the PRIMARY KEY field of the entity passed to it is set. Doctrine can only query for existing entities in the database/entity map based on the PRIMARY KEY of that entity, when using methods like ArrayCollection::contains(). If the name property of the Tag entity is only a UNIQUE KEY, that's probably why it's always returning false. You will need a PRIMARY KEY to use methods like contains() effectively.
The rest of the code in the addTag() method after the if block creates a key for the ArrayCollection of Tags either by the value in the PRIMARY KEY property (preferred if not null) or by the Tag entity's hash (search Google for "PHP + spl_object_hash", used by Doctrine to index entities). So, you are creating an indexed association, so that if you add the same entity twice before a flush, it will just be re-added at the same key, but not duplicated.
Two main solutions
First
Use a data transformer
class TagsTransformer implements DataTransformerInterface
{
/**
* #var ObjectManager
*/
private $om;
/**
* #param ObjectManager $om
*/
public function __construct(ObjectManager $om)
{
$this->om = $om;
}
/**
* used to give a "form value"
*/
public function transform($tag)
{
if (null === $tag) {
//do proper actions
}
return $issue->getName();
}
/**
* used to give "a db value"
*/
public function reverseTransform($name)
{
if (!$name) {
//do proper actions
}
$issue = $this->om
->getRepository('YourBundleName:Tag')
->findOneBy(array('name' => $name))
;
if (null === $name) {
//create a new tag
}
return $tag;
}
}
Second
Use lifecycle callback. In particular you can use prePersist trigger onto your article entity? In that way you can check for pre-existing tags and let your entity manager manage them for you (so he don't need to try to persist causing errors).
You can learn more about prePersist here
HINT FOR SECOND SOLUTION
Make a custom repository method for search and fetch old tags (if any)

Categories