Can a single table inheritance entity extend a class table inheritance entity? - symfony

This is my base/parent entity, setup so its children are using their own tables.
/**
* #ORM\Entity
* #ORM\Table(name="layer_object")
* #ORM\InheritanceType("JOINED")
* #ORM\DiscriminatorColumn(name="discr", type="string")
* #ORM\DiscriminatorMap({"service"="Service", "aircraft"="Aircraft", ...})
*/
class LayerObject {}
Aircraft entity. A simple child that is doing well
/**
* #ORM\Entity
* #ORM\Table(name="aircraft")
*/
class Aircraft extends LayerObject
Service entity. A complex child, that itself is using single table inheritance.
/**
* #ORM\Entity
* #ORM\Table(name="service")
* #ORM\InheritanceType("SINGLE_TABLE")
* #ORM\DiscriminatorColumn(name="discr", type="string")
* #ORM\DiscriminatorMap({"ground"="Ground", "onboard"="Onboard"})
*/
class Service extends LayerObject {}
A child of the Service entity
/**
* #ORM\Entity
*/
class Ground extends Service {}
app/console doctrine:schema:validate finds no errors but app/console doctrine:schema:update --force just won't generate the 'service' table, the one that should use single table inheritance. Seems like the service entity definition is simply ignored.
Sure I could create the SQL for this table by hand, but the application will grow and at some point I will need to use migrations.
Could anyone point me in some direction? Thanks.
Found a duplicate, but there are no answers so far, see: Doctrine 2 multiple level inheritance
Edit:
When I use class table inheritance for the 2nd level too (#ORM\InheritanceType("JOINED") for the Service entity) it works pretty well. See: Doctrine2 Multiple level inheritance

What you're trying to achieve is not possible with pure mapping.
The documentation for Class Table Inheritance and Single Table Inheritance clearly state:
The #InheritanceType, #DiscriminatorColumn and #DiscriminatorMap must
be specified on the topmost class that is part of the mapped entity
hierarchy.
You might be able to make this work by implementing a subscriber to the loadClassMetaData event that changes the inheritance-type dynamically (i.e. based on annotations on of the child entities).
Some further inspiration can be found in this article.

Related

Is it possible to use a custom naming strategy for auto generated Doctrine DiscriminatorMaps?

I am working a custom bundle which should be used in different Symfony 5+ projects. The bundle includes some entities which inherit from the same Mapped Superclass:
/**
* #ORM\InheritanceType("SINGLE_TABLE")
* #ORM\Table(name="vehicles")
* #ORM\DiscriminatorColumn(name="vehicle_type", type="string")
* #ORM\DiscriminatorMap({
* "race_car" = "RaceCar",
* "sailboat" = "Sailboat",
* ...})
*/
abstract class Vehicle {
/** #Column(type="string") */
protected $color;
// ... more fields and methods
}
/**
* #ORM\Entity
*/
class RaceCar extends Vehicle {
...
}
/**
* #ORM\Entity
*/
class SailBoat extends Vehicle {
...
}
This works fine and it is no problem to use a custom naming strategy (or completely custom type names) in the DiscriminatorMap.
Now I would like to add more Vehicle sub classes to a project which uses the bundle. Since it seems not to be possible to extend the DiscriminatorMap outside its original definition (is it?), I switched to not specifying a map manually but letting Doctrine build it automatically:
If no discriminator map is provided, then the map is generated
automatically. The automatically generated discriminator map contains
the lowercase short name of each class as key.
While this works in a new project, all existing entries in the database of an existing project already use the custom naming. This would conflict with the auto generated lowercase naming: e.g. race_car vs. racecar.
Is it possible to provide a custom naming strategy which is than be used by Doctrine to build the map?
Or do I have to manually update the database by replacing the old mapping names with the new (lowercase) ones?

How to customize an entity property in Sylius?

I'm working on a Sylius application and want to modify a property of an entity.
To be more concrete: What I want to achieve, is to make the ProductVariant.onHand (or actually the corresponding column in the database) nullable.
The documentation of Sylius provides an auspicious article "Customizing Models". But it doesn't describe, how to change the definition of an existing property.
How to modify a property of a Sylius (Core) entity like ProductVariant.onHand?
What I tried so far: I extended the Sylius\Component\Core\Model\ProductVariant and added a Doctrine annotation to the onHand property:
/**
* #ORM\Entity
* #ORM\Table(name="sylius_product_variant")
*/
class ProductVariant extends BaseProductVariant
{
...
/**
* ...
* #ORM\Column(type="integer", nullable=true)
*/
protected $onHand = 0;
...
}
Well, extending the class was definitely a correct step. And it also worked correctly:
$ bin/console debug:container --parameter=sylius.model.product_variant.class
------------------------------------ -----------------------------------
Parameter Value
------------------------------------ -----------------------------------
sylius.model.product_variant.class App\Entity\Product\ProductVariant
------------------------------------ -----------------------------------
But the naïve adding of the property definition led to an error:
$ ./bin/console doctrine:schema:validate
Property "onHand" in "App\Entity\Product\ProductVariant" was already declared, but it must be declared only once
Looks like ProductVariant has it's mapping in config files.
If a bundle defines its entity mapping in configuration files instead of annotations, you can override them as any other regular bundle configuration file. The only caveat is that you must override all those mapping configuration files and not just the ones you actually want to override.
https://symfony.com/doc/4.4/bundles/override.html#entities-entity-mapping
You could also try to create a new entity with the desired mapping (you will need to add all of the columns yourself) and point sylius.model.product_variant.class to this new class.
Entity configs can be overridden by adding the AttributeOverrides annotation:
/**
* #ORM\Entity
* #ORM\Table(name="sylius_product_variant")
* #ORM\AttributeOverrides({
* #ORM\AttributeOverride(
* name="onHand",
* column=#ORM\Column(
* name="on_hand",
* type="integer",
* nullable=true
* )
* )
* })
*/
class ProductVariant extends BaseProductVariant
{
...
/**
* ...
* no changes
*/
protected $onHand;
...
}
Relevant Doctrine docu articles:
Override Field Association Mappings In Subclasses
Inheritance Mapping -> Overrides

Entity associated with non-entity

I have an interface SupplierInterface with 2 implementations: B2BSupplier (a Doctrine entity), RetailSupplier (a static object).
<?php
namespace MyBundle\Model;
interface SupplierInterface {
const B2B = 'B2B';
const RETAIL = 'Retail';
/**
* #return string
*/
public function getSupplierType();
/**
* #return string
*/
public function __toString();
}
Another entity, Supply has a many-to-one relationship with a Supplier. Normally this isn't problematic. But because RetailSupplier is not a Doctrine entity, I'm a bit flummoxed about how to proceed.
Supply looks like this:
<?php
namespace MyBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Validator\Constraints as Assert;
use Gedmo\Blameable\Traits\BlameableEntity;
use Gedmo\Timestampable\Traits\TimestampableEntity;
/**
* Supply
*
* #ORM\Table(name="cir_supply")
* #ORM\Entity()
*/
class Supply
{
use BlameableEntity;
use TimestampableEntity;
/**
* #var int
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\ManyToOne(targetEntity="B2BSupplier")
* #ORM\JoinColumn(name="supplier_id", referencedColumnName="id", nullable=true)
*/
protected $supplier; // <-- PROBLEM, since supplier could be B2BSupplier entity, or it could be vanilla object RetailSupplier
/**
* #ORM\ManyToOne(targetEntity="Chemical", inversedBy="supplies")
* #ORM\JoinColumn(name="chemical_id", referencedColumnName="id", nullable=false)
*/
protected $chemical;
/**
* #ORM\Column(name="external_id", type="string")
*/
protected $externalId;
//getters and setters ...
}
How do I specify a Doctrine relationship when that relationship might not always be valid?
From my experience I'm 99% sure you can't do what you want in your current setup. That being said, there are a few workarounds I can think of. Also before I go into the workarounds. You should think if you really want OneToOne relation on 'supplier' or will ManyToOne work better. OneToOne has some Lazy loading issues and also Workaround 3 work better with ManyToOne.
Workaround 1:
Remove the relation and make the supplier filed contain the id, without having a relation defined.
Extend SupplierRepository 'find' method to handle the cases where id is
2.1 'null' there is no relation in witch case it returns RetailSupplier
2.2 call parent::find for all other cases
2.3 Optional: if null relations are required change 2.1 to use '0' instead of null (adds con 3)
Pros:
fast to achieve from your current setup
keep database foreign key (if step 2.3 is ignored)
Cons:
hidden behavior of the 'find' method
you loose the your doctrine relation
not scalable for other types of Suppliers
source of the information is split between the app and the database
if step 2.3 is required, you loose database foraign key ('0' will not be a foraign key)
Workaround 2:
Modify getSupplier to return RetailSupplier if $this->supplier is null
Modify setSupplier to set null if $supplier is instance of RetailSupplyer
Optinal: Change the first 2 steps to handle '0' as RetailSupplyer and 'null' as no relation
Pros:
fast to achieve from your current setup
keep database foreign key (if step 3 is ignored)
keep doctrine relation
Cons:
hidden behavior of the setter and getter
not scalable for other types of Suppliers
if step 3 is required, you loose database foraign key ('0' will not be a foraign key)
source of the information is split between the app and the database
Workaround 3 (doctrine inheritance mapping):
Create an abstract (called Supplier) this will be inherited by RetailSupplyer and B2BSupplier
Add inheritance metadata to Supplier abstract something like this
Create an entity for RetailSupplyer and a database table with one single line to start (the first RetailSupplier)
Change your database to match your inheritance mapping settings (for more info http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/inheritance-mapping.html)
Change your relation to ManyToOne on $supplier and make it to point to Supplier
Pros:
source of the information is only the database
no hidden behavior in your code
scalable for other types of suppliers and other more retail suppliers
Cons:
harder to achieve from your current setup (database changes, new doctrine setup, possibly some refactor)
pros/cons: Depending on the selected inheritance type you can have full relation path in your database (with foraign key), or you can have no relations. This is up to you ;) after you read the documentation for inheritance mapping.
PS: If I had to choose i will go with Workaround 3. It is hardest to achieve, but solid do it.
Hope this helps and happy coding
Alexandru Cosoi

Use Doctrine to merge entity with different subclass

I have a mapped superclass AbstractQuestion with single-table-inheritance.
/**
* #ORM\Entity
* #ORM\MappedSuperclass
* #ORM\Table(name="Question")
* #ORM\InheritanceType("SINGLE_TABLE")
* #ORM\DiscriminatorColumn(name="dtype", type="string")
* #ORM\DiscriminatorMap({
* "simple": "SimpleQuestion",
* "dropdown": "DropdownQuestion"
* })
*/
abstract class AbstractQuestion
SimpleQuestion and DropdownQuestion inherit from this superclass.
/**
* Class SimpleQuestion.
* #ORM\Entity()
*/
class SimpleQuestion extends AbstractQuestion
I want to modify an existing SimpleQuestion and make it a DropdownQuestion.
When saving a question, I deserialise and merge the question, which contains an ID and the 'dtype' and other properties.
$dquestion = $this->serial->fromJson($request->getContent(), AbstractQuestion::class);
$question = $this->em->merge($dquestion);
$this->em->flush();
So I submit something like:
{ id: 12, dtype: 'dropdown', 'text': 'What is my favourite animal?'}
After the deserialisation, $dquestion is a DropdownQuestion object as I desired, but after the merge $question is a SimpleQuestion object as it was in the database previously, so any unique properties of DropdownQuestion are lost and the question is saved as a SimpleQuestion. Is there any way to work around this?
You will first have to delete the existing record (SimpleQuestion) and then insert the new record (DropdownQuestion). Type casting is not supported in Doctrine 2.
Note.
You can probably change the discriminator column with a pure SQL query, but this is absolutely not recommended and will for sure give you problems...
Check also the answers here and here since they might be interesting for you.

Symfony: How do I annotate entity properties that are objects to get Doctrine to store a foreign key?

I'm still getting to grips with Symfony and Doctine and I appreciate this might sound overly simple.
I have at present two basic entities: WebSite (having id and canonicalUrl properties) and Job which has, as one property, a WebSite.
A Job has one WebSite; a WebSite can be referenced by many Jobs. Both are under the same namespace.
Relevant here is the Job entity:
/**
*
* #ORM\Entity
*/
class Job
{
/**
*
* #var integer
*
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
*
* #var WebSite
*/
protected $website;
}
In database terms, a persisted Job should be storing the id of the relevant WebSite.
Without any changes to the above, calling php app/console doctrine:migrations:diff generates a new migration for a table named Job with a single id field.
How do I annotate Job::website such that Doctrine knows to create an integer field and to get the value as the id of the Website object?
You must explicitly define the relationship. The shortest would be
/**
* #ORM\Entity
*/
class Job
{
/**
* #var WebSite
*
* #ORM\ManyToOne(targetEntity="Website")
*/
protected $website;
}
However, should you find yourself wanting to tweak the relationship to better suit your needs, have a look at the annotation reference (ManyToOne and JoinColumn for this particular case). There's also quite a comprehensive article about association mapping, which you might find interesting.

Resources