Doctrine2, uniqueConstraint on JoinColumn for ZF2? - doctrine-orm

I have a data feed that has duplicated content (no idea why, it's an external feed), however we need to insert all items with a constraint on the title and type, i.e.
These can exist:
Name_A, Type_A
Name_A, Type_B
But only one of these should exist:
Name_A, Type_A
Name_A, Type_A
Here's the Entity code I'm using:
/**
* Restauration
*
* #ORM\Table(name="restauration", uniqueConstraints={#ORM\UniqueConstraint(name="name_unique", columns={"name_1", "restauration_type"})})
* #ORM\Entity(repositoryClass="iMotionTools\Repository\RestaurationRepository")
*/
class Restauration
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="NONE")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name_1", type="string", length=128, nullable=true)
*/
private $name1;
/**
* #var string
*
* #ORM\ManyToOne(targetEntity="RestaurationType", cascade={"persist"})
* #ORM\JoinColumn(name="restauration_type", referencedColumnName="id")
*/
private $type;
}
But I get this error when parsing and inserting the data:
SQLSTATE[23000]: Integrity constraint violation: 19 columns name_1, restauration_type are not unique:91:C:\coding\currate\vendor\doctrine\dbal\lib\Doctrine\DBAL\DBALException.php
I'm wondering whether the easy way is to just ignore the thrown exception? Looks like it's a driverExceptionDuringQuery that gets thrown during my call to $em->persist(); but I'm not sure how I would ignore if the call contains the above error?

If you want to ignore that, stop using constraint integrity.
{#ORM\UniqueConstraint(name="name_unique", columns={"name_1" //etc...
Your data has name_1 not unique this is why you have this error, integrity constraint check this, you can't ignore that, without remove the unique constraint parameters.
Edit :
You have to then, before persist data, check with your actual data in the table, if there is a duplicate entry for both name_1 AND Type, and do not persist them.
for check both you can use :
#UniqueEntity({"name", "type"})
found here :
validation multiple constraint columns
Even if it's for SF2, it's the same concept

I've removed the UniqueConstraint attribute from the table and added a function to check the objects list (which later get $entity->persist()-ed), using an array so that I can easily use it for different entity types, and it seems to work well now.
$key = $hashList ? '' : $page['id'];
foreach ($hashList as $method) {
$val = $object->{$method}();
if(is_object($val)) {
$val = $val->getId();
}
$key .= $val;
}
$key = md5($key);
$objects[$key] = $object;
Where $hashList = array('getName', 'getType') - and getType returns an object (since it's another entity), but which always has the getId() function... probably not the best solution but it works for my situation...

Related

How to join column and target entity manually in manytoone relation?

I have two fields : account_type_id and account_id.
How can i manually map doctrine TargetEntity to join Company Entity if accountTypeId = 1 OR join User Entity if account_type_id = 2 ?
<?php
/** #Entity */
class Accounts
{
// 1= Company, 2 = User
private $accountType;
/**
* #ManyToOne(targetEntity="Companies")
*/
private $company;
/**
* #ManyToOne(targetEntity="Users")
*/
private $user;
//...
}
Unfortunately, joining different columns on the fly cannot be done automatically, but you can have both fields set as nullable and only set the correct one when persisting the Account entity.
This would be the annotation:
/**
* #ORM\ManyToOne(targetEntity="Users", inversedBy="users")
* #ORM\JoinColumn(name="user_id", referencedColumnName="id", nullable=true)
*/
private $user;
Keep in mind that nullable=true is the default anyway, I'm just being specific here.
If you want to go defensively about this, you can have an additional check in getter
/**
* #return User
* #throws \Exception
*/
public function getUser()
{
if ($this->accountType !== 2) {
throw new \Exception("Entity is not of type 'user'");
}
return $this->user;
}

FatalError when flushing after persisting datas

I want to persist datas from a new Entity into the database, one of the most basic operation right?
But when I flush, I get the following fatal error:
FatalErrorException in DateTimeType.php line 53:
Error: Call to a member function format() on string
So here's how I want to persist my datas, on Symfony 3.2, PHP 5.6 (sigh) and mariadb 10.3:
$myEntity = new myEntity();
$myEntity->setFoo('a');
$myEntity->setBar('b');
$myEntity->setDateCreation(new \DateTime());
$this->em->persist($myEntity);
$this->em->flush();
My entity:
class myEntity
{
// ...
/**
* #var string
*
* #ORM\Column(name="sFoo", type="string", length=255, nullable=false, unique=true)
*/
private $foo;
/**
* #var string
*
* #ORM\Column(name="sBar", type="string", length=255, nullable=false)
*/
private $bar;
/**
* #var \DateTime
*
* #ORM\Column(name="dDateCreation", type="date")
*/
private $dateCreation;
// Getter and setters are classics, here's the one for the date
public function setDateCreation($dateCreation)
{
$this->dateCreation = $dateCreation;
}
}
I get absolutely no error if I stop the script before flushing. When I dump my entity just before, all the properties are set correctly :
MyEntity {#2301 ▼
-id: null
-foo: "a"
-bar "b
-dateCreation: DateTime {#2204 ▼
+"date": "2020-07-06 12:32:17.000000"
+"timezone_type": 3
+"timezone": "Europe/Paris"
}
}
From the database side : the columns seems to be correctly set: sFoo and sBar are VARCHAR(255) dDateCreation is date.
I eventually changed the date fields type from date to string, with a string as argument like "20200706", just for trying. And... I get the same error, even if there are no DateTime anymore involved in this.
Either I'm completely blind and there is an obvious thing I forgot in the code I've posted, or the reason comes from elsewhere.
I tried the same operation with an other Entity, same result.

Is there a way to add a aggregated/calculated field (concat) to a Doctrine entity?

I have a USER entity, which countains two fields : firstname and lastname.
I would like to know how I can add a virtual column called fullname (firstname + ' ' + upper(lastname)) to my entity.
I have read already the Doctrine documentation about aggregated fields but I am not sure how to use this in my context.
I don't want to add a concat and upper formula to every DQL I will launch on my users but I would like to add a computed field once in my entity. Doing a PHP getter is not a solution.
/**
* User
*
* #ORM\Table(name="[user]")
* #ORM\Entity(repositoryClass="UserRepository")
*/
class User
{
/**
* #var string|null
*
* #ORM\Column(name="lastname", type="string")
*/
private $lastname;
/**
* #var string|null
*
* #ORM\Column(name="firstname", type="string")
*/
private $firstname;
}
if I add an aggregate field like this to my entity :
/**
* #ORM\Column(type="string")
*/
private $fullName='';
public getFullName(){
return $this->firstname . ' ' . $this->lastname;
}
my code crashes when I call find() or findAll() on a User entity (invalid column name)
Maybe that's the wrong approach do do that "magic". If you need that fullname field only for the view, you can use your getter as it is. In case you need it persisted, you should concatinate the first- and lastname string in its setters and set the fullname property. in this case, your getter should return that property.

Doctrine2 - Doctrine generating query with associated entity - InvalidFieldNameException

Yep, the title suggests: Doctrine is looking for a fieldname that's not there. That's both true and not true at the same time, though I cannot figure out how to fix it.
The full error:
File: D:\path\to\project\vendor\doctrine\dbal\lib\Doctrine\DBAL\Driver\AbstractMySQLDriver.php:71
Message: An exception occurred while executing 'SELECT DISTINCT id_2
FROM (SELECT p0_.name AS name_0, p0_.code AS code_1, p0_.id AS id_2
FROM product_statuses p0_) dctrn_result ORDER BY p0_.language_id ASC, name_0 ASC LIMIT 25
OFFSET 0':
SQLSTATE[42S22]: Column not found: 1054 Unknown column
'p0_.language_id' in 'order clause'
The query the error is caused by (from error above):
SELECT DISTINCT id_2
FROM (
SELECT p0_.name AS name_0, p0_.code AS code_1, p0_.id AS id_2
FROM product_statuses p0_
) dctrn_result
ORDER BY p0_.language_id ASC, name_0 ASC
LIMIT 25 OFFSET 0
Clearly, that query is not going to work. The ORDER BY should be in the sub-query, or else it should replace p0_ in the ORDER BY with dctrn_result and also get the language_id column in the sub-query to be returned.
The query is build using the QueryBuilder in the indexAction of a Controller in Zend Framework. All is very normal and the same function works perfectly fine when using a the addOrderBy() function for a single ORDER BY statement. In this instance I wish to use 2, first by language, then by name. But the above happens.
If someone knows a full solution to this (or maybe it's a bug?), that would be nice. Else a hint in the right direction to help me solve this issue would be greatly appreciated.
Below additional information - Entity and indexAction()
ProductStatus.php - Entity - Note the presence of language_id column
/**
* #ORM\Table(name="product_statuses")
* #ORM\Entity(repositoryClass="Hzw\Product\Repository\ProductStatusRepository")
*/
class ProductStatus extends AbstractEntity
{
/**
* #var string
* #ORM\Column(name="name", type="string", length=255, nullable=false)
*/
protected $name;
/**
* #var string
* #ORM\Column(name="code", type="string", length=255, nullable=false)
*/
protected $code;
/**
* #var Language
* #ORM\ManyToOne(targetEntity="Hzw\Country\Entity\Language")
* #ORM\JoinColumn(name="language_id", referencedColumnName="id")
*/
protected $language;
/**
* #var ArrayCollection|Product[]
* #ORM\OneToMany(targetEntity="Hzw\Product\Entity\Product", mappedBy="status")
*/
protected $products;
[Getters/Setters]
}
IndexAction - Removed parts not directly related to QueryBuilder. Added in comments showing params as they are.
/** #var QueryBuilder $qb */
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select($asParam) // 'pro'
->from($emEntity, $asParam); // Hzw\Product\Entity\ProductStatus, 'pro'
if (count($queryParams) > 0 && !is_null($query)) {
// [...] creates WHERE statement, unused in this instance
}
if (isset($orderBy)) {
if (is_array($orderBy)) {
// !!! This else is executed !!! <-----
if (is_array($orderDirection)) { // 'ASC'
// [...] other code
} else {
// $orderBy = ['language', 'name'], $orderDirection = 'ASC'
foreach ($orderBy as $orderParam) {
$qb->addOrderBy($asParam . '.' . $orderParam, $orderDirection);
}
}
} else {
// This works fine. A single $orderBy with a single $orderDirection
$qb->addOrderBy($asParam . '.' . $orderBy, $orderDirection);
}
}
================================================
UPDATE: I found the problem
The above issue is not caused by incorrect mapping or a possible bug. It's that the QueryBuilder does not automatically handle associations between entities when creating queries.
My expectation was that when an entity, such as ProductStatus above, contains the id's of the relation (i.e. language_id column), that it would be possible to use those properties in the QueryBuilder without issues.
Please see my own answer below how I fixed my functionality to be able to have a default handling of a single level of nesting (i.e. ProducStatus#language == Language, be able to use language.name as ORDER BY identifier).
Ok, after more searching around and digging into how and where this goes wrong, I found out that Doctrine does not handle relation type properties of entities during the generation of queries; or maybe does not default to using say, the primary key of an entity if nothing is specified.
In the use case of my question above, the language property is of a #ORM\ManyToOne association to the Language entity.
My use case calls for the ability to handle at lease one level of relations for default actions. So after I realized that this is not handled automatically (or with modifications such as language.id or language.name as identifiers) I decided to write a little function for it.
/**
* Adds order by parameters to QueryBuilder.
*
* Supports single level nesting of associations. For example:
*
* Entity Product
* product#name
* product#language.name
*
* Language being associated entity, but must be ordered by name.
*
* #param QueryBuilder $qb
* #param string $tableKey - short alias (e.g. 'tab' with 'table AS tab') used for the starting table
* #param string|array $orderBy - string for single orderBy, array for multiple
* #param string|array $orderDirection - string for single orderDirection (ASC default), array for multiple. Must be same count as $orderBy.
*/
public function createOrderBy(QueryBuilder $qb, $tableKey, $orderBy, $orderDirection = 'ASC')
{
if (!is_array($orderBy)) {
$orderBy = [$orderBy];
}
if (!is_array($orderDirection)) {
$orderDirection = [$orderDirection];
}
// $orderDirection is an array. We check if it's of equal length with $orderBy, else throw an error.
if (count($orderBy) !== count($orderDirection)) {
throw new \InvalidArgumentException(
$this->getTranslator()->translate(
'If you specify both OrderBy and OrderDirection as arrays, they should be of equal length.'
)
);
}
$queryKeys = [$tableKey];
foreach ($orderBy as $key => $orderParam) {
if (strpos($orderParam, '.')) {
if (substr_count($orderParam, '.') === 1) {
list($entity, $property) = explode('.', $orderParam);
$shortName = strtolower(substr($entity, 0, 3)); // Might not be unique...
$shortKey = $shortName . '_' . (count($queryKeys) + 1); // Now it's unique, use $shortKey when continuing
$queryKeys[] = $shortKey;
$shortName = strtolower(substr($entity, 0, 3));
$qb->join($tableKey . '.' . $entity, $shortName, Join::WITH);
$qb->addOrderBy($shortName . '.' . $property, $orderDirection[$key]);
} else {
throw new \InvalidArgumentException(
$this->getTranslator()->translate(
'Only single join statements are supported. Please write a custom function for deeper nesting.'
)
);
}
} else {
$qb->addOrderBy($tableKey . '.' . $orderParam, $orderDirection[$key]);
}
}
}
It by no means supports everything the QueryBuilder offers and is definitely not a final solution. But it gives a starting point and solid "default functionality" for an abstract function.

Doctrine 2 - Insert new item in database

I'm trying to make something very simple.. but I do wrong, and I don't know what is the problem. Just I'm trying to insert new item to database with Doctrine 2:
$favouriteBook = new UserFavouriteBook;
$favouriteBook->user_id = 5;
$favouriteBook->book_id = 8;
$favouriteBook->created_at = new DateTime("now");
$this->_em->persist($favouriteBook);
$this->_em->flush();
As you can see.. is very simple, but that, give me next error:
Error: Message: SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'user_id' cannot be null
Obviosly, if I make a "dump" before "persist" and "flush" of $favouriteBook, all looks be correct..
This is my "favouriteBook" entity:
/** #Column(type="integer")
* #Id
*/
private $user_id;
/** #Column(type="integer")
* #Id
*/
private $book_id;
/**
* #ManyToOne(targetEntity="Book", inversedBy="usersFavourite")
* #JoinColumn(name="book_id", referencedColumnName="id")
*/
private $book;
/**
* #ManyToOne(targetEntity="User", inversedBy="favouriteBooks")
* #JoinColumn(name="user_id", referencedColumnName="id")
*/
private $user;
/** #Column(type="datetime") */
private $created_at;
public function __get($property) {
return $this->$property;
}
public function __set($property, $value) {
$this->$property = $value;
}
Anyone can image what is the problem? .. I don't know what else try.. Thanks
I think what beberlei is saying is that within your favouriteBook entity, you don't need to define the user_id and book_id as class properties, b/c the book and user properties you have set already recognize these as the relevant join columns. Also, your attempt to persist the favouriteBook entity failed because you need to set the book and user entity associations within the favouriteBook entity, not the foreign keys. So it would be:
$favouriteBook = new UserFavouriteBook;
$favouriteBook->book = $book;
$favouriteBook->user = $user;
$favouriteBook->created_at = new DateTime("now");
$this->_em->persist($favouriteBook);
$this->_em->flush();
You are mapping foreign keys and the associations. You have to modify the association not the foreign key field. Its bad-practice to map them both, you should remove $book_id and $user_id completly.