How can I define a Doctrine property in a parent class and override the association in a class which extends the parent class? When using annotation, this was implemented by using AssociationOverride, however, I don't think they are available when using PHP 8 attributes
Why I want to:
I have a class AbstractTenantEntity whose purpose is to restrict access to data to a given Tenant (i.e. account, owner, etc) that owns the data, and any entity which extends this class will have tenant_id inserted into the database when created and all other requests will add the tenant_id to the WHERE clause. Tenant typically does not have collections of the various entities which extend AbstractTenantEntity, but a few do. When using annotations, I handled it by applying Doctrine's AssociationOverride annotation to the extended classes which should have a collection in Tenant, but I don't know how to accomplish this when using PHP 8 attributes?
My attempt described below was unsuccessful as I incorrectly thought that the annotation class would magically work with attributes if modified appropriately, but now I see other code must be able to apply the appropriate logic based on the attributes. As such, I abandoned this approach and just made the properties protected and duplicated them in the concrete class.
My attempt:
Tenant entity
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\OneToMany;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
#[Entity()]
class Tenant
{
#[Id, Column(type: "integer")]
#[GeneratedValue]
private ?int $id = null;
#[OneToMany(targetEntity: Asset::class, mappedBy: 'tenant')]
private array|Collection|ArrayCollection $assets;
// Other properties and typical getters and setters
}
AbstractTenantEntity entity
use Doctrine\ORM\Mapping\ManyToOne;
use Doctrine\ORM\Mapping\JoinColumn;
abstract class AbstractTenantEntity implements TenantInterface
{
/**
* inversedBy performed in child where required
*/
#[ManyToOne(targetEntity: Tenant::class)]
#[JoinColumn(nullable: false)]
protected ?Tenant $tenant = null;
// Typical getters and setters
}
This is the part which has me stuck. When using annotation, my code would be as follows:
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity()
* #ORM\AssociationOverrides({
* #ORM\AssociationOverride(name="tenant", inversedBy="assets")
* })
*/
class Asset extends AbstractTenantEntity
{
// Various properties and typical getters and setters
}
But AssociationOverrides hasn't been modified to work with attributes, so based on the official class, I created my own class similar to the others which Doctrine has updated:
namespace App\Mapping;
use Attribute;
use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor;
use Doctrine\ORM\Mapping\Annotation;
/**
* This annotation is used to override association mapping of property for an entity relationship.
*
* #Annotation
* #NamedArgumentConstructor()
* #Target("ANNOTATION")
*/
#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
final class AssociationOverride implements Annotation
{
/**
* The name of the relationship property whose mapping is being overridden.
*
* #var string
*/
public $name;
/**
* The join column that is being mapped to the persistent attribute.
*
* #var array<\Doctrine\ORM\Mapping\JoinColumn>
*/
public $joinColumns;
/**
* The join table that maps the relationship.
*
* #var \Doctrine\ORM\Mapping\JoinTable
*/
public $joinTable;
/**
* The name of the association-field on the inverse-side.
*
* #var string
*/
public $inversedBy;
/**
* The fetching strategy to use for the association.
*
* #var string
* #Enum({"LAZY", "EAGER", "EXTRA_LAZY"})
*/
public $fetch;
public function __construct(
?string $name = null,
?array $joinColumns = null,
?string $joinTable = null,
?string $inversedBy = null,
?string $fetch = null
) {
$this->name = $name;
$this->joinColumns = $joinColumns;
$this->joinTable = $joinTable;
$this->inversedBy = $inversedBy;
$this->fetch = $fetch;
//$this->debug('__construct',);
}
private function debug(string $message, string $file='test.json', ?int $options = null)
{
$content = file_exists($file)?json_decode(file_get_contents($file), true):[];
$content[] = ['message'=>$message, 'object_vars'=>get_object_vars($this), 'debug_backtrace'=>debug_backtrace($options)];
file_put_contents($file, json_encode($content, JSON_PRETTY_PRINT));
}
}
When validating the mapping, Doctrine complains that target-entity does not contain the required inversedBy. I've spent some time going through the Doctrine source code but have not made much progress.
Does my current approach have merit and if so please fill in the gaps. If not, however, how would you recommend meeting this need?
It has been resolved by this pr: https://github.com/doctrine/orm/pull/9241
ps: PHP 8.1 is required
#[AttributeOverrides([
new AttributeOverride(
name: "id",
column: new Column(name: "guest_id", type: "integer", length: 140)
),
new AttributeOverride(
name: "name",
column: new Column(name: "guest_name", nullable: false, unique: true, length: 240)
)]
)]
Override Field Association Mappings In Subclasses
Sometimes there is a need to persist entities but override all or part of the mapping metadata. Sometimes also the mapping to override comes from entities using traits where the traits have mapping metadata. This tutorial explains how to override mapping metadata, i.e. attributes and associations metadata in particular. The example here shows the overriding of a class that uses a trait but is similar when extending a base class as shown at the end of this tutorial.
Suppose we have a class ExampleEntityWithOverride. This class uses trait ExampleTrait:
<?php
/**
* #Entity
*
* #AttributeOverrides({
* #AttributeOverride(name="foo",
* column=#Column(
* name = "foo_overridden",
* type = "integer",
* length = 140,
* nullable = false,
* unique = false
* )
* )
* })
*
* #AssociationOverrides({
* #AssociationOverride(name="bar",
* joinColumns=#JoinColumn(
* name="example_entity_overridden_bar_id", referencedColumnName="id"
* )
* )
* })
*/
class ExampleEntityWithOverride
{
use ExampleTrait;
}
/**
* #Entity
*/
class Bar
{
/** #Id #Column(type="string") */
private $id;
}
The docblock is showing metadata override of the attribute and association type. It basically changes the names of the columns mapped for a property foo and for the association bar which relates to Bar class shown above. Here is the trait which has mapping metadata that is overridden by the annotation above:
<?php
/**
* Trait class
*/
trait ExampleTrait
{
/** #Id #Column(type="string") */
private $id;
/**
* #Column(name="trait_foo", type="integer", length=100, nullable=true, unique=true)
*/
protected $foo;
/**
* #OneToOne(targetEntity="Bar", cascade={"persist", "merge"})
* #JoinColumn(name="example_trait_bar_id", referencedColumnName="id")
*/
protected $bar;
}
The case for just extending a class would be just the same but:
<?php
class ExampleEntityWithOverride extends BaseEntityWithSomeMapping
{
// ...
}
Overriding is also supported via XML and YAML (examples).
When clicking the "Edit" link in EasyAdmin's list view of an entity that contains a field with type="date", I'm getting this error message:
Unable to transform value for property path "birthday": Expected a string.
I have this in my entity:
/**
* #ORM\Column(type="date")
* #Assert\NotBlank()
* #Assert\Date()
*/
private $birthday;
There are 2 solutions.
Quick and dirty (Symfony < 5)
Change this in config/packages/easy_admin.yaml:
easy_admin:
entities:
MyEntity:
form:
fields:
- { property: 'birthday', type: 'date' }
See https://symfony.com/doc/master/bundles/EasyAdminBundle/book/edit-new-configuration.html#the-special-form-view for further configuration details.
Quick and clean
#Assert\Date() will be deprecated for type="date" fields in Symfony 4.2 (and thus probably removed in Symfony 5). The validation relies on the \DateTimeInterface type hint of the setter. In total:
/**
* #ORM\Column(type="date")
* #Assert\NotBlank()
*/
private $birthday;
public function setBirthday(?\DateTimeInterface $birthday): self
{
// ...
return $this;
}
See https://github.com/EasyCorp/EasyAdminBundle/issues/2381 for some background information.
I'm trying to extend Sylius\Component\Core\Model\Taxon by adding new data fields. The same procedure did work on another model outside of Sylius Core. When running doctrine:migrations:diff, the error message is "The table with name 'sylius_dev.sylius_taxon' already exists."
The response for php bin/console debug:container --parameter=sylius.model.taxon.class does not change at all.
This is my new class in /src/AppBundle/Entity/FooTaxon.php:
<?php
namespace AppBundle\Entity;
use Sylius\Component\Core\Model\Taxon as BaseTaxon;
class FooTaxon extends BaseTaxon
{
/**
* #var string
*/
private $field_one;
/**
* #return string
*/
public function getFieldOne(): string
{
return $this->field_one;
}
/**
* #param string $new_value
*/
public function setFieldOne(string $new_value): void
{
$this->field_one = $new_value;
}
/**
* #var int
*/
private $field_two;
/**
* #return int
*/
public function getFieldTwo(): int
{
return $this->field_two;
}
/**
* #param int $new_value
*/
public function setFieldTwo(int $new_value): void
{
$this->field_two = $new_value;
}
}
This is my /src/AppBundle/Resources/config/doctrine/FooTaxon.orm.yml:
AppBundle\Entity\FooTaxon:
type: entity
table: sylius_taxon
fields:
field_one:
type: string
nullable: false
field_two:
type: integer
nullable: false
And here is the new entry in /app/config/config.yml:
sylius_core:
resources:
product_taxon:
classes:
model: AppBundle\Entity\FooTaxon
Any help would be appreciated since I'm new to both Symfony and Sylius.
You should use this instead of sylius_core node:
sylius_taxonomy:
resources:
taxon:
classes:
model: AppBundle\Entity\FooTaxon
And better use upperCase in entity property names instead of snake_case.
I'm using EasyAdminBundle for entity management and to upload images I want to useVichUploaderBundle.
Following the documentation configure the Bundle:
https://github.com/javiereguiluz/EasyAdminBundle/blob/master/Resources/doc/integration/vichuploaderbundle.rst
I do not use annotations but yml as described in the documentation:
https://github.com/dustin10/VichUploaderBundle/blob/master/Resources/doc/mapping/yaml.md
My code looks like this:
//app/config/config.yml
vich_uploader:
db_driver: orm
mappings:
torneo_images:
uri_prefix: '%app.path.torneo_images%'
upload_destination: '%kernel.root_dir%/../web/uploads/images/torneos'
..........
easy_admin:
form:
fields:
- logo
- { property: 'imageFile', type: 'file' }
The yml configuration file:
//BackendBundle/Resources/config/doctrine/Torneos.orm.yml
......
logo:
type: string
nullable: true
length: 255
options:
fixed: false
imageFile:
mapping: torneo_images
filename_property: logo
Add to Entity
//BackendBundle/Entity/Torneos.orm.yml
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\Validator\Constraints as Assert;
use Vich\UploaderBundle\Mapping\PropertyMapping as Vich;
namespace BackendBundle\Entity;
.......
/**
* #var string
*/
private $logo;
/**
* #var File
*/
private $imageFile;
.......
/**
* Set logo
*
* #param string $logo
*
* #return Torneos
*/
public function setLogo($logo)
{
$this->logo = $logo;
return $this;
}
/**
* Get logo
*
* #return string
*/
public function getLogo()
{
return $this->logo;
}
/**
* If manually uploading a file (i.e. not using Symfony Form) ensure an instance
* of 'UploadedFile' is injected into this setter to trigger the update. If this
* bundle's configuration parameter 'inject_on_load' is set to 'true' this setter
* must be able to accept an instance of 'File' as the bundle will inject one here
* during Doctrine hydration.
*
* #param File|\Symfony\Component\HttpFoundation\File\UploadedFile $image
*
* #return Torneos
*/
public function setImageFile(File $logo = null)
{
$this->imageFile = $logo;
// VERY IMPORTANT:
// It is required that at least one field changes if you are using Doctrine,
// otherwise the event listeners won't be called and the file is lost
//if ($image) {
// if 'updatedAt' is not defined in your entity, use another property
// $this->updatedAt = new \DateTime('now');
//}
return $this;
}
/**
* #return File|null
*/
public function getImageFile()
{
return $this->imageFile;
}
Also add this code (I'm not sure if it's correct)
//BackendBundle/Resources/config/vich_uploader/Torneos.orm.yml
BackendBundle\Entity\Torneos:
imageFile:
mapping: torneo_images
filename_property: logo
Can anyone give me some idea to fix it?
The solution was quite simple.
The error occurs because the use are placed before thenamespace in the controller.
namespace BackendBundle\Entity;
Regards
I've finally succeeded to create a working self-referencing relationship in Doctrine/Symfony2. But, when I request a findAll the table rows aren't returned in the order I want. (Maybe it's not THAT easy, but I can't find any solution anymore.)
The table "categories"
id parentId name
1 NULL music
2 NULL films
3 1 bands
4 1 guitars
5 NULL books
6 2 actors
The file "category.orm.yml"
FormBundle\Entity\Category:
type: entity
oneToMany:
children:
fetch: "EAGER"
targetEntity: FormBundle\Entity\Category
mappedBy: parent
cascade: ["all"]
manyToOne:
parent:
targetEntity: FormBundle\Entity\Category
inversedBy: children
joinColumn:
name: parentId
referencedColumnName: id
table: categories
repositoryClass: FormBundle\Entity\CategoryRepository
id:
id:
column: id
type: integer
id: true
generator:
strategy: AUTO
fields:
name:
type: string
length: '100'
lifecycleCallbacks: { }
I tried an orderBy (in any way I could find, on any field) but I haven't succeeded or had ANY progress with it.
The entity file "Category.php"
<?php
namespace FormBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
/**
* Category
*/
class Category
{
/**
* #var integer
*/
private $id;
/**
* #var category
*/
private $parent;
/**
* #var arrayCollection
*/
private $children;
/**
* #var string
*/
private $name;
public function __construct()
{
$this->children = new ArrayCollection();
}
public function getChildren() {
return $this->children;
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set parentId
*
* #param integer $parentId
* #return Category
*/
public function setParent(Category $parent)
{
$this->parent = $parent;
return $this;
}
/**
* Get parentId
*
* #return integer
*/
public function getParent()
{
return $this->parent;
}
/**
* Set name
*
* #param string $name
* #return Category
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name
*
* #return string
*/
public function getName()
{
return $this->name;
}
}
What I want as output
<b>music</b>
bands
guitars
<b>films</b>
actors
<b>books</b>
This will be a list, but let's not worry about that right now! It's about the order!
My question
What do I put in my controller to use the relations and fetch the rows in the order I want? Or even better; what do I put in the controller and the repository?
I don't want to use DQL since that is not how Doctrine is meant to be used. I want to learn Doctrine and this is seems to be a very good thing to learn. Yes, I have read the docs for a few days, but nothing seems to work for me. Maybe I overlooked something.
Your tree depth will likely be > 1, Doctrine extensions with his Tree-NestedSet will be a good pick. If it's not the case, your question becomes trivial :
In your controller :
$parentCategories = $categoryRepository->findBy(array('parent' => null));
return $this->render('path_to_your_view.html.twig', array(
'parentCategories' => $parentCategories
));
In your view :
{% for parentCategory in parentCategories %}
<b>{{ parentCategory.getName() | e }}</b>
{% for childCategory in parentCategory.getChildren() %}
<p>{{ childCategory.getName() | e }}</p>
{% endfor %}
{% endfor %}