How to associate Doctrine entities with each other so, that there are optional references between the two so that only part of the composite reference is null?
Consider three tables and their Doctrine entities:
Company
id | name
---+-----
01 | Foo
02 | Bar
Position
id | company_id | name
---+------------+------------
01 | 01 | Chef
02 | 01 | Waitress
01 | 02 | Captain
02 | 02 | Mechanic
User
id | company_id | main_position_id | alternate_position_id
---+------------+------------------+-----------------------
01 | 01 | 01 | null
02 | 01 | 02 | null
03 | 01 | 02 | null
04 | 01 | 01 | 02
05 | 02 | 01 | 02
06 | 02 | 02 | null
07 | 02 | 02 | null
All three have Doctrine entities mapped for them:
use Doctrine\ORM\Mapping as ORM;
class Company {
/**
* #ORM\Id()
* #ORM\GeneratedValue(strategy="NONE")
* #ORM\Column()
*/
private $id;
/**
* #ORM\Column()
*/
private $name;
}
Position
class Position {
/**
* #ORM\Id()
* #ORM\GeneratedValue(strategy="NONE")
* #ORM\Column()
*/
private $id;
/**
* #ORM\Id()
* #ORM\GeneratedValue(strategy="NONE")
* #ORM\Column()
*/
private $company_id;
/**
* #ORM\ManyToOne(TargetEntity="Company")
* #ORM\JoinColumn(name="company_id", referencedColumnName="id")
*/
private $company;
/**
* #ORM\Column()
*/
private $name;
}
User
class User {
/**
* #ORM\Id()
* #ORM\GeneratedValue(strategy="NONE")
* #ORM\Column()
*/
private $id;
/**
* #ORM\Column()
*/
private $company_id;
/**
* #ORM\Column()
*/
private $main_position_id;
/**
* #ORM\Column()
*/
private $alternate_position_id;
/**
* #ORM\ManyToOne(TargetEntity="Position")
* #ORM\JoinColumns(
* #ORM\JoinColumn(name="company_id", referencedColumnName="company_id")
* #ORM\JoinColumn(name="main_position", referencedColumnName="id")
* )
*/
private $main_position;
/**
* #ORM\ManyToOne(TargetEntity="Position")
* #ORM\JoinColumns(
* #ORM\JoinColumn(name="company_id", referencedColumnName="company_id")
* #ORM\JoinColumn(name="alternate_position", referencedColumnName="id", nullable=true)
* )
*/
private $alternate_position;
}
Above works as expected for $main_position and but will fail with OutOfBoundsException "Missing value for primary key id on Position" if $alternate_position_id is null. Database structure is given and cannot be altered.
Expectation is that $alternate_position should either be valid Position entity if defined, or null if the value in database is null.
Update: As a workaround I added a PostLoad() LifeCycleEvent to load the associations on the fly, if needed, but that still comes with an unfortunate performance impact.
Update: So far the cleanest option seems to be to remove the #ORM\ManyToOne(targetEntity="xx") and replace it with #ORM\Column(). This will prevent associations from hydrating, and thus throwing, when entity is instantiated and will allow the getters to load the associated entities on the fly. Downside of this is that it does require access to the entity manager. One option to access EntityManager is to add a "PostLoad" Doctrine Lifecycle Event to the entity:
/** #ORM\PostLoad() */
public function postLoadHandler(LifecycleEventArgs $event) {
$this->positionRepository=$event->getEntityManager()->getRepository(Position::class);
}
public function getAlternatePosition() {
return $this->positionRepository->findOneBy(['company_id' => $this->company_id, 'id' => $this->alternate_position_id]);
}
This does come with significant performance impact, though.
As you know, your tables are using a composite primary/foreign keys. So if you use
* #ORM\JoinColumns(
* #ORM\JoinColumn(name="company_id", referencedColumnName="company_id")
* #ORM\JoinColumn(name="alternate_position", referencedColumnName="id", nullable=true)
* )
And the values of the columns "alternate_position" and "company_id" are null and "01" respectively, Doctrine will look for a row with the following values:
id | company_id | name
-----+------------+------------------------------
null | 01 | <Name value in your database>
Even so, you can use criteria API to filter the values and return your expected value:
<?php
use Doctrine\Common\Collections\Criteria;
class User
{
//...More code
/**
* #ORM\ManyToOne(TargetEntity="Position")
*/
private $alternate_position;
public function getAlternatePosition()
{
$criteria = Criteria::create();
$criteria = $criteria->leftJoin(...);
$criteria = $criteria->where(...);
return $this->alternate_position->matching($criteria);
}
}
References:
https://github.com/doctrine/orm/issues/6647
Doctrine2 association mapping with conditions
Related
In my table the id is auto increment. And cid and dsid can not be NULL. On insert i need somehow to insert in dsid the value of the auto increment cid. How can i manage this in doctrine ORM / Symfony?
Table:
--------------------------------------
cid | dsid | lid
--------------------------------------
Entity:
/**
* #var int
*
* #ORM\Column(name="CID", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $cid;
/**
* #var int
*
* #ORM\Column(name="DSID", type="integer", nullable=false)
*/
private $dsid;
/**
* #var int
*
* #ORM\Column(name="LID", type="integer", nullable=false)
*/
private $lid;
Id generation takes place on database side during inserting, it has nothing to do with Doctrine. You can implement trigger on database side. or make fields nullable to insert a record, and then have postFlush event that will set those fields equal to ID.
I have two entities: People and Document
A people can have n childrens:
/**
*
* #ORM\ManyToOne(targetEntity="People")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="dad_id", referencedColumnName="id")
* })
*/
private $dad;
/**
*
* #ORM\OneToMany(targetEntity="People", mappedBy="dad")
*/
private $childrens;
And a people can have n Documents:
DocumentEntity:
/**
*
* #ORM\ManyToOne(targetEntity="People")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="people_id", referencedColumnName="id")
* })
*/
private $people;
I want to get all peoples (and their children) and their documents of type License in one query:
$qb = $this->em->getRepository('People')->createQueryBuilder('p');
$qb
->select(array('p','d'))
->leftJoin('p.documents','d','WITH','d.type = :type')
->setParameter('type','license')
;
$peoples = $qb->getQuery()->getResult();
Fine, if i call $people->getDocuments() returns only Documents of type 'License'. But when i do this '$people->getChildrens()[0]->getDocuments()' returns for me all Documents of this people.
How i can do a 'left join WITH' cascading on a tree structure?
My default locale is pl_PL. When i switch to en_US the following code gives expected result:
// locale set to en_US
$product = $em->getRepository('model\Product')->find(1);
$category = $em->getRepository('model\ProductCategory')->find(1);
echo $product->getName();
echo $category->getName();
// result
beach ball
summer
But when I want to get category by model\Product association it's not translated:
// locale set to en_US
$product = $em->getRepository('model\Product')->find(1);
echo $product->getName();
echo $product->getCategories()->first()->getName();
// result
beach ball
lato - pl_PL instead of en_US
Is it translatable extension bug or is there something wrong in my code?
Dump:
select id, name from products; select object_id, locale, field, content from products_translations;
id | name
----+---------------
1 | pilka plazowa
object_id | locale | field | content
-----------+--------+-------+------------
1 | en_US | name | beach ball
select id, name from products_categories; select object_id, locale, field, content from products_categories_translations;
id | name
----+------
1 | lato
object_id | locale | field | content
-----------+--------+-------+---------
1 | en_US | name | summer
model\Product
/**
* #ORM\Table("products")
* #ORM\Entity(repositoryClass="repository\TranslatableRepository")
* #Gedmo\TranslationEntity(class="model\ProductTranslation")
*/
class Product {
/**
* #Gedmo\Translatable
* #ORM\Column(type="string", length=255)
*/
protected $name;
/**
* #ORM\OneToMany(targetEntity="model\ProductCategory", mappedBy="product")
*/
protected $category_list;
}
model\ProductCategory
/**
* #ORM\Table("products_categories")
* #Gedmo\TranslationEntity(class="model\ProductCategoryTranslation")
* #ORM\Entity(repositoryClass="repository\TranslatableRepository")
*/
class ProductCategory {
/**
* #Gedmo\Translatable
* #ORM\Column(type="string", length=255)
*/
protected $name;
}
I have two entities with a many-to-many relationship (Scenario and Step). I needed to save the order of the steps within a scenario so i added a third entity called ScenarioToStep with a displayOrder property.
class ScenarioToStep
{
/**
* #ORM\Id
*
* #ORM\ManyToOne(targetEntity="Hakisa\Bundle\GettingStartedBundle\Entity\Scenario", inversedBy="scenarioToStep")
* #ORM\JoinColumn(name="scenario_id", referencedColumnName="id")
*
* #var Hakisa\Bundle\GettingStartedBundle\Entity\Scenario
*/
private $scenario;
/**
* #ORM\Id
*
* #ORM\ManyToOne(targetEntity="Hakisa\Bundle\GettingStartedBundle\Entity\Step", inversedBy="stepToScenario")
* #ORM\JoinColumn(name="step_id", referencedColumnName="id")
*
* #var Hakisa\Bundle\GettingStartedBundle\Entity\Step
*/
private $step;
/**
* Order of the step in the scenario
*
* #ORM\Id
*
* #ORM\Column(type="integer")
*
* #var integer
*/
private $displayOrder;
The 3 properties makes the primary key so i can have a step multiple times in a scenario
I have a UI where i can add steps via drag n drop, remove steps or sort step. This UI send an array to PHP where the index is the position and the value is the id of the step.
array(
0 => 1,
1 => 2,
2 => 3,
3 => 1,
4 => 4
)
I want to update my Scenario entity to match this new ordering. I first tried something like:
$scenario->getScenarioToStep()->clear();
foreach ($tabIds as $pos => $stepId) {
$sts = new ScenarioToStep();
$sts->setScenario($scenario)->setStep($step)->setOrder($pos);
$scenario->getScenarioToStep()->add($sts);
}
but i end up with duplicate key errors
if i do similar thing but instead of instanciating a new scenario i ask entity manager for a reference (proxy) then the association table is always empty
Thanks to anyone who has an idea on how i should perform my save
I'm working with a Legacy database (osCommerce-based and heavily hacked), which contains a table called orders_total, defined as follows:
CREATE TABLE `orders_total` (
`orders_total_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`orders_id` int(11) unsigned NOT NULL DEFAULT '0',
`title` varchar(255) NOT NULL,
`text` varchar(255) NOT NULL,
`value` decimal(15,4) NOT NULL DEFAULT '0.0000',
`class` varchar(40) NOT NULL,
`sort_order` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`orders_total_id`),
KEY `idx_order_id` (`orders_id`)
);
The "class" column defines the type of "total" that the row represents. It's a bit of a mess in that some "totals" are not totals at all, but surcharges, and some are discounts. An example:
+-----------------+-----------+--------------+----------+-----------+----------------------+------------+
| orders_total_id | orders_id | title | text | value | class | sort_order |
+-----------------+-----------+--------------+----------+-----------+----------------------+------------+
| 781797 | 190000 | Sub-Total: | $1427.29 | 1427.2916 | ot_subtotal | 1 |
| 781798 | 190000 | Courier: | $172.05 | 172.0500 | ot_shipping | 2 |
| 781799 | 190000 | Insurance: | $47.62 | 47.6200 | ot_insurance | 3 |
| 781800 | 190000 | Visa/MC Fee: | $41.80 | 41.8000 | ot_surcharge_visa_mc | 4 |
| 781801 | 190000 | <b>Total:</b>| $1688.76 | 1688.7616 | ot_total | 10 |
+-----------------+-----------+--------------+----------+-----------+----------------------+------------+
What I'd like to do is use single table inheritance to split out the table classes into OrderTotal, OrderCharge and OrderDiscount entities, and then through one to many relationships with the Order entity be able to pull up each collection. I've tried the following:
<?php
namespace Shop\Entity;
use Application\Entity\BaseEntity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\InheritanceType("SINGLE_TABLE")
* #ORM\DiscriminatorColumn(name="class", type="string")
* #ORM\DiscriminatorMap({
* "ot_cc_surcharge" = "OrderCharge",
* "ot_coupon" = "OrderDiscount",
* "ot_insurance" = "OrderCharge",
* "ot_payment_surcharge" = "OrderCharge",
* "ot_paypal_surcharge" = "OrderCharge",
* "ot_shipping" = "OrderCharge",
* "ot_subtotal" = "OrderTotal",
* "ot_surcharge" = "OrderCharge",
* "ot_surcharge_amex" = "OrderCharge",
* "ot_surcharge_paypal" = "OrderCharge",
* "ot_surcharge_visa_mc" = "OrderCharge",
* "ot_total" = "OrderTotal"
* })
* #ORM\Table(name="orders_total")
*
*/
class OrderTotalBase extends BaseEntity {
/*....*/
}
/**
* #ORM\Entity
* #ORM\Table(name="orders_total")
*/
class OrderCharge extends OrderTotalBase {
}
/**
* #ORM\Entity
* #ORM\Table(name="orders_total")
*/
class OrderTotal extends OrderTotalBase {
}
/**
* #ORM\Entity
* #ORM\Table(name="orders_total")
*/
class OrderDiscount extends OrderTotalBase {
}
/*....*/
class Order {
/*....*/
/**
* #ORM\OneToMany(targetEntity="OrderTotal", mappedBy="order", fetch="EAGER")
* #var PersistentCollection
*/
protected $orderTotals;
/**
* #ORM\OneToMany(targetEntity="OrderCharge", mappedBy="order", fetch="EAGER")
* #var PersistentCollection
*/
protected $orderCharges;
/**
* #ORM\OneToMany(targetEntity="OrderDiscount", mappedBy="order", fetch="EAGER")
* #var PersistentCollection
*/
protected $orderDiscounts;
}
Then added the respective getters and setters to the Order class.
Problem is, when I call $order->getOrderCharges(), it returns the entire list of records above all as OrderCharge objects. When I call $order->getOrderTotals() I get exactly the same results but with OrderTotal objects.
Does STI allow me to only get the correct objects according to the #DiscriminatorMap?
While (as explained above) your mappings are invalid, there is a way of fetching all entities in an inheritance according to the discriminator column in DQL:
SELECT r FROM RootEntity r WHERE r INSTANCE OF SubType
Or simply select the correct type with DQL:
SELECT e FROM SubType e
For your OrderTotal case, the association should be
/**
* #ORM\OneToMany(targetEntity="OrderTotal", mappedBy="totalOrder")
* #var \Doctrine\Common\Collections\Collection
*/
protected $orderTotals;
Once you have fixed your mappings and the CLI validator confirms it (orm:validate-schema), that collection will be correctly filtered