Can I use the same table to represent different Entities in Symfony? - php

I am migrating an old PHP project to Symfony. I am trying to create the entities based on the existing database schema which I can not change. I am facing a problem :
There is a table that would represent two different entities. Basically, there is a boolean (a tinyint(1)), if the boolean is false, then the row of the table is representing a cart. If the boolean is true, then the row is representing an order.
Is it possible for Doctrine to make the distinction between these and to fetch those entities accordingly ? The solution I was willing to implement was creating several entities and overwrite the find() and findAll() methods in these entities' repositories. Is there another way to achieve that ?

This is what doctrine call Inheritance Mapping.
So you'll have one Cart entity and one Order entity extended it.
/**
* #ORM\Table()
* #ORM\Entity(repositoryClass="App\Repository\CartRepository")
* #ORM\InheritanceType(value="SINGLE_TABLE")
* #ORM\DiscriminatorColumn(name="is_order", columnDefinition="BOOL DEFAULT FALSE")
* #ORM\DiscriminatorMap(
* value={
* CART::IS_CART=Cart::class,
* CART::IS_ORDER=Order::class
* }
* )
*/
class Cart {
const IS_CART = FALSE;
const IS_ORDER = TRUE;
... // Entity field, getters, setters, functions...
}
Then your Order Entity.
/**
* #ORM\Entity(repositoryClass=OrderRepository::class)
*/
class Order extends Cart {...}
There is maybe some mistake in this code I didn't test it but it should be ok.

Related

Doctrine launches INSERT always instead of UPDATE for existing entities

Let's say I have the following entities:
App\Entity\MainEntity:
/**
* #var object
*
* #ORM\OneToOne(targetEntity="App\Entity\DependentEntity", fetch="EAGER")
* #ORM\JoinColumn(name="DependentEntityType1FK", referencedColumnName="DependentEntityIDPK")
*/
private $dependentEntityType1;
/**
* #var object
*
* #ORM\OneToOne(targetEntity="App\Entity\DependentEntity", fetch="EAGER")
* #ORM\JoinColumn(name="DependentEntityType2FK", referencedColumnName="DependentEntityIDPK")
*/
private $dependentEntityType2;
Basically, one-directional 1:1 relationship from main entity to the same dependent entity using two different columns in the main entity table.
It doesn't matter, whether I use fetch="EAGER" or normal lazy loading through Doctrine proxy classes, when I do something like this:
$mainEntity = $this->mainEntityRepository->find(74);
$mainEntity->setDependentEntityType1($this->dependentEntityRepository->find(35));
$this->mainEntityRepository->saveTest($mainEntity);
where ::saveTest() is:
public function saveTest(MainEntity $mainEntity) {
$this->_em->persist($mainEntity->getDependentEntityType1());
$this->_em->merge($mainEntity);
$this->_em->flush();
}
it always tries to INSERT a new dependent entity to the table, even though I never made any changes (and even if I made them, it should have been UPDATE! for it)
The question is: why does Doctrine decide this dependent entity is a new one if I did $this->dependentEntityRepository->find(35) , so loaded an existing one?
I tried fetch="EAGER" thinking that spl_object_hash might return different hashes for a Proxy class instance and the actual DependantEntity one, but it doesn't matter, the DependantEntity is for some reason always considered as "new".
UPDATE: here is the code of ::setDependentEntityType1()
public function setDependentEntityType1(DependentEntity $dependentEntity) : void {
$this->dependentEntity = $dependentEntity;
}

Merge is creating new record for children

Merge is creating not working for children #OneToMany
I am using Php Doctrine and I am using #OnToMany mapping with cascade all. I have a parent class SalesOrder and child class SalesOrderDetails.
Case 1 : Save - When I save new record sales order along with sales order details. It is working as expected.
Case 2 : Update - Here is the issue, I am merging the Sales Order which is fine however its inserting new records for its children SalesOrderDetail instead of updating it. Ideally it should it apply mergebut for children as well but its not.
As of now, I am getting the Sales Order Details by id from DB then change the properties of it. Ideally that should not be the case, mean if we set the id to unmanned object, it should update instead of creating new records.
Note:
1. Merge is working with parent object if it has the id value.
2. I am not adding new item here, I am just updating the existing recorded through merge.
SalesOrder.php
/**
* #Entity #Table(name="sales_orders")
* */
class SalesOrder extends BaseEntity {
/**
* #OneToMany(targetEntity="SalesOrderDetail",cascade="all", mappedBy="salesOrder" )
*/
protected $itemSet;
function __construct() {
$this->itemSet = new ArrayCollection();
}
}
SalesOrderDetail.php
/**
* #Entity #Table(name="sales_order_details")
* */
class SalesOrderDetail extends BaseEntity {
/** #Id #Column(type="integer") #GeneratedValue * */
protected $id;
/**
* #ManyToOne(targetEntity="SalesOrder")
* #JoinColumn(name="order_no", referencedColumnName="order_no")
*/
protected $salesOrder;
}
Debug Mode screen
If I use cascade={"merge"}
I am getting different error if I am using Cascades merge
Type: Doctrine\ORM\ORMInvalidArgumentException Message: Multiple
non-persisted new entities were found through the given association
graph: * A new entity was found through the relationship
'Ziletech\Database\Entity\SalesOrder#itemSet' that was not configured
to cascade persist operations for entity:
Ziletech\Database\Entity\SalesOrderDetail#0000000052218380000000007058b4a6.
To solve this issue: Either explicitly call EntityManager#persist() on
this unknown entity or configure cascade persist this association in
the mapping for example #ManyToOne(..,cascade={"persist"}). If you
cannot find out which entity causes the problem implement
'Ziletech\Database\Entity\SalesOrderDetail#__toString()' to get a
clue. * A new entity was found through the relationship
'Ziletech\Database\Entity\SalesOrder#itemSet' that was not configured
to cascade persist operations for entity:
Ziletech\Database\Entity\SalesOrderDetail#0000000052218071000000007058b4a6.
To solve this issue: Either explicitly call EntityManager#persist() on
this unknown entity or configure cascade persist this association in
the mapping for example #ManyToOne(..,cascade={"persist"}). If you
cannot find out which entity causes the problem implement
'Ziletech\Database\Entity\SalesOrderDetail#__toString()' to get a
clue.
You have a mistake in your mapping, cascade needs an array
/**
* #OneToMany(targetEntity="SalesOrderDetail", cascade={"all"}, mappedBy="salesOrder" )
*/
protected $itemSet;

How do I search by properties of entity which do not have #Column anotation in Doctrine2?

/**
* #ORM\Entity
*/
class Order extends BaseEntity
{
// this is trait for #Id
use Identifier;
/**
* #ORM\Column(type="integer")
*/
protected $costPerUnit;
/**
* #ORM\Column(type="integer")
*/
protected $numberOfUnits;
// i want to search by this property
protected $totalCost;
public function getTotalCost()
{
return $this->numberOfUnits * $this->costPerUnit;
}
}
I have an entity like this and I'd like to be able to do for example
$orderRepository->findOneByTotalCost('999')
$orderRepository->findBy(['totalCost' => '400']);
Is this possible in Doctrine2? Or would I go about it differently?
Like I said in my comments, it's likely you're wrestling with an issue that shouldn't have occurred in the first place. Still, having a SUM value mapped to a property is possible using doctrine, in a variety of ways: Check the aggregate field docs to find out which would solve your problem best.
To my eyes, is that you're using entities as more than what they really are: Entities represent records in a database, Doctrine is a DBAL. Searching data using entities (or repositories) is querying the database. You could solve the problem by adding custom methods to your entity manager or a custom repository class that'll query all of the data required to compute the totalCost value for all entities, and return only those you need. Alternatively, use the connection from your DBAL to query for the id's you're after (for example), then use those values to get to the actual entities. Or, like I said before: use aggregate fields.
The problems you have with the findOneByTotalCost and findBy examples you show is that the first requires you to write a method Called findOneByTotalCost yourself. The problem with your use of findBy is simply that your argument is malformed: the array should be associative: use the mapped column names as keys, and the values are what you want to query for:
$repo->findBy(
['totalCost' => 400]
);
is what you're looking for, not ['totalCost', 400]. As for the entity itself, you'll need to add an annotation:
Yes it is, judging by your use of #ORM\Entity annotations in the doc-blocks, this ought to do it:
/**
* #ORM\Column(type="string", length=255)
*/
protected $regioun = 'Spain';
The update the table, and you'll be able to:
$entities = $repo->findBy(
['region' => 'Spain']
);
Don't forget that this code represents a table in a DB: you can search on any of the fields, but use indexes, which you can do by adding annotations at the top of your class definition:
/**
* #ORM\Table(name="tblname", indexes={
* #ORM\Index(name="region", columns={"region"})
* })
*/
class Foo
{}
As ever: in DB's, indexes matter
You should write a method findOneByTotalCost on your entity repository, something like:
public function findOneByTotalCost ($queryParams){
$query = 'select o
from <yourEntity> o
where o.numberOfUnits * o.costPerUnit = :myParam';
$dql = $this->getEntityManager()->createQuery($query);
$dql->setParameter('myParam', $queryParams);
return $dql ->execute();
}
Them, $orderRepository->findOneByTotalCost('999') should work.

Doctrine Single Table Inheritance Query All Instances Of

I'm working on a notification system, so I have a notification abstract class and sub-classes (forumPostNotification, privateMessageNotification, etc). They are stored using Single Table Inheritance, so they're all in one table with a discriminating field.
I would like to get all the notifications that apply to a user at once, instead of having to query each type of notification individually, however I'm not sure how to do this in DQL/symfony (it would be easy in SQL).
I believe this: (Doctrine 2: how to write a DQL select statement to search some, but not all the entities in a single table inheritance table) is similar to what I'd like to achieve, but I'm not sure how to query the abstract object. It's also not in the Entity directory, but in Entity/Notifications/Notification.php.
I'll add some code for clarification:
Notification.php
/**
* Notification Class
*#ORM\Entity
* #ORM\InheritanceType("SINGLE_TABLE")
* #ORM\DiscriminatorColumn(name="type", type="string")
* #ORM\DiscriminatorMap({
* "notification"="Notification",
* "forumPostNotification"="ForumPostNotification",
* ...
* })
* #ORM\Table(name="notification")
*/
abstract class Notification
{
/**
* #ORM\ManyToOne(targetEntity="Acme\MainBundle\Entity\User", inversedBy="notifications")
* #ORM\JoinColumn(name="user_id", referencedColumnName="id")
*/
private $user;
//...
}
ForumPostNotification.php
/**
* Forum Post Notification
* #ORM\Entity
*/
class ForumPostNotification extends Notification
{
//..
}
PrivateMessageNotification.php
/**
* Private Message Notification
* #ORM\Entity
*/
class PrivateMessageNotification extends Notification
{
//..
}
I'd like to be able to do something like this, one way or another (I understand that I can't query from Notification, since it's an abstract class. I just wrote it like this to convey what I'd like to achieve):
$notifications = $em->createQuery('
SELECT n
FROM AcmeMainBundle:Notification n
WHERE n.dateDeactivated IS NULL
ORDER BY n.dateCreated ASC
')->getResult();
we have created similar situation with orders and products. Because you can have different types of product inside one order we made one parent class Product and inherited ex. SpecialProduct, SalesProduct etc.
We were able to define a relation between Order (in your case User) and "Product" (in your case Notification), and that's all. We get every Products for the order by $order->getProducts(). The method returns us a list of well prepared products with specific classes ex
order->products[SingleProduct, SingleProduct, SingleProduct, SpecialProduct, SalesProduct, SingleProduct]
So, in conclusion. Only one thing you need to do to get all notifications per user is defining a proper relation between your user and abstract parent class.
It was simply, but... it's not so good when you're going to get only notification from specific type. The query passed in your link is not pretty. In my opinion you should create a proper queryBuilder - it's quite similar.
At the end you cannot use the $user->getNotifications(), but you have to get notifications directly from repository -
$em->get('AcmeBundle:User')->getForumPostNotifications()
Kind regards,
Piotr Pasich
This is in fact so simple, I'm amazed they've not documented it properly.
In your repository, do:
return $this->_em->createQueryBuilder()
->select('notification')
->from(Notification::class, 'notification')
// whatever else you need
please notice creating the query builder from entity manager, not entity repository itself

Polymorphic relationships with Doctrine2

How do I create traditional polymorphic relationships with Doctrine 2?
I have read a lot of answers that suggest using Single Table Inheritance but I can't see how this would help in my situation. Here's what I'm trying to do:
I have some utility entities, like an Address, an Email and a PhoneNumber.
I have some 'contactable' entities, like a Customer, Employer, Business. Each of these should contain a OneToMany relationship with the above utility entities.
Ideally, I'd like to create an abstract base class called 'ContactableEntity' that contains these relationships, but I know it is not possible to put OneToMany relationships in mapped superclasses with doctrine-- that's fine.
However, I am still at a loss at how I can relate these without massive redundancy in code. Do I make Address an STI type, with a 'CustomerAddress' subclass that contains the relationship directly to a Customer? Is there no way to reduce the amount of repetition?
Why not just make your base ContactableEntity concrete?
EDIT:
Just did a few experiments in a project I've done that uses CTI. I don't see any reason that the same strategy wouldn't work with STI.
Basically, I have something like:
/**
* Base class for orders. Actual orders are some subclass of order.
*
* #Entity
* #Table(name="OOrder")
* #InheritanceType("JOINED")
* #DiscriminatorColumn(name="discr", type="string")
* #DiscriminatorMap({"CAOrder" = "CAOrder", "AmazonOrder" = "AmazonOrder"})
*/
abstract class Order {
/**
* CSRs can add notes to orders of any type
* #OneToMany(targetEntity = "OrderNote", mappedBy = "order", cascade={"all"})
* #OrderBy({"created" = "ASC"})
*/
protected $notes;
// ...
}
/**
* #Entity
*/
class AmazonOrder extends Order {
/**
* #Column(type="string", length="20")
*/
protected $amazonOrderId;
// ...
}
/**
* #Entity
*/
class OrderNote {
// ...
/**
* #ManyToOne(targetEntity="Order", inversedBy="notes")
*/
protected $order;
// ...
}
And it seems to work exactly as expected. I can get an OrderNote, and it's $order property will contain some subclass of Order.
Is there some restriction on using STI that makes this not possible for you? If so, I'd suggest moving to CTI. But I can't imagine why this wouldn't work with STI.
If the contactable entity shall be abstract (#MappedSuperclass) you'll need to use the ResolveTargetEntityListener provided by Doctrine 2.2+.
It basically allows you to define a relationship by specifying an interface instead of a concrete entity. (Maybe you want to define/inherit several interfaces as you speak of multiple "contactables"). For instance you then can implement the interface in your abstract class or concrete class. Finally you'll need to define/associate the concrete class (entity) to the related interface within the config.yml
An example can be found in the Symfony docs: http://symfony.com/doc/current/cookbook/doctrine/resolve_target_entity.html

Categories