CakePHP 2x to 3x, HABTM with keepExisting - has-and-belongs-to-many

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!!!

Related

n-n relation in Backpackforlaravel: doesn't delete corresponding items

I'm having troubles with a n-n relationship in my Backpackforlaravel app. I have Registrations and Sessions, the setup in the model looks like this:
Registration:
public function sessions()
{
return $this->belongsToMany(\App\Models\Session::class);
}
Session:
public function registrations()
{
return $this->belongsToMany(\App\Models\Registration::class);
}
So the setup seems to be fine, and I also have a registration_session table in my database.
What I want to achieve is, that whenever a Session gets deleted, I want to delete all the entries in the registration_session table, but also all the Registrations. I thought the deletion of the entries in the registration_session table maybe is a standard, but it didn't delete them when I deleted a session. In order to achieve both, I did the following in the destroy function of my SessionCrudController:
public function destroy($id)
{
$this->crud->hasAccessOrFail('delete');
foreach ($this->crud->getCurrentEntry()->registrations as $registration) {
$registration->delete();
}
DB::table('registration_session')->where('session_id', $id)->delete(); //really required?
return $this->crud->delete($id);
}
I have the feeling that I'm doing things way more complicated than I should, so I would appreciate any recommendations.
Update: this is how I store the data initially in the table...the information is coming from a form, so it's not added in a CRUD panel:
DB::table('registration_session')->insert([
'registration_id' => $reg->id,
'session_id' => $sesId
]);
By default, Backpack or Laravel don't concern themselves with deleting related entries, because they assume your Database layer will handle it. With most DBMSs you can specify such rules, and Laravel makes it easy to do so in their migrations. Take a look at foreign key constraints in the Laravel docs, it's pretty easy to build your migration in such a way that any time one item gets deleted, the related items will to, using "cascade":
$table->foreignId('user_id')
->constrained()
->onUpdate('cascade')
->onDelete('cascade');

nWidart Modules relationships

I have an app and I want to allow modules in it. I find nWidart /Laravel-modules is the best solution for this. I have used it in the past, but in my previous projects I was the sole developer, so when I created a model inside a module, to create the relationships between it and one of my base models, I just went and edit both files:
In App\Models\Disease I would add a new method:
public function symptoms(){
return $this->hasMany( Modules\Treatments\Entities\Symptom::class );
}
In Modules\Treatments\Entities\Symptom, the opposite:
public function disease(){
return $this->belongsTo( App\Models\Symptom::class );
}
Now, I would like to create those relationships, but without writing code in the App\Models files (of course I know there would be some modification required to make it work, I just mean without having to edit the files every time a module is created). Is there a way to do it? Is it possible to work that out?
Ok, in the module Entities I created a new model Modules\Treatments\Entities\Disease which extends from App\Models\Disease and I inserted the relationship method there.
use App\Models\Disease as BaseDisease;
class Disease extends BaseDisease{
/**
* Relationship between a Disease and it's symptoms
*
* #return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function symptoms(){
return $this->hasMany( Modules\Treatments\Entities\Symptom::class );
}
}
The down side of this approach is that the relationship is only present when I load the Disease from Modules\Treatments\Entities\Disease but I couldn't find another way to do it

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.

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

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.

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.

Resources