Unable to access parent entity attributes by ID - symfony

I am trying to work out why I can't access the name (or any attribute) of the parent entity of my Category entity. My view works fine and displays all the attributes other than the parent information.
What I am looking for is the name (i.e. "Interior") of the parent, rather than the ID. But I can't seem to access it.
namespace AyrshireMinis\CarBundle\Entity;
class Category
{
/**
* #var integer
*/
protected $id;
/**
* #var integer
*/
protected $parent_id;
/**
* #var Category
*/
protected $parent;
public function __construct() {}
/**
* #return int
*/
public function getParentId()
{
return $this->parent_id;
}
public function getParent()
{
return new self($this->parent_id);
}
}
My Doctrine entity mapping:
fields:
parent_id:
type: integer
length: 11
nullable: true
In my Twig template I am attempting to pull out the parent category's name like this:
<td>
{{ category.parent.name }}
</td>
However, I get no error but no category name either.
This is a brief view of my SQL GUI:
The table is hierarchical.

public function getParent()
{
return new self($this->parent_id);
}
What you are doing here is creating a new category with all properties blank. Take a look at the symfony and doctrine documentation to learn how to create relationships properly.

I ended up resolving this by removing the parent_id from my ORM mapping and replacing it with this:
manyToOne:
parent:
targetEntity: AyrshireMinis\CarBundle\Entity\Category
inversedBy: children
Then added these member variables to my Category entity class:
/**
* #var Category
*/
private $children;
/**
* #var Category
*/
private $parent;

Related

Symfony 4: remove collection from entity

I have a product entity and product image entity. I want to use soft delete on product entity only and make a delete on product image entity.
The soft delete works fine. When I delete the product, the deleted_at column is set to current time.
So I would like to delete product image when the deleted_at column is updated.
I was wondering if I can do it directly in entity class? and how?
Product entity where I try to make the collection delation in setDeletedAt function.
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="App\Repository\ProductRepository")
* #ORM\Table(name="product")
*/
class Product
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\OneToMany(targetEntity="App\Entity\ProductImage", mappedBy="product", orphanRemoval=true, cascade={"persist"})
*/
private $productImages;
/**
* #ORM\Column(type="datetime", nullable=true)
*/
private $deleted_at;
public function __construct()
{
$this->productImages = new ArrayCollection();
}
public function setDeletedAt(?\DateTimeInterface $deleted_at): self
{
// Here I try to remove images when deleted_at column is updated
$productImage = $this->getProductImages();
$this->removeProductImage($productImage);
$this->deleted_at = $deleted_at;
return $this;
}
/**
* #return Collection|ProductImage[]
*/
public function getProductImages(): Collection
{
return $this->productImages;
}
public function addProductImage(ProductImage $productImage): self
{
if (!$this->productImages->contains($productImage)) {
$this->productImages[] = $productImage;
$productImage->setProduct($this);
}
return $this;
}
public function removeProductImage(ProductImage $productImage): self
{
if ($this->productImages->contains($productImage)) {
$this->productImages->removeElement($productImage);
// set the owning side to null (unless already changed)
if ($productImage->getProduct() === $this) {
$productImage->setProduct(null);
}
}
return $this;
}
}
But when I make the soft delete, setDeletedAt() is called and the following error is returned:
Argument 1 passed to App\Entity\Product::removeProductImage() must be an instance of App\Entity\ProductImage, instance of Doctrine\ORM\PersistentCollection given, called in ...
Thanks for your help!
---- UPDATE ----
Solution provided by John works fine:
foreach ($this->getProductImages() as $pi) {
$this->removeProductImage($pi);
}
Thanks!
pretty self-explaining error:
at this point:
$productImage = $this->getProductImages();
$this->removeProductImage($productImage);
you are passing a collection instead a single ProductImage object.
to delete them all, just do:
foreach ($this->getProductImages() as $pi) {
$this->removeProductImage($pi);
}

API Platform : search filter with group's fields

I'm working with API Platform and I'm looking to make something.
I made a Person Entity like this :
class Person
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
*/
private $name;
/**
* #ORM\Column(type="string", length=255)
*/
private $firstname;
/**
* #ORM\Column(type="datetime")
*/
private $birthdate;
}
With defined groups for the fields :
App\Entity\Person:
attributes:
id:
groups: ['private']
name:
groups: ['public']
firstname:
groups: ['public']
birthdate:
groups: ['public']
I also precised that if I want all the collection of that resource, only public fields should be serialized :
App\Entity\Person:
collectionOperations:
get:
filters: ['search_filter']
normalization_context:
groups: ['public']
formats: ['json']
As you can see, I applied a search filter. In that case I can retrieve resources from their fields precised as query parameters.
However, I want to apply this filter only with public fields.
So I don't want that the http://localhost/api/people?id=1 request works, since the id field is private.
I see that it is possible to precise the fields wanted as arguments for SearchFilter, but it would be more useful to precise the group name instead, because I intend to work with more groups.
I tried to look in GroupFilters, but it doesn't help me, because it is a serializer filter...
What do you recommend me ?
After a few hours of digging, I finally found my answer :
I created my own filter and it has a SearchFilter instance injected.
In order to compare fields'groups sent into the QueryParam, I had to extend my filter with the AbstractContextAwareFilter class.
I compare these groups with the resource / Entity metadata information provided by the ClassMetadataFactory class. I had to use the annotation syntax instead in order to write my groups instead of yaml otherwise they won't be detected.
If a group is not in the normalization ones, I throw an exception, else I leave the SearchFilter to do the filter process.
Here is my work :
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\AbstractContextAwareFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\AbstractFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use Doctrine\Common\Persistence\ManagerRegistry;
use Doctrine\ORM\QueryBuilder;
use http\Exception\RuntimeException;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Serializer\Mapping\ClassMetadata;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
/**
* Class RestrictGroupFilter
* #package App\Filters
*/
class RestrictGroupFilter extends AbstractContextAwareFilter
{
/**
* #var $decorated AbstractFilter
*/
private $filter;
private $metadataFactory;
public function __construct(AbstractFilter $filter, ClassMetadataFactory $metadataFactory,ManagerRegistry $managerRegistry, ?RequestStack $requestStack = null, LoggerInterface $logger = null, array $properties = null)
{
parent::__construct($managerRegistry, $requestStack, $logger, $properties);
$this->filter = $filter;
$this->metadataFactory = $metadataFactory;
}
protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null, array $context = [])
{
/**
* #var $classMetadata ClassMetadata
*/
$classMetadata = $this->metadataFactory->getMetadataFor($resourceClass); #retrieve of Entity's class's attribute metadata
#prepare to check context's group with normalization ones
foreach ($context["groups"] as $group)
{
if(!in_array($group,$classMetadata->attributesMetadata[$property]->getGroups())){ //if one group is not found in normalization groups
throw new RuntimeException("$property's group denied." /*Groups authorized : ".implode(", ",$context["groups"])*/);
}
}
//Filter is enabled if all is good
$this->filter->filterProperty($property,$value,$queryBuilder,$queryNameGenerator,$resourceClass,$operationName);
}
public function getDescription(string $resourceClass): array
{
// TODO: Implement getDescription() method.
return $this->filter->getDescription($resourceClass);
}
}
For the services :
search_filter:
parent: 'api_platform.doctrine.orm.search_filter'
tags: ['api_platform.filter']
autowire: false
autoconfigure: false
'App\Filters\RestrictGroupFilter':
arguments: [ '#search_filter','#serializer.mapping.class_metadata_factory']

Doctrine not honoring self-referencing relationship

I have a User entity with a self-referencing one-to-many relationship - Every User owns a set of students (who are also users):
<?php
namespace AppBundle\Entity;
use FOS\UserBundle\Model\User as BaseUser;
use Doctrine\Common\Collections\ArrayCollection;
/**
* User
*/
class User extends BaseUser
{
/**
* #var int
*/
protected $id;
private $students;
// ....
/**
* Get id
*
* #return int
*/
public function getId()
{
return $this->id;
}
public function __construct() {
$this->students = new ArrayCollection();
// ...
parent::__construct();
}
/**
* Remove student
*
* #return User
*/
public function removeStudent($student)
{
$this->students->removeElement($student);
return $this;
}
/**
* Add a student
*
* #param User $students
*
* #return User
*/
public function addStudent($student)
{
$this->students->add($student);
return $this;
}
/**
* Get students
*
* #return User
*/
public function getStudents()
{
$this->students;
}
/**
* Set students
*
* #param User $students
*
* #return User
*/
public function setStudents($students)
{
$this->students = $students;
return $this;
}
// ....
}
The mapping is done as a one-to-many unidirectional with join table
AppBundle\Entity\User:
type: entity
table: null
repositoryClass: AppBundle\Repository\UserRepository
id:
id:
type: integer
id: true
generator:
strategy: AUTO
fields:
// ...
manyToMany:
students:
targetEntity: User
joinTable:
name: mentors_students
joinColumns:
mentor_id:
referencedColumnName: id
inverseJoinColumns:
student_id:
referencedColumnName: id
unique: true
lifecycleCallbacks: { }
Now when I add/edit a user using the EasyAdmin bundle, I can add the students for that user. However, when I retrieve the entity, the students property is always null. For example, when I view the list of users:
Here the user 'sagarl3232' is supposed to be a student of 'sj' but the view above clearly shows the property when retrieved is null.
The entity is persisted correctly in the database. That is, the join table has the right values:
Why is Doctrine doing this to me? Isn't it supposed to hydrate the students array automatically?
Your getter doesnt return anything!
/**
* Get students
*
* #return User[]
*/
public function getStudents()
{
return $this->students;
}
BTW why you should adjust the docblock too. The getter is supposed to return an array User[]

Doctrine OneToMany relation does not fetch related entities

I am stuck at this case, I reproduced it in an example from symfony documentation, here it how it looks:
FIXTURES
/**
* #ORM\Entity
*/
class Category
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue
*/
private $id;
/**
* #ORM\OneToMany(targetEntity="Product", mappedBy="category", fetch="EAGER")
*/
private $products;
public function __construct()
{
$this->products = new ArrayCollection();
}
public function products(): Collection
{
return $this->products;
}
public function id()
{
return $this->id;
}
}
and related Product class
/**
* #ORM\Entity
*/
class Product
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue
*/
protected $id;
/**
* #ORM\ManyToOne(targetEntity="Category", inversedBy="products")
* #ORM\JoinColumn(name="category_id", referencedColumnName="id")
*/
private $category;
public function __construct($category)
{
$this->category = $category;
}
public function id()
{
return $this->id;
}
public function category()
{
return $this->category;
}
}
TEST
Now I have this snippet of test code where I want to fetch Category and be able to get its Products:
$cat = new Category();
$prod = new Product($cat);
$this->entityManager->persist($prod);
$this->entityManager->persist($cat);
$this->entityManager->flush();
$crepo = $this->getEntityManager()->getRepository(Category::class);
$c = $crepo->findAll()[0];
var_dump(get_class($c->products()), $c->products()->count())
What I am getting is products of class PersistentCollection which is expected, but the count is 0 while there should be 1 product.
I can see that in the database I have proper category and product records with proper foreign key set.
WORKAROUND
I am debugging PersistentCollection for products and can see that its flag is set to initialized = true. With this I am able to force this to work by calling
$c->products()->setInitialized(false);
$c->products()->initialize();
But afaik this is not how it should work, should it ?
I managed to found an answer. It basically works but not when run in the same process. If I split the script in two - first one persists, second retrieves the data then the products collection will contain products related to category.
This is because when it is done in single process doctrine does not know that the category in question has products, and since it retrieves the same object it just saved and that was created few lines above, the entity manager won't populate the collection using database but will use the one from the category object. And the category object does not have products in products collection, since there is no call like $this->products()->add($category) neither in Product constructor or anywhere else. Only forcing to reinitialize the collection works since then it really retrieves products from database

Symfony2 FosUserBundle and SonataUserBundle : overriding entities?

I use FosUserBundle and SonataUserBundle for my Symfony2 project.
I get confused now. I want to add fields for the entity User but it's not working. There is no update for the schema for example.
Here is my config :
AppKernel:
...
new FOS\UserBundle\FOSUserBundle(),
new Sonata\UserBundle\SonataUserBundle(),
new Application\Sonata\UserBundle\ApplicationSonataUserBundle('FOSUserBundle')
config.yml:
...
# FOSUserBundle Configuration
fos_user:
db_driver: orm # BDD type
firewall_name: main # firewall name
user_class: Application\Sonata\UserBundle\Entity\User # entity class defined
And the User entity with added fields, in app/Application/Sonata/userBundle/Entity/
namespace Application\Sonata\UserBundle\Entity;
use Sonata\UserBundle\Entity\BaseUser as BaseUser;
/**
* This file has been generated by the Sonata EasyExtends bundle ( http://sonata- project.org/easy-extends )
*
* References :
* working with object : http://www.doctrine-project.org/projects/orm/2.0 /docs/reference/working-with-objects/en
*
* #author <yourname> <youremail>
*/
class User extends BaseUser
{
/**
* #var integer $id
*/
protected $id;
/**
* #var string
*/
protected $institution;
/**
* #var string
*/
protected $department;
/**
* #var string
*/
protected $city;
/**
* #var string
*/
protected $country;
/**
* Get id
*
* #return integer $id
*/
public function getId()
{
return $this->id;
}
/**
* #return string
*/
public function getInstitution()
{
return $this->institution;
}
/**
* #return string
*/
public function getDepartment()
{
return $this->department;
}
/**
* #return string
*/
public function getCity()
{
return $this->city;
}
/**
* #return string
*/
public function getCountry()
{
return $this->country;
}
}
In my database, table fos_user_user seems to be the table with user saved data.
Added fields (country, city...) are not created when calling "php app/console doctrine:schema:update --force". How to add fields to the user entity ? I'm lost in fosuserbundle and sonatauserbundle....
In fact, it appear that the way i use fos and sonata user bundles force me to use XML definition. So for example adding a field called "city" you have to add these lines :
User.php (in /app/Application/Sonata/UserBundle/Entity/) :
protected $city;
User.orm.xml (in /app/Application/Sonata/UserBundle/Ressource/config/doctrine/) :
<field name="city" column="city" type="text"/>
Maybe putting this in the documentation of the bundles would be interesting for newbies ;)
I found a better solution if you want use annotations.
Delete UserBundle/Resorces/config/doctrine folder and you can use annotations in your UserBundle/Entity folder

Resources