How to create a clone of an entity collection without preserving relationship in doctrine? - ajax

I've been trying to figure out how to get this to work for along time but without any luck. Due to a complex logic in an app I'm working on, I need to create an isolated clone of a entity collection without preserving what so ever relation to the database. Whatever changes I do on the cloned collection should not be tracked by Doctrine at all and should be treated as if it doesn't exist at all.
Here's an example code:
/*
* #ORM\Entity()
*/
class Person
{
/**
* #var integer
*
* #ORM\Id
* #ORM\Column(name="person_id", type="integer",nullable=false)
* #ORM\GeneratedValue(strategy="AUTO")
*/
public $id;
/**
* #var ArrayCollection
*
* #ORM\OneToMany(targetEntity="Car", mappedBy="person", cascade={"persist"})
*/
public $cars;
}
/**
* #ORM\Entity()
* #ORM\HasLifecycleCallbacks()
*/
class Car
{
/**
* #var integer
*
* #ORM\Id
* #ORM\Column(name="car_id", type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/*
* #ORM\JoinColumn(name="person_id", referencedColumnName="person_id", nullable=true)
* #ORM\ManyToOne(targetEntity="Person", inversedBy="cars", cascade={"persist"})
*/
private $person;
}
I've already tried the following code in my controller to store the collection into the session but it still somehow stores the relationships:
$tmp = clone $person;
$this->get('session')->set('carCollection', $tmp->getCars());
$tmpCars = clone $person->getCars();
$tmpCollection = new ArrayCollection();
foreach($tmpCars as $car) {
$tmpCollection->add(clone $car);
}
$this->get('session')->set('carCollection', $tmpCollection);
$tmpCars = clone $person->getCars();
$tmpCollection = new ArrayCollection();
foreach($tmpCars as $car) {
$clone = clone $car;
$entityManager->detach($car);
$tmpCollection->add(clone $clone);
}
$this->get('session')->set('carCollection', $tmpCollection);
Apparently I'm doing something wrong here because I end up having more results in the Car collection when flushing the entity even though the collection itself has the correct number of records. I have a suspicion that somewhere in the chain Doctrine doesn't compute correctly what needs to be done.
Any ideas or directions on how to solve or debug this?
Follow-up question: When retrieving back the cloned collection from the session will it still be an isolated clone or Doctrine will try merge it back?

I'm writing this answer to give directions to anybody who might have similar issues. I couldn't find many topics or documentation in this manner which is why I decided to share my experience. I am no deep expert on Doctrine an how it internally works, so I won't go into big details of "how it works". I will rather focus on the end result.
Storing entities which have relations to other entities into a session is quite problematic. When you retrieve it from the session, Doctrine loses track of the relationships (OneToMany, ManyToOne, etc). This leads to some undesired effects:
Doctrine wrongly decides to insert a new record of an existing entity.
Doctrine might throw exceptions such as A new entity was found through the relationship 'Acme\MyBundle\Entity\Person#cars' that was not configured to cascade persist operations for entity: Opel. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity or configure cascade persist this association in the mapping for example #ManyToOne(..,cascade={"persist"}). and at least 2 other types of exceptions which might seem totally irrelevant at first.
Apparently when fetching a result from the database and it "as-is" in your session things get really messy, specially if the entity has relations to other entities (which was my case). Pay big attention if you have entity relationships - they might need to be "refreshed" if you start getting strange exceptions.
There are a couple of ways to overcome this issue. One of which is to use the data sent via the form (as #flec suggested) by using $myForm->getData(). This approach might work well for you, but unfortunately it was not the case with me (too complex to explain).
What I ended up doing was implementing the \Serializable in the entity. I also created a method called __toArray() which converted my entity into an array. What data you return in the __toArray() method is totally up to you and your business logic. The array data is stored into the session and you use it to re-create a fresh object with all necessary relations.
Hope this helps somebody.

I think hydrators/extractors would be the way to go for you.
They can extract the data from an entity and you can pass them to a newly created instance of that entity via the hydrator.
The only thing you'll need to do in between is the unsetting of the relation properties.
They should be fetchable via a metadata class via doctrine somehow.

Related

Automatic filter on attribute depending on authenticated user

I have an entity called event, the event can have many rooms and a room can have many participants.
If I access all events (with a specific user) I can filter events where the user has no access right (no room with a connection to the specific user) by using extensions.
That works fine.
The response contains all events which have at least one room with access rights.
But If the event has multiple rooms and the user has only access to one room. The response includes both rooms. I created a RoomExtension, but this class will not be invoked.
Thanks
Your problem is caused by the fact that filters and extensions only work on the query that retrieves the primary entities. The related entities are retrieved using Doctrines associations wich are part of the domain model that is meant to be the single source of truth for all purposes. What you need is a user-specic view on that model, which in the context of api's usually consists of DTOs.
I think there are basically two solutions:
Query primarily for Events and convert the into EventDTOs, then either query for - or filter out - the related Rooms,
Query primarily for Rooms, then group them into EventDTOs.
I explain the second solution here because i guesss that it is simpeler and it shoud make your RoomExtension work out of the box, which makes it the better fit to your question, but also because i happen to have built and tested something similar in a tutorial so it is a lot less work to write an answer with confidence.
The downside of this solution is that it does not support pagination.
Bucause this solution primarily queries for Rooms, the the operation is on the Room resource. If it where the only collectionOperation of Room it could be like this:
(..)
* #ApiResource(
* collectionOperations={
* "get_accessible_events"={
* "method"="GET",
* "path"="/rooms/accessible-events",
* "output"=EventDTO::class,
* "pagination_enabled"=false
* }
* }
* }
*/
class Room {
(..)
(This does not have to be your only collectionOperation, you can still have "get", "post" and others).
Right now this still produces a flat collection of Rooms, you need to group them into EventDTOs. The DTOs page of the docs suggest to make a DataTransformer to produce the DTOs, but that only works if your DTOs are one to one with the entities retrieved by the query. But a CollectionDataProvider can do the trick. Because you do not need to adapt the query itself you can simply decorate the default CollectionDataProvider service:
namespace App\DataProvider;
use ApiPlatform\Core\Api\OperationType;
use App\DTO\EventDTO;
use ApiPlatform\Core\DataProvider\ContextAwareCollectionDataProviderInterface;
use ApiPlatform\Core\DataProvider\CollectionDataProviderInterface;
use ApiPlatform\Core\DataProvider\RestrictedDataProviderInterface;
use App\Entity\Room;
class RoomAccessibleEventCollectionDataProvider implements ContextAwareCollectionDataProviderInterface, RestrictedDataProviderInterface
{
/** #var CollectionDataProviderInterface */
private $dataProvider;
/**
* #param CollectionDataProviderInterface $dataProvider The built-in orm CollectionDataProvider of API Platform
*/
public function __construct(CollectionDataProviderInterface $dataProvider)
{
$this->dataProvider = $dataProvider;
}
/**
* {#inheritdoc}
*/
public function supports(string $resourceClass, string $operationName = null, array $context = []): bool
{
return Room::class === $resourceClass
&& $operationName == 'get_accessible_events';
}
/**
* {#inheritdoc}
*/
public function getCollection(string $resourceClass, string $operationName = null, array $context = []): array
{
$rooms = $this->dataProvider->getCollection($resourceClass, $operationName, $context);
$dtos = [];
foreach ($rooms as $room) {
$key = $room->getId();
if (isset($dtos[$key])) {
$dtos[$key]->addRoom($room);
} else {
$dto = new EventDTO($room->getEvent());
$dto->addRoom($room);
$dtos[$key] = $dto;
}
}
return $dtos;
}
}
You do need to configure the service in config/services.yaml:
'App\DataProvider\RoomAccessibleEventCollectionDataProvider':
arguments:
$dataProvider: '#api_platform.doctrine.orm.default.collection_data_provider'
This does not replace the default CollectionDataProvider but adds another one that gets the default one injected.
I guess you can make the EventDTO class yourself now. Then it should work. Filters defined on Room will also work as usual, for example if rooms can be filtered by the date of their event ?event.date[gte]=2020-10-10 will only find rooms with events on or after 2020-10-10 and return their EventDTO's.
However, in the swagger docs the get_accessible_events operations summary and descriptions still come from Room. You can look up how to add a SwaggerDecorator in the docs or take a look at the chapter9-api branch of the tutorial. The latter also contains complete explanations and tested code for entities, the DTO (Report Model) and an extension for only showing data the user is authorized for, but is not taylored to your questions and would all together be way beyond what a to the point answer.
I can not give you any more hints on this site with respect the other solution because this site will probably see them as an incomplete or unclear answer and punish me for it.

CakePHP 2x to 3x, HABTM with keepExisting

I have two models that are associated with a Has And Belongs To Many Association in Cake 2.x. I'm trying to port my application over to Cake 3.3. In general, I'm having an issue replicating the "keepExisting" functionality found in the Cake 2.x associations. Right now, I'm trying to use the "through" functionality, but I'm not sure I'm barking up the right tree.
I have the following Tables:
class ContestsTable extends Table
{
/**
* Initialize method
*
* #param array $config The configuration for the Table.
* #return void
*/
public function initialize(array $config)
{
parent::initialize($config);
$this->table('contests');
$this->displayField('name');
$this->primaryKey('id');
$this->addBehavior('Timestamp');
$this->belongsToMany('Events', [
'through' => 'ContestsEvents'
]);
}
}
class EventsTable extends Table
{
/**
* Initialize method
*
* #param array $config The configuration for the Table.
* #return void
*/
public function initialize(array $config)
{
parent::initialize($config);
$this->table('events');
$this->displayField('name');
$this->primaryKey('id');
$this->addBehavior('Timestamp');
$this->belongsToMany('Events', [
'saveStrategy' => 'append',
'through' => 'ContestsEvents'
]);
}
}
class ContestsEventsTable extends Table
{
public function initialize(array $config) {
$this->belongsTo('Contests');
$this->belongsTo('Events');
}
}
When I save the initial contest with associated events, the save is fine and the association records are created in the join table. When I edit the contest record and change the list of the events, any of the selected events are added...but the old ones that are not selected anymore are not deleted. The second issue is that a new join record is created for already existing relationships. I would like to keep the already existing records rather than creating new ones. Later on in the app, I'll be using the join records id in an additional relationship...and I do not want the id to be regenerated each time the overlying contest record is updated.
I hope I'm making sense in describing what I'm trying to do. But in the simplest form, I'm trying to replicate the functionality found via the 'unique' => 'keepExisting' option included in Cake 2x, just can't crack the code on how to accomplish this in Cake 3.3?
Thanks in advance for your help.
OK, So here it goes. First, I think I wrote the question above out of frustration, so my apologies if it wasn't clear. I went on to working on other parts of my app, with this issue stuck in the back of my head. Well, I figured it out...so here's my answer:
I was an idiot.
Today, I took some time with a sandbox app and basically got the functionality to work on a sandbox table, but couldn't get it to work on the app's tables. Eventually I narrowed it down to the fact that it was a database issue. Yes...I somehow made the foreign_key fields in my join table VARCHAR and not INT. Yep...that will do it.
Well, I guess my point is to log this for all eternity...Check your database fields and tables are set up correctly!!!

Validating values in entity - by setter or assert validation?

I have question about OOP strategy of validating values in entity. Lets say I have entity like this:
/*
* #ORM\Table()
* #ORM\Entity(repositoryClass="My\PageBundle\Entity\PageRepository")
*/
class File {
/*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/*
* #ORM\Column(name="type", type="string")
*/
private $type;
/*
* #ORM\ManyToOne(targetEntity="File")
* #ORM\JoinColumn(name="parent_id", referencedColumnName="id")
*/
private $parent;
}
Now I want to only entities type "parent" could be parents. I can do it in 2 ways.
Using Symfony Callback validation:
if ($this->getParent() != null && $this->getParent()->getType() != 'group')
$context->addViolationAt('parent', 'Invalid parent.', array(), null);
which is quite obvious, but works only when I call validator, or
put this logic in setter like it's proposed in Symfony book
You should check generated entities and adjust getter/setter logic to your own needs
for example:
setParent(File $parent) {
if ($parent->getType() != 'group')
throw new \Exception('Invalid parent');
$this->parent = $parent;
}
Which approach is better? Using Validation Constraints created specifically for this purpose or getters and setters?
If using Validation - should I always use default getters and setters or are they any fancy (and useful) things I can do inside them (any examples)?
I would use Symfony Callback validation, first off because it is a dedicated construct for validating entities and because your example is closer to the design philosophy of the component intended for callback validation.
While it is true that PHP OOP books encourage you to not only use getters and setters for retrieving property values, I've always used them to ensure default values and constraints on fields (i.e. I have an $id property, which is of type int, and I want to make sure to cast the value of the $id to int when setting it, even if somebody mistakenly sent in a string/whatever).
Beyond that, I also think it's weird to have a try/catch block for a setter, especially when I know that I'm sending in the proper argument type.
UPDATE: Just noticed the final part of your question. I use my Symfony getters and setters basically just for retrieving/storing values. I tend to set default values for things in constructors. I've seen some code over the years where the getters and setters do some pretty wild things, which really didn't belong within the scope of an Entity, but closer to a Service or Repository.
I think of Entities as the constructs allowing me to map the database table to a PHP class - they don't need to be smart, special, or do anything outside that scope.
I prefer keeping them simple and using the rest of the architecture to compliment the possible operations. Is it something that offers some general functionality and that can be isolated independently? Make a Service for that. Is it something to do with manipulating entities, retrieving certain info or handling them in a specific way? Make a Repository for that.

How to make Doctrine refresh / resync bidirectional entity association?

I have an Entity that has bidirectional ManyToOne/OneToMany relationship with another entity:
class BookShelf {
/**
* #OneToMany(targetEntity="Book", mappedBy="shelf", cascade={"persist"})
*/
public $books;
}
class Book {
/**
* #ManyToOne(targetEntity="BookShelf", inversedBy="books", cascade={"persist"})
*/
public $shelf;
}
I'm trying to create a new book, and have that object be listed in the bookshelf.
$book = new Book();
$book->shelf = $shelf;
$em->persist($book); $em->flush();
$shelf->showBooks();
After that, the $shelf->books does not contain the book, but instead it contains NULL. However the book is correctly inserted into the database, and when I run $shelf->showBooks() on another pageload, the book is listed properly.
I tried adding $em->refresh($book) and $em->refresh($shelf) but it doesn't help, the association still isn't refreshed.
Doctrine manual does suggest that I could use $shelf->books->add($book) to manually synchronize the association, but since there are initially no books, $shelf->books is NULL and I cannot call any methods on it.
How can I make Doctrine reload the association to include the newly created associated entity?
(Related: "Doctrine and unrefreshed relationships")
And as I later noticed, the very same Doctrine manual I linked to tells me to set the property as an ArrayCollection in the constructor, excatly so that the $shelf->books->add($book) does work. That is:
public function __construct() {
$this->books = new \Doctrine\Common\Collections\ArrayCollection();
}
Stupid me. I'll post the answer here in case someone else happens to come looking for the same issue, after being just as stupid. Which is unlikely, I guess.

How to count many-to-many relations in Symfony2 using DQL

I would like to count the number of tags given a specific article. I've got two entities (Article, Tag) which are related by a many-to-many association:
//Bundle/Entity/Article.php
/**
* #ORM\ManyToMany(targetEntity="Tag")
*/
private $tags;
Now I've got n articles with m tags and I would like to know how often a specific tag has been used.
I'm relatively new to both Symfony2 and Doctrine. The problem is that I don't know where to fit such a query (I guess it should reside in the ArticleRepository but on the other hand it would make sense to have it in the TagRepository) and how to JOIN the correct tables (in this case Article, article_tag, Tag).
The simplest way that I can think of is to just set up a bidirectional relationship between Article and Tag:
class Article
{
/**
* #ORM\ManyToMany(targetEntity="Tag", inversedBy="articles")
*/
private $tags;
}
class Tag
{
/**
* #ORM\ManyToMany(targetEntity="Article", mappedBy="tags")
*/
private $articles;
}
Then you can (assuming you've set up standard getters and setters) use $tag->getArticles()->count();, where $tag is a managed Tag entity, to get the number of articles attached to that tag. This works because when populating ToMany relationship properties, Doctrine uses an instance of Doctrine\Common\Collections\ArrayCollection. Check out the source here.
Also, if you go this route, make sure you read the documentation on picking an owning and inverse side here.
You can count the number of tags by using a specific article..lets say article id=5:
$query=$em->createQuery("SELECT count(t.id) FROM Tag t WHERE ?1 MEMBER OF t.articles");
$query->setParameter(1,5 );
$result = $query->getSingleScalarResult();
This gives me the number of tags in Article Entity only for article id = 5.

Resources