How to customize an entity property in Sylius? - symfony

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

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?

Doctrine Entity Repository how to add 'andWhere' to all 'find*' functions?

For legacy reasons I have table that is used for many purposes. Only subset of rows is relevant to Entity I'm writing. Criteria is simple, just check 'project' field for specific value.
Instead of reimplementing find, findAll, findByName, findByID, findBy.... Just notify doctrine to append single condition to them all. Is that possible without reimplementing each and every find* ?
Or maybe it can be done on lover level still?
UPDATE:
Reworked question, to specify what kind of solution would be acceptable.
An available easy-to-use solution is to create a Repository with your custom find function.
Then, if all your entities has a specific Repository, make them (Repository) extending from yours (which contains the custom find method), otherwise (you doesn't have a Repository per entity), assign the repository to all your entities with the repositoryClass option of the #ORM\Entity annotation like :
#ORM\Entity(repositoryClass="YourMainRepository")
Otherwise, if you doesn't want put any repository in your entities, override the whole default repository and customise the find method.
I already used the last option because of a specific need, also I invite you to see the following question :
Abstract repository and #Entity annotation inheritance
Look at the solution wich contains a gist of all required steps for override the default doctrine repository.
See Custom Repository classes
Entity:
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Phrase
*
* #ORM\Table(name="User")
* #ORM\Entity(repositoryClass="AppBundle\Repository\UserRepository")
*/
class User
{
/**
* #var int
*
* #ORM\Column(name="id", type="bigint")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
.............................................................
.............................................................
.............................................................
Your Repository:
namespace AppBundle\Repository;
use Doctrine\ORM\EntityRepository;
class UserRepository extends EntityRepository
{
/** For example **/
public function getByName($name)
{
$qb = $this->createQueryBuilder('u')
->where('u.name = :name')->setParameter('name', $name)
->andWhere('u.lastname LIKE :name')->setParameter('lastname', '%'.$name.'%');
$query = $qb->getQuery();
return $query->getResult();
}
}
In Your Controller:
/**
* #Route("/", name="index")
*/
public function indexAction(Request $request)
{
$userRepository = $this->getDoctrine()->getRepository('AppBundle:User');
$userName = $userRepository->getByName($name);
..................................................................................
..................................................................................

Doctrine arrayCollections and relationship

I'm quite new with Doctrine, so I hope someone can help me or redirect me to the good documentation page.
I'm building an app with two entity (I reduce for explanations) :
- Tender
- File
For each tender, we can have one or more files. So I made the following objects.
Tender:
<?php
namespace TenderBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="tender")
*/
class Tender
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $tender_id;
/**
* #ORM\Column(type="array")
* #ORM\ManyToOne(targetEntity="File", inversedBy="tenders")
* #ORM\JoinColumn(name="tender_files", referencedColumnName="file_id")
*/
private $tender_files;
}
File:
<?php
namespace TenderBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\File\UploadedFile;
/**
* #ORM\Entity
* #ORM\Table(name="file")
*/
class File
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $file_id;
/**
* #ORM\OneToMany(targetEntity="Tender", mappedBy="tender_files", cascade={"persist", "remove"})
*/
private $file_tender;
}
First question: is it the right way to do this?
(of course, i've created the methods to get and set attributes, but they're basic).
When I persist each of my File object i'm trying to add then to my Tender instance. But to do this, I need to make $tender_files public and do this:
$tender->tender_files[]
This is not a viable solution for me because I need all my fields are private and I want to recover my object when I try to call this:
$this->getDoctrine()->getManager()->getRepository('TenderBundle:Tender')->find($id)->getTenderFiles()->getFileName();
So, I'm explaining and asking to find the right way to do what I want. I hope what i need is clear and i'm here to answers questions or show more code if needed.
Thanks!
Like Richard has mentioned, you're missing getters and setters which are declared to be public. They'll have access to your private variables. The quick way to do this with symfony:
php app/console doctrine:generate:entities
It'll generate something like this:
public function addTenderFile(\TenderBundle\Entity\File $file)
{
$this->tender_files[] = $file;
return $this;
}
/**
* Remove
*/
public function removeTenderFile(\TenderBundle\Entity\File $file)
{
$this->tender_files->removeElement($file);
}
/**
* Get
*/
public function getTenderFiles()
{
return $this->tender_files;
}
It's good practice if you're a beginner to see how your code lines up with the auto generator. Once you understand what's going on, just let the generator do the grunt work.
You should have a setter and getter in your File entity similar to this:
public function setTender(\Your\Namespace\Tender $tender)
{
$this->tender = $tender;
return $this;
}
public function setTender()
{
return $this->tender;
}
So when you instance (or create) File, you can go like so:
$file = new File(); // or file fetched from DB, etc.
// here set $file properties or whatever
$tender->setFile($file);
$entityManager->persist($tender);
$entityManager->flush();
Then your tender will be properly associated with your file.
Similarly from the File end, you should be able to do:
$file->addTender($tender);
$entityManager->persist($file);
$entityManager->flush();
And your tender will be added to your File->tenders collection.
For more information the documentation is very useful and has more or less everything you need to get started.
Also, save yourself manually creating getters and setters by using generate:doctrine:entity
This is incorrect:
/**
* #ORM\Column(type="array")
* #ORM\ManyToOne(targetEntity="File", inversedBy="tenders")
* #ORM\JoinColumn(name="tender_files", referencedColumnName="file_id")
*/
private $tender_files;
You can't persist an array to your database. A database row is one entity and it's corresponding attributes. If a tender can have many files, then this relationship should be:
* #ORM\OneToMany
Likewise for the File entity. If many files can have one Tender then it's relationship should be:
* #ORM\ManyToOne
For relationship mapping using Doctrine, it's helpful to read left-to-right with the entity YOU'RE CURRENTLY IN being on the left, and the entity you're setting as a variable being on the right.
If you're in Tender reading left-to-right Tender may have "OneToMany" files. And File(s) may have ManyToOne Tender. Doctrine Association Mapping

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

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.

Symfony2 Doctrine, setup a page with many to many self referencing relationship

I have tried following the instructions on Doctrines manuals:
http://docs.doctrine-project.org/en/latest/reference/association-mapping.html#many-to-many-self-referencing
I'm trying to setup a pages entity, which has many subPages and I would also like to be able to access a pages parentPage.
Following the instructions above I am told by symfony2 that I need to setup a "use" due to a semantic error.
Could anybody instruct me on what to do to allow me to do this as I am quite stuck.
Sample code is:
namespace Pages\Bundle\PageBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* Page
*
* #ORM\Table(name="Page")
* #ORM\Entity(repositoryClass="Pages\Bundle\PageBundle\Entity\PageRepository")
*/
class Page
{
/**
* Constructor
*/
public function __construct()
{
$this->subPages = new \Doctrine\Common\Collections\ArrayCollection();
$this->parentPages = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* #ManyToMany(targetEntity="Page", mappedBy="subPages")
**/
private $parentPages;
/**
* #ManyToMany(targetEntity="Page", inversedBy="parentPages")
* #JoinTable(name="sub_pages",
* joinColumns={#JoinColumn(name="parent_page_id", referencedColumnName="id")},
* inverseJoinColumns={#JoinColumn(name="sub_page_id", referencedColumnName="id")}
* )
**/
private $subPages;
...
(other variables follow but are content / metas)
Error response when running is:
[Semantical Error] The annotation "#ManyToMany" in property
Pages\Bundle\PageBundle\Entity\Page::$parentPages was never imported. Did you maybe forget to add a "use" statement for this annotation?
Try using #ORM\ManyToMany instead of just #ManyToMany
(and also #ORM\JoinTable)

Resources