Symfony/Doctrine: Accessing unmapped(?) data within entity - symfony

in Symfony (5.3.7 at present) I've got main data-entities and settings seperated. For example there is a user entity (default stuff), a TypeUserSetting defining the different settings and UserSetting which is m:n in between with the current setting stored.
namespace App\Entity;
/**
* #ORM\Entity(repositoryClass=TypeUserSettingRepository::class)
*/
class TypeUserSetting
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=200)
*/
private $description;
/**
* #ORM\OneToMany(targetEntity=UserSetting::class, mappedBy="setting")
*/
private $userSettings;
/**
* #ORM\Column(type="string", length=255, nullable=true)
*/
private $default_value;
and
class UserSetting
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=4000, nullable=true)
*/
private $value;
/**
* #ORM\ManyToOne(targetEntity=User::class, inversedBy="userSettings")
* #ORM\JoinColumn(nullable=false)
*/
private $user;
/**
* #ORM\ManyToOne(targetEntity=TypeUserSetting::class, inversedBy="userSettings",cascade={"persist"})
* #ORM\JoinColumn(nullable=false)
*/
private $setting;
Thats not that complicated so far...
My problem is, that oftenly settings don't exist, because the users did not set them. In that case I want to use the default.
class User ...
/**
* #ORM\OneToMany(targetEntity=UserSetting::class, mappedBy="user", orphanRemoval=true)
*/
private $userSettings;
...
public function getSettingById(int $id):string
{
foreach ($this->getUserSettings() as $oneSetting) {
if ($oneSetting->getId() === $id)
return $oneSetting->getValue();
}
//Not set, return the default
....
}
And here we go... If there is no setting, the loop fails and I want to get the default form the coresponding TypeUserSetting. This is not mapped, so I have to get it from the database, but I didn't find a way to access that properly.
Possible solutions I found that far:
Insert the UserSetting for all users by SQL when adding a TypeUserSetting. This would avoid the whole problem, but I simply don't like that.
Adding a static method to the TypeUserSetting-repo to get the value. I think that's ugly and somehow going back to last century...
Inject the TypeUserSetting-repo by LifeCycle-hooks which (in my oppinion) isn't the way entities should be used.
Injecting the repo from the controller that calls the function... I think this would be the opposite of encapsulation and separation of concerns. (and I think about hitting my head to the wall, just for having this kind of thoughts)
Anybody got a good idea to solve that?
Thanks in advance

Related

SYMFONY, API PLATFORM how to add edit and show links to the serialized object

I'm working with SYMFONY and API PLATFORM to create REST API.
I have a Project Entity as an API Resource :
class Project
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
*/
private $reference;
/**
* #ORM\Column(type="string", length=255, unique=true)
* #Gedmo\Slug(fields={"reference"})
*/
private $slug;
/**
* #ORM\Column(type="datetime")
* #Gedmo\Timestampable(on="create")
*/
private $createdAt;
/**
* #ORM\Column(type="datetime")
* #Gedmo\Timestampable(on="update")
*/
private $updatedAt;
/**
* #ORM\ManyToOne(targetEntity=User::class, inversedBy="projects")
* #ORM\JoinColumn(nullable=false)
*/
private $user;
/**
* #ORM\ManyToOne(targetEntity=Type::class, inversedBy="projects")
* #ORM\JoinColumn(nullable=false)
*/
private $type;
/**
* #ORM\ManyToOne(targetEntity=Status::class, inversedBy="projects")
* #ORM\JoinColumn(nullable=false)
*/
private $status;
With postman i get :
How can i add edit and show route to get a serialized object like this :
"hydra:member": [
{
...
"status": "/api/statuses/6",
"edit": "<a href='link_to_edit'>edit</a>", // add a link to edit
"show": "<a href='link_to_show'>show</a>" // add a link to show
},
knowing that i don't want to add edit and show to the entity properties or mapped them
Thanks for the help
Technically, you already have your edit and show routes (if you didn't customize them) : you only have to make a PUT or GET request to the value of the #id field of each object.
If you want to add an extra property to your entity, that isn't mapped you can do something like this :
/**
* #SerializedName("edit_route")
*
* #Groups({"projects:read"}))
*
* #return string
*/
public function getEditRoute()
{
return 'your_edit_route';
}
I wouldn't return HTML in this kind of field though, especially if your route is anything else than GET, and apps that use you API might not use HTML, so you're better off returning the simplest value and letting them do their thing with it.

Symfony OneToMany with associative array : new row inserted instead of update

I have to internationalize an app and particularly an entity called Program. To do so, I created an other entity ProgramIntl which contains a "locale" attribute (en_GB, fr_FR, etc) and strings which must be internationalized. I want the programIntl attribute in Program to be an associative array (with locale as key).
We have an API to read/write programs. GET and POST works fine but when I want to update data (PUT), the programIntl is not updated: an insert query is launched (and fails because of the unique constraint, but that's not the question).
Here is the code:
In Program.php:
/**
* #var
*
* #ORM\OneToMany(targetEntity="ProgramIntl", mappedBy="program", cascade={"persist", "remove", "merge"}, indexBy="locale", fetch="EAGER")
* #ORM\JoinColumn(nullable=false, onDelete="cascade")
* #Groups({"program_read", "program_write"})
*/
private $programIntl;
public function addProgramIntl($programIntl)
{
$this->programIntl[$programIntl->getLocale()] = $programIntl;
$programIntl->setProgram($this);
return $this;
}
public function setProgramIntl($programIntls)
{
$this->programIntl->clear();
foreach ($programIntls as $locale => $programIntl) {
$programIntl->setLocale($locale);
$this->addProgramIntl($programIntl);
}
}
public function getProgramIntl()
{
return $this->programIntl;
}
In ProgramIntl.php:
/**
* #ORM\Entity(repositoryClass="App\Repository\ProgramIntlRepository")
* #ORM\Table(name="program_intl",uniqueConstraints={#ORM\UniqueConstraint(name="program_intl_unique", columns={"program_id", "locale"})})
*/
class ProgramIntl
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
* #Groups({"program_read", "program_write"})
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Program", inversedBy="programIntl")
* #ORM\JoinColumn(nullable=false)
*/
private $program;
/**
* #ORM\Column(type="string", length=5, options={"fixed" = true})
*/
private $locale;
/**
* #ORM\Column(type="string", length=64)
* #Assert\NotBlank()
* #Groups({"program_read", "program_write"})
*/
private $some_attr;
/* ... */
}
Any idea of what could be the reason of the "insert" instead of "update" ?
Thanks
I forgot to mention that we use api-platform.
But I found the solution myself. In case anyone is interested, adding the following annotation to classes Program and ProgramIntl solved the problem:
/* #ApiResource(attributes={
* "normalization_context"={"groups"={"program_read", "program_write"}},
* "denormalization_context"={"groups"={"program_read", "program_write"}}
* }) */

Some OneToMany relationships are returned as objects and some as IRIs - API-Platform

I would like to include related questions and question groups as list of IRIs when I make get request to Form.
I have few entities:
Form (fields: question_groups, questions)
QuestionGroup (fields: form, questions)
Question (fields: form, question_group)
When I make get request for the form, it returns:
Form
questions ['/api/questions/1',...]
questionGroups [{id:1,name:'foo',questions: ['/api/questions/1',...]...},...]
As you can see, Form.questions is list of IRIs but Form.questionGroups is list of objects. I would like to have both of them as IRIs.
On the image below under questionGroups field there is a questionGroup field but there is no question field under questions or answer under answers,...
The whole thing makes no sense to me, I have tried to set #MaxDepth, it changed nothing (except when i have used #MaxDepth(0) which threw 500 error without any error massage in response or in php log)
Can anyone explain why, and what should I do to load both questions and questionGroups as list of IRIs?
Thank you
Here are relevant parts of entities mentioned above.
/**
* #ORM\Entity
* #ApiResource
* #ORM\HasLifecycleCallbacks
* #Gedmo\SoftDeleteable(fieldName="deleted_at", timeAware=false, hardDelete=true)
*/
class Form
{
/**
* #ORM\Column(type="text", length=200, nullable=false)
*/
private $name;
/**
* #ORM\OneToMany(targetEntity="QuestionGroup", mappedBy="form", cascade={"REMOVE"})
*/
private $question_groups;
/**
* #ORM\OneToMany(targetEntity="Question", mappedBy="form", cascade={"REMOVE"})
*/
private $questions;
}
/**
* #ORM\Entity
* #ApiResource
* #Gedmo\SoftDeleteable(fieldName="deleted_at", timeAware=false, hardDelete=true)
*/
class QuestionGroup
{
/**
* #ORM\Column(type="string", length=200, nullable=false)
*/
private $name;
/**
* #ORM\ManyToOne(targetEntity="Form",inversedBy="question_groups")
*/
private $form;
/**
* #ORM\OneToMany(targetEntity="Question", mappedBy="question_group", cascade={"REMOVE"})
*/
private $questions;
}
/**
* #ORM\Entity
* #ApiResource
* #Gedmo\SoftDeleteable(fieldName="deleted_at", timeAware=false, hardDelete=true)
*/
class Question
{
/**
* #ORM\Column(type="string", length=200, nullable=false)
*/
private $name;
/**
* #ORM\ManyToOne(targetEntity="Form",inversedBy="questions")
*/
private $form;
/**
* #ORM\ManyToOne(targetEntity="QuestionGroup", inversedBy="questions")
*/
private $question_group;
}
I have figured out that if Entity field/property is snake_case like "$question_groups" then API will return it as list of objects and if its camelCase like "$questionGroups" it will be returned as list of IRIs. Side note: normalisationContext does not like snake_case. if you use normalisationContext and the property is snake_cased than it will be not included in the response data.

Symfony api-platform: user should retrieve his own entities

I have this entity
/**
* #ApiResource()
* #ORM\Entity(repositoryClass="App\Repository\FeedRepository")
*/
class Feed implements AuthoredEntityInterface
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\User", inversedBy="myFeeds")
* #ORM\JoinColumn(nullable=false)
*/
private $user;
/**
* #ORM\Column(type="string", length=255)
*/
private $name;
/**
* #ORM\Column(type="string", length=2083, unique=true)
*/
private $url;
// various getters and setters
}
using the AuthoredEntityInterface I made I can automatically set the user to the logged user.
I'd need to know how to set the collectionOperations so when I am logged in as the user with id = 1, when I call /api/feeds I will only retrieve items with user = 1. If this is possible I would like to do this with an annotation, otherwise any other method is ok.
thanks.
If it is just for connected user, what you need is a current user extension (doctrine extension). Else, you need to create a "subresource' link.
Link to Extension, and to Subresource.
Enjoy :) (and thank you timisorean for the review)

Symfony 3 - Multiple relationships to the same Data Model

I have the following code in Symfony 3:
A class Appointment
<?php
/**
* Appointment
*
* #ORM\Entity
* #ORM\Table(name="ev_appointment")
*/
class Appointment
{
/**
* #ORM\OneToMany(targetEntity="EmailForward", mappedBy="_appointment")
*/
private $_email_forwards;
/**
* #ORM\OneToMany(targetEntity="ParticipationRequest", mappedBy="_appointment")
*/
private $_participation_requests;
}
A class EmailForward
<?php
/**
* #ORM\Entity
* #ORM\Table(name="ev_email_forward")
*/
class EmailForward
{
/**
* #ORM\ManyToOne(targetEntity="Appointment" , inversedBy="_email_forwards")
* #ORM\JoinColumn(name="ev_appointment_id", referencedColumnName="id")
*/
private $_appointment;
/**
* #ORM\Column(type="string", length=255, name="email", nullable=true)
*/
private $_email;
/**
* #ORM\Column(type="datetime", name="forwarded_at", nullable=true)
*/
private $_forwarded_at;
/**
* #ORM\Column(type="string", length=255, name="source", nullable=true)
*/
private $_source;
}
A class ParticipationRequest
<?php
/**
* #ORM\Entity
* #ORM\Table(name="ev_participation_request")
*/
class ParticipationRequest
{
/**
* #ORM\ManyToOne(targetEntity="Appointment", inversedBy="_participation_requests")
* #ORM\JoinColumn(name="ev_appointment_id", referencedColumnName="id")
*/
private $_appointment;
/**
* #ORM\Column(type="string", length=255, name="email", nullable=true)
*/
private $_email;
/**
* #ORM\Column(type="datetime", name="forwarded_at", nullable=true)
*/
private $_forwarded_at;
/**
* #ORM\Column(type="string", length=255, name="source", nullable=true)
*/
private $_source;
}
Now seems to me like I have 2 relationships with 2 Entities that have the exact same structure. So I am wondering, what is the right way to go?
On the one hand I could leave it as it is, because it does, work. But again, if some information was to be the same in both fields, it seems kinda like a waste to have 2 DB entries with the exact same info, and also harder to mantain afterwards.
Is there a more intelligent approach to solve this issue?
You could use e.g. Single Table Inheritance.
By that you only define the structure for EmailForward and ParticipationRequest once and all data will be persisted in one table in the database. During ORM mapping Doctrine will recognize which type you're using and instantiate the correct Object for you.
I don't see how to solve the 'if data is same in both relations it will be peristed twice' because
if it was always the same you wouldn't need two relations
there is no real way to keep it in one persistence - only option I see would be to create another relation from EmailForward and ParticipationRequest which keeps the data which might be needed twice and is referenced from both Objects then.

Resources