Symfony 5 : setting a property based on a parent property value - symfony

In Symfony 5, let's say we have 3 entities linked like this :
Foo is an entity that have Bar as child. Foo as one property called fooProperty.
Bar have Foo as parent, and Baz as child
Baz have Bar as parent, of course. Baz have one property called bazProperty.
Let's say that the value of bazProperty is dependent of the value of the value of fooProperty. My first idea was to refer to foo entity inside the baz entity class :
function setBazProperty($value) {
if ($this->getBar()->getFoo()->getFooProperty > 0) {
$this->bazProperty = $value;
} else {
$this->bazProperty = 0;
}
}
But this occur many sql queries, as Doctrine will ask first to get Bar entity, then Foo entity.
So I imagine to access to Foo entity through an unique query managed in a repository class.
But, because of the separation of concern, I wouldn't inject the repository in the Baz entity, but I would use a service instead.
So I've created a BazService with two arguments in the constructor :
public function __construct(Baz $baz, BazRepository $bazRepository)
{
$this->baz = $baz;
$this->bazRepository= $bazRepository;
}
In this service, I've also added a method fetching Foo entity :
public function getFoo()
{
return $this->bazRepository->getFoo($this->baz);
}
And last, in a controller, now I would like to get Foo entity :
$bazService = new BazService($baz);
$foo = $bazService->getFoo();
Here are my questions :
I'm not able to initialize bazService in the controller. The constructor ask for 2 argmuments (the entity and the repository) and I would like only to provide the entity and to inject automatically the repository class.
I've tried to add it in serices.yaml without success (probably because I didn't instantiate bazService in the constructor of my controller) :
App\Service\BazService:
arguments:
$bazRepository: App\Repository\BazRepository
Is there any other solution ? How I can inject the entity class differently in the service class ?
Using a service when setting a property is too complex is the recommended solution ? Some article (here, here and here) recommend to use a service when the method inside entity class become more complex and require external entity or repositories. But maybe there's a lighter solution...

Separation of concerns is IMHO the right argument to look at. There are some approaches to go for, that depend largely on how you retrieve the entity. However, in my opinion, the concern of an entity is NOT to fetch some other entities data from the database, it is the repository's or maybe the controller's concern. So let's see how to do that ...
One way is to automatically retrieve the parent entity / entities. Depending on your use case, you could do that in general (via fetch="EAGER" - see: #ManyToOne / #OneToOne), otherwise you could implement a special repository function, that fetches the additional entities. If your entities always only have at most one parent each, this can absolutely reduce the number of queries from 3 to 1, since the parent and parent of parent entities can be retrieved simultaneously.
// in BazRepository
public function getWithParents($id) {
$qb = $this->createQueryBuilder('baz');
$qb->leftJoin('baz.bar', 'bar')
->addSelect('bar')
->leftJoin('bar.foo', 'foo')
->addSelect('foo')
->where('baz.id = :id')
->setParameter('id', $id);
return $qb->getQuery()->getOneOrNullResult();
}
if the child entity accesses the parent entity, it should just use the entity from cache and avoid a second query (source: https://symfonycasts.com/screencast/doctrine-relations/join-n-plus-one)
If having the entities is already "too much", you can slightly cheat by (again) creating a custom repository method that not only fetches the Baz entity, but also the Foo.fooProperty value and sets it for a virtual/temporary property on the Baz entity.
// in BazRepository
public function getWithFooProperty(int $id) {
$qb = $this->createQueryBuilder('baz');
$qb->leftJoin('baz.bar', 'bar')
->lefTJoin('bar.foo', 'foo')
->select('foo.fooProperty as fooProperty')
->where('baz.id = :id')
->setParameter('id', $id);
$result = $qb->getQuery()->getResult(); // should be an array with an array with two keys, but I might be wrong
if(count($result) == 0) {
return null;
}
$baz = $row[0][0];
$baz->fooProperty = $row[0][1];
return $baz;
}
(Disclaimer: please check the $result here, to see if the accesses are correct)
you now could access it in Baz:
function getFooProperty() {
if(isset($this->fooProperty)) {
return $this->fooProperty;
} else {
// fallback, in case entity was fetched by another repository method
return $this->getBar()->getFoo()->getFooProperty();
}
}

Related

Receive all kind of Entity as an Argument in Symfony

I have a method to get a change set of Entity from the unit of work (entity manager) in Symfony and I would like it to receive all Entities (Post, User...) instead of specific Entity.
public function getChanges(Post $event): array
{
$uow = $this->entityManager->getUnitOfWork();
$uow->computeChangeSets();
return $uow->getEntityChangeSet($event);
}
Do you have any idea to do it?
One solution is getting the object as the argument but I prefer to get only the Symfony Entity objects in the function.
Look for doctrine entity listener.
https://symfony.com/doc/current/doctrine/events.html#doctrine-entity-listeners
And do not filter entity inside it, remove this part from the example:
// if this listener only applies to certain entity types,
// add some code to check the entity type as early as possible
if (!$entity instanceof Product) {
return;
}
if (!$this->entityManager->contains($entity)) {
throw new Exception('The given object must be doctrine entity');
}

What is the best way to create a singleton entity in Symfony 4?

I want to create a settings page, which only has a form in it. If the form is submitted it only updates settings entity but never creates another one. Currently, I achieved this like:
/**
* #param SettingsRepository $settingsRepository
* #return Settings
*/
public function getEntity(SettingsRepository $settingsRepository): Settings
{
$settings = $settingsRepository->find(1);
if($settings == null)
{
$settings = new Settings();
}
return $settings;
}
In SettingsController I call getEntity() method which returns new Settings entity (if the setting were not set yet) or already existing Settings entity (if setting were set at least once).
However my solution is quite ugly and it has hardcoded entity id "1", so I'm looking for a better solution.
Settings controller:
public function index(
Request $request,
SettingsRepository $settingsRepository,
FlashBagInterface $flashBag,
TranslatorInterface $translator,
SettingsService $settingsService
): Response
{
// getEntity() method above
$settings = $settingsService->getEntity($settingsRepository);
$settingsForm = $this->createForm(SettingsType::class, $settings);
$settingsForm->handleRequest($request);
if ($settingsForm->isSubmitted() && $settingsForm->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($settings);
$em->flush();
return $this->redirectToRoute('app_admin_settings_index');
}
return $this->render(
'admin/settings/index.html.twig',
[
'settings_form' => $settingsForm->createView(),
]
);
}
You could use Doctrine Embeddables here.
Settings, strictly speaking, should not be mapped to entities, since they are not identifiable, nor meant to be. That is, of course, a matter of debate. Really, a Settings object is more of a value object than an entity. Read here for more info.
So, in cases like these better than having a one to one relationship and all that fuzz, you probably will be fine with a simple Value Object called settings, that will be mapped to the database as a Doctrine Embeddable.
You can make this object a singleton by creating instances of it only in factory methods, making the constructor private, preventing cloning and all that. Usually, it is enough only making it immutable, meaning, no behavior can alter it's state. If you need to mutate it, then the method responsible for that should create a new instance of it.
You can have a a method like this Settings::createFromArray() and antoher called Settings::createDefaults() that you will use when you new up an entity: always default config.
Then, the setSettings method on your entity receieves only a settings object as an argument.
If you don't like inmutablity, you can also make setter methods for the Settings object.

Dynamically eager loading deep relationships with Doctrine

I'm currently working on an API using the following stack;
Symfony (3)
FOSRestBundle
Fractal
I'm wanting to integrate the ability to specify, via query parameter, which relationships to include when retrieving an entity/collection, e.g;
[GET] /users?include=friends.addresses
Fractal comes with the ability to handle includes however, as this happens around the serialization point of the response building, each related entity is retrieved via lazy loading, thus triggering additional queries.
Is there a way to tell Doctrine, when retrieving a collection, to dynamically also retrieve relationships specified? Ive seen the following from the Doctrine docs which shows how to dynamically change the fetch mode however this only seems to work with associations on the target entity (friends in the example above) and not deeper relations (addresses of friends in the example).
Thanks!
If I remember correctly you can "preload" relations by joining them in rather than letting the lazy loading mechanism handle it. An idea could be to create a service that creates a query builder based on your criteria. This is a crude snippet of what I mean:
class EagerService
{
protected $em;
public function __construct(EntityManager $em)
{
$this->em = $em;
}
public function resolveIncludes($class, $alias, $includes)
{
// Parse includes into an array
if (strpos($includes, '.') !== false) {
$relations = explode('.', $includes);
} else {
$relations = [$includes];
}
// The next relation is owned by the previous one, so we keep track of the previous relation
$previousRelation = $alias;
$qb = $em->getRepository($class)->getQueryBuilder($previousRelation);
foreach ($relations as $relation) {
// Add inner joins to the query builder referencing the new relation
$qb->innerJoin("{$previousRelation}.{$relation}", $relation);
$previousRelation = $relation;
}
// Return query builder or the result of the query
return $qb;
}
}

Symfony2-entityManager inside an Entity

I am using Symfony version 2.7.6. I have created an entity named EmployeeBasicInfo having fields
firstname
lastname
identificationCode etc
I have created a callback function for validating Identification code in EmployeeBasicInfo entity itself which looks like
/**
* #Assert\Callback(groups={"edit_myinfo"})
*/
public function validateIdentificationCode(ExecutionContextInterface $context)
{
if ($this->getEmployeeFirstName() == 'fakename') {
$context->buildViolation('This name sounds totally fake!')
->atPath('employeeFirstName')
->addViolation();
}
}
and this callback function works properly
Actually I want such a callback functionality which checks identidfication code against database. I have added $em = $this->getDoctrine()->getManager(); inside the callback function and the error is like Attempted to call an undefined method named "getDoctrine" of class "XXX\EmployeeBundle\Entity\EmployeeBasicInfo".. Please advise me the effective way
Do not inject the EntityManager in your Entity. One basic concept of the DataMapper-Pattern is, that your entity does not have to know about your data source and its connectors.
I'd suggest to write a custom validation constraint, in which you inject the dependencies you need.
EntityManager, Repository to query, etc. Whatever service suits you.
Have a look at how to create custom constraint validators with dependencies
I would suggest you use a service to do this
class EmployeeUtility($connection)
{
public function __construct($conn) { $this->connection = $v; }
public function validateIdentificationCode($emloyeeId, $validationCode)
{
// Your code here
}
}
In your controller, you inject the service:
$employeeUtility = $this->get('employee.utility');
$employeeUtility->validateIdentificationCode(1,'GF38883dkDdW3373d');
Alternatively, add the code in a repository class.

$em->remove() symfony2 erasing all rows

I have an issue when erasing something from a BD.
The problem is that it not only erase the object i looked for (using findOneBy), but all the objects related to the principal id.
//---Controller
$new = $this->getDoctrine()->getManager();
$OBJcar = $new->getRepository('SomeOtherBundle:CarEntityClass')
->findOneBy(array('idOwner' => $idowner, 'idCar' => $idcar));
if($OBJcar){
$new->remove($OBJcar);
$new->flush();
$msj="The car for an specific owner has been erased.";
}
//---Profiler (Query)
"START TRANSACTION"
Parameters: { }
Time: 0.22 ms
DELETE FROM schema.CarTable WHERE id_owner = ?
Parameters: ['123456']
Time: 0.63 ms
"COMMIT"
Parameters: { }
Time: 0.63 ms
How to erase the one row i am getting from the db?
I voted down the answer above, because I'm tired of people using string DQLs.
It's not standartized, non-object oriented(even though in background dql operates with objects), it doesn't use caching mechanisms query builder provides, it's non-flexible and simply looks unclean.
Here is the "right way"(IMHO):
You add the repository class for entity
You add the method you need with a query builder in it
You call the method while passing parameters needed for specific REPOSITORY OBJECT ORIENTED ACTION
You get an easy-to-handle result
Here's the code:
namespace ProjectName\BundleName\Repository;
use Doctrine\ORM\EntityRepository;
class CarRepository extends EntityRepository
{
public function deleteCarWithOwner($ownerId,$carId)
{
$isDeleted = $this->createQueryBuilder("car")
->delete()
->where('car.id = :carId')->setParameter("carId", $carId)
->andWhere('car.idOwner = :ownerId')->setParameter("ownerId", $ownerId)
->getQuery()->execute();
return $isDeleted;
}
}
Also, refer to http://doctrine-orm.readthedocs.org/en/latest/reference/query-builder.html for query builder details. There are a lot of "pros" and I see no "cons" for using builder.
LATE UPDATE
Also, a lot of Doctrine's entity events are not dispatched when using DQL.
Use DQL
$query = $em->createQuery('DELETE SomeOtherBundle:CarEntityClass c WHERE c.idOwner = 4 AND c.id = 10');
$query->execute();
This will remove only single car with ID 10 and owner with ID 4.

Resources