Order (sub-)categories in Doctrine with Symfony2/YML - php

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 %}

Related

How to join multiple entities on a foreign ID in Symfony 4 using a query builder?

I'm trying to learn Symfony. Today I was following The associations tutorial. I decided to make a small application that a House, Kitchens, Bedrooms, and cabinets. I (tried to ;-) ) make a small Class diagram using draw.io to give you a better idea.
So basically a House can have multiple Bedrooms and multiple Kitchens. Each kitchen can have multiple cabinets. The House has an id and a name. The Bedroom and Kitchen as well. The cabinet has id, shopUrl and is also linked via a foreign key (account_id) to its parent Kitchen.
I also link the Kitchen and the Bedroom to the House using a foreign key (house_id). So I followed the tutorial and created the House entity:
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
/**
* #ORM\Entity(repositoryClass="App\Repository\HouseRepository")
*/
class House implements \JsonSerializable
{
/**
* #ORM\OneToMany(targetEntity="App\Entity\Kitchen", mappedBy="house")
*/
private $kitchen;
/**
* #ORM\OneToMany(targetEntity="App\Entity\Bedroom", mappedBy="house")
*/
private $bedroom;
/**
* House constructor
*/
public function __construct()
{
$this->kitchen = new ArrayCollection();
$this->bedroom = new ArrayCollection();
}
/**
* #return Collection|Kitchen[]
*/
public function getKitchen(): Collection
{
return $this->kitchen;
}
/**
* #return Collection|Bedroom[]
*/
public function getBedroom(): Collection
{
return $this->bedroom;
}
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
*/
private $name;
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
public function jsonSerialize()
{
return get_object_vars($this);
}
}
The House repository is empty (a.k.a: only containts the automatically generated code from Symfony):
<?php
namespace App\Repository;
use App\Entity\House;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Symfony\Bridge\Doctrine\RegistryInterface;
/**
* #method House|null find($id, $lockMode = null, $lockVersion = null)
* #method House|null findOneBy(array $criteria, array $orderBy = null)
* #method House[] findAll()
* #method House[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class HouseRepository extends ServiceEntityRepository
{
public function __construct(RegistryInterface $registry)
{
parent::__construct($registry, House::class);
}
}
The Bedroom entity is this:
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="App\Repository\BedroomRepository")
*/
class Bedroom implements \JsonSerializable
{
/**
* #ORM\ManyToOne(targetEntity="App\Entity\House", inversedBy="bedroom")
*/
private $house;
public function getHouse(): House
{
return $this->house;
}
public function setHouse(House $house): self
{
$this->house = $house;
return $this;
}
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
*/
private $name;
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
public function jsonSerialize()
{
return get_object_vars($this);
}
}
and the Bedroom repository is also empty.
The Kitchen entity has the following code:
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="App\Repository\KitchenRepository")
*/
class Kitchen implements \JsonSerializable
{
/**
* #ORM\OneToMany(targetEntity="App\Entity\Cabinet", mappedBy="kitchen")
*/
private $cabinet;
/**
* Kitchen constructor
*/
public function __construct()
{
$this->cabinet= new ArrayCollection();
}
/**
* #return Collection|Cabinet[]
*/
public function getCabinet(): Collection
{
return $this->cabinet;
}
/**
* #ORM\ManyToOne(targetEntity="App\Entity\House", inversedBy="kitchen")
*/
private $house;
public function getHouse(): House
{
return $this->house;
}
public function setHouse(House $house): self
{
$this->house = $house;
return $this;
}
/**
* #ORM\Id()
* #ORM\GeneratedValue(strategy="UUID")
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
*/
private $name;
public function getId(): int
{
return $this->id;
}
public function getName(): string
{
return $this->name;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
public function jsonSerialize()
{
return get_object_vars($this);
}
}
and the Kitchen repository is also empty.
Finally, the cabinet consists of the following:
<?php
namespace App\Entity;
use DateTime;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="App\Repository\CabinetRepository")
*/
class Cabinet
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
*/
private $shopUrl;
private $account_id;
/**
* #ORM\ManyToOne(targetEntity="Kitchen", inversedBy="cabinet")
*/
private $kitchen;
/**
* Cabinet constructor.
* #param string $shopUrl
* #param Kitchen $kitchen
* #param int $id
*/
public function __construct(string $shopUrl, Kitchen $kitchen = null, int $id = null)
{
$this->shopUrl = $shopUrl;
$this->kitchen = $kitchen;
$this->id = $id;
}
public function setId(int $id): self
{
$this->id = $id;
return $this;
}
public function getId(): int
{
return $this->id;
}
public function getShopUrl(): string
{
return $this->shopUrl;
}
public function getKitchen(): Kitchen
{
return $this->kitchen;
}
public function setKitchen(Kitchen $kitchen): self
{
$this->kitchen = $kitchen;
$this->account_id = $kitchen->getId();
return $this;
}
public function setAccount_id(int $account_id): self
{
$this->account_id = $account_id;
return $this;
}
public function getAccount_id(): int
{
return $this->account_id;
}
}
In contrast to the other entities, the cabinet has some logic (this is where I actually need help). Since Bedroom and Kitchen are associated with a House, I would like to give a Bedroom, then look up all the kitchens associated with the same house as the Bedroom and then return all cabinets that these kitchens have. I know it may seem illogical but I discovered this too late to come up with another concept. My current code doesn't work because I'm not sure whether this is possible and because it's a bit too complex for me to grasp at this moment. But I have this as the content of the cabinet repo:
<?php
namespace App\Repository;
use App\Entity\Bedroom;
use App\Entity\House;
use App\Entity\Cabinet;
use App\Entity\Kitchen;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Symfony\Bridge\Doctrine\RegistryInterface;
/**
* #method Cabinet|null find($id, $lockMode = null, $lockVersion = null)
* #method Cabinet|null findOneBy(array $criteria, array $orderBy = null)
* #method Cabinet[] findAll()
* #method Cabinet[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class CabinetRepository extends ServiceEntityRepository
{
public function __construct(RegistryInterface $registry)
{
parent::__construct($registry, Cabinet::class);
}
public function findByBedroom(Bedroom $bedroom) //use joins??
{
return $this->createQueryBuilder('cabinet')
->join('cabinet.bedroom', 'bedroom')
->join('cabinet.kitchen', 'kitchen')
->addSelect('cabinet')
->andWhere('cabinet.bedroom = :idBedroom')
->setParameter('idBedroom', $bedroom->getId())
->orderBy('time', 'ASC')
->getQuery()
->getResult();
}
}
I'm using PHPStorm and no error are showing anywhere but of course, the querybuilder doesn't return anything. How can I fix it? I couldn't find any questions that try to achieve what I'm trying to do.
I added data manually to the database, so there's data in there. And using Sequel Pro I can see the relations. The data (as far as I'm concerned) is fine. The queryBuilder is where the mistakes are.
A demonstrative example with some data:
This is the Bedroom data:
id name house_id
325 bigBedroomOne 1666
815 smallBedroomOne 555
902 bigBedroomTwo 1666
This is the House data:
id name
1666 bigHouse
555 smallHouse
This is the Kitchen data:
id name house_id
1 bigKitchen 1666
2 smallKitchen 555
55 mediumKitchen 555
And finally, this is the cabinets data:
id shopUrl account_id
1 ur.l 55
88 co.m 2
33 ne.t 1
So in this example I would like to plug in the Bedroom id 815 which is associated with the house_id 555. Then from there all the Kitchen associated with that house_id, should be selected, so 2 and 55. Finally, the cabinets with id 1 and 88 should be returned.
Edit: When running bin/console doc:sch:val I get this back:
`Mapping
[OK] The mapping files are correct.
Database
[OK] The database schema is in sync with the mapping files.`
In your debug bar in symfony you should probably be seeing some errors in doctrine. These won't show up in PHPStorm. You need to rename $kitchen and $bedroom to their plural forms $kitchens and $bedrooms (and change your getters/setters to match) since this is how you define things in the owning side of your doctrine relationships.
A simpler approach than your repository method would be to do what you want in your controller to let doctrine do your heavy lifting:
$cabinets = [];
$house = $bedroom->getHouse();
$kitchens = $house->getKitchens();
foreach ($kitchens as $kitchen) {
$kitchenCabinets = $kitchen->getCabinets();
$cabinets = array_merge($cabinets, $kitchenCabinets);
}
There are several minor problems in your code, more on that later.
Here is \App\Repository\CabinetRepository::findByBedroom:
public function findByBedroom(Bedroom $bedroom) //use joins??
{
return $this->createQueryBuilder('cabinet')
->join('cabinet.kitchen', 'kitchen')
->join('kitchen.house', 'house')
->join('house.bedroom', 'bedroom')
->addSelect('cabinet')
->andWhere('bedroom = :bedroom')
->setParameter('bedroom', $bedroom)
->getQuery()
->getResult();
}
For bedroom entity with ID 815 the code above returns the following (formatted as symfony/var-dumper would do that):
array:2 [▼
0 => Cabinet {#387 ▼
-id: 88
-shopUrl: "co.m"
-account_id: null
-kitchen: Kitchen {#354 ▼
+__isInitialized__: false
-cabinet: null
-house: null
-id: 2
-name: null
…2
}
}
1 => Cabinet {#364 ▼
-id: 1
-shopUrl: "ur.l "
-account_id: null
-kitchen: Kitchen {#370 ▼
+__isInitialized__: false
-cabinet: null
-house: null
-id: 55
-name: null
…2
}
}
]
Note: house references are null because of lazy loading.
So, small problems in your code:
Your query in CabinerRepository was doing wrong joins. For correct joins see code above.
That query referring to unknown field time. I have removed that reference.
And also was using bedroom ID instead of bedroom entity.
Your Kitchen.php is incomplete, it refers Collection and ArrayCollection classes, but there are no corresponding use directives. Just add this after namespace before class:
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
Update: here is how to get repository reference:
public function myControllerAction(/* other parameters */, CabinetRepository $cabRepo)
{
$cabinet = $cabRepo->find($id);
// OR, if you don't want to add parameter for some reason:
$cabRepo = $this->getDoctrine()->getRepository(Cabinet::class);
$cabinet = $cabRepo->find($id);
}
First I think because you joining with both entities at the same time in this
...
->join('cabinet.bedroom', 'bedroom')
->join('cabinet.kitchen', 'kitchen')
...
and because that will be with INNER JOIN, it will require that cabined is required both bedroom and kitchen cabinet.
For that there is few solutions to work through:
Proper one would be redesign you entities. I think it might not be hard to use Doctrine inheritance
you might change joins to left, so relation is not mandatory (will work, but in general its not good solution because of wrong design)

Sylius: How to extend Taxon model?

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.

Symfony Parent Child category relation

I have a Product Category table, where I have stored categories if parent_id is null then assume this is parent category if not null then it has child categories. Now the issue is I can't show parent category name in listing page, so can someone have an idea how to make a relation in Product Category entity?
Symfony version: 3.3
Table Structure:(product_category)
id
parent_id
slug
title
I have tried this code in ProductCategory entity but it doesn't show any relational data:
class ProductCategory
{
/**
* #ORM\OneToMany(targetEntity="ProductCategory", mappedBy="parent")
* #ORM\JoinColumn(name="parent_id", referencedColumnName="id", nullable=true)
*/
private $parent;
I usually use children and parent and this is how I use It:
/**
* One ProductCategory has Many ProductCategories.
* #ORM\OneToMany(targetEntity="ProductCategory", mappedBy="parent")
*/
private $children;
/**
* Many ProductCategories have One ProductCategory.
* #ORM\ManyToOne(targetEntity="ProductCategory", inversedBy="children")
* #ORM\JoinColumn(name="parent_id", referencedColumnName="id", nullable=true)
*/
private $parent;
EDIT
/**
* ProductCategory constructor.
*/
public function __construct()
{
$this->children = new ArrayCollection();
}
Getters / Setters and this stuff:
/**
* #param ProductCategory $children
* #return $this
*/
public function addChildren (ProductCategory $children)
{
$this->children[] = $children;
return $this;
}
Using entity with self reference, as Alessandro described, then let's say you have all your records extracted with doctrine using $qb->(bla bla bla)->getResult(); into a variable called $categories. Then you just have to iterate through it like that:
foreach($categories as $category) {
echo $category->getTitle() . ' has parent ' . ($category->getParent() ? $category->getParent()->getTitle() : 'none');
}
To understand more about array collection, read: http://www.doctrine-project.org/api/common/2.3/class-Doctrine.Common.Collections.ArrayCollection.html

Symfony / PHP - Entity inside other entity in Form Type class

I have the following problem. Entity Payment represents types of payment. So I can have for example: 'Credit Card' with 2€ fee, 'PayPal' with 1.50€ fee and so on.
Then I have entity OrderPaymentItem which stores payment type used in e-shop order. Why? I need stored payment for each e-shop order because when I change fee of payment, it use fee which was used by customer when he/she was completion order ... If I connect Payment directly to Order entity instead of to OrderPaymentItem, it recalculate old orders and it is mistake. When you change fee, it should change only in new orders ... But I don't know how to correct it. First look at these two classes:
The first entity is Payment:
class Payment
{
protected $id;
protected $method;
protected $fee;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set fee
*
* #param integer $fee
* #return Payment
*/
public function setFee($fee)
{
$this->fee = $fee;
return $this;
}
/**
* Get fee
*
* #return integer
*/
public function getFee()
{
return $this->fee;
}
/**
* Set method
*
* #param string $method
* #return Payment
*/
public function setMethod($method)
{
$this->method = $method;
return $this;
}
/**
* Get method
*
* #return string
*/
public function getMethod()
{
return $this->method;
}
}
AppBundle\Entity\Payment:
type: entity
table: payment
id:
id:
type: integer
generator:
strategy: AUTO
fields:
method:
column: method
type: string
nullable: false
unique: true
length: 64
fee:
column: fee
type: integer
nullable: false
The second entity is OrderPaymentItem:
class OrderPaymentItem
{
protected $id;
protected $method;
protected $fee;
protected $paymentId;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set fee
*
* #param integer $fee
* #return Payment
*/
public function setFee($fee)
{
$this->fee = $fee;
return $this;
}
/**
* Get fee
*
* #return integer
*/
public function getFee()
{
return $this->fee;
}
/**
* Set method
*
* #param string $method
* #return Payment
*/
public function setMethod($method)
{
$this->method = $method;
return $this;
}
/**
* Get method
*
* #return string
*/
public function getMethod()
{
return $this->method;
}
/**
* Set paymentId
*
* #param \AppBundle\Entity\Payment $paymentId
* #return OrderDiscountItem
*/
public function setPaymentId(\AppBundle\Entity\Payment $paymentId = null)
{
$this->paymentId = $paymentId;
return $this;
}
/**
* Get paymentId
*
* #return \AppBundle\Entity\Payment
*/
public function getPaymentId()
{
return $this->paymentId;
}
}
AppBundle\Entity\OrderPaymentItem:
type: entity
table: order_payment_item
id:
id:
type: integer
generator:
strategy: AUTO
fields:
method:
column: method
type: string
nullable: false
length: 64
fee:
column: fee
type: integer
nullable: false
manyToOne:
paymentId:
targetEntity: AppBundle\Entity\Payment
joinColumn:
name: payment_id
referencedColumnName: id
onDelete: SET NULL
nullable: true
Originally I have following form builder in basket form:
$builder->add('payment', 'entity', [
'label' => 'Platba',
'class' => 'AppBundle:Payment',
'data_class' => 'AppBundle\Entity\Payment',
'property' => 'method',
'multiple' => false,
'expanded' => true
]);
You can see that these two classes are almost same. Only OrderPaymentItem contains relation to Payment - not required but good for back compatibility.
I need to correct it now and use OrderPaymentItem instead of Payment. I need to use list of Payments, but save it as OrderPaymentItem.
Can anybody help me?
Since the two classes are almost the same, you can use Doctrine discriminator.
Take a look at this
When using a discriminator map, for example, if using Single Table Inheritance, your ‘mapped entity’ will be extending a base entity.
This should help you
P.S : I hope it's not too late...

Doctrine Extensions Tranlatable with Slugable

When i translate an entity, everything is fine but slug.
My entity:
/**
* #Gedmo\Translatable
* #var string
*/
private $slug;
My Orm.yml
slug:
type: string
length: 1000
nullable: false
gedmo:
translatable: {}
slug:
separator: -
fields:
- title
My ext_translations table:
Title and content successfully translated to given language. Slug is generating over title in posts table. I could not translate slug.
Any idea?
I suggest you to change from Doctrine Extensions to DoctrineBehaviors, because the development of the doctrine extensions has stopped. Also what you want is easily possible with doctrine behaviour:
Your Entity class:
class Entity {
use ORMBehaviors\Translatable\Translatable;
}
Your EntityTranslation class:
class EntityTranslation {
use ORMBehaviors\Translatable\Translation;
use ORMBehaviors\Sluggable\Sluggable;
/**
* #var string
* #ORM\Column(type="text", nullable=true)
*/
protected $title;
public function getSluggableFields()
{
return [ 'title' ];
}
}

Categories