How can I add to a collection of a new obect? - symfony

i've got an object with a ManyToMany-relation to an other object via a collection in Typo3 Flow. After create a new instance of that object (which is successfully added to the repository) I can simply add to this collection.
Code snippet of abc Model:
/**
* #var \Doctrine\Common\Collections\Collection<[..]\Domain\Model\Xyz>
* #ORM\ManyToMany(targetEntity="[..]\Domain\Model\Xyz")
*/
protected $xyzs;
[...]
public function getXYZs() {
return $this->xyzs;
}
public function addXYZ([..]\Domain\Model\Xyz $xyz) {
if(!$this->xyzs->contains($xyz))
$this->xyzs->add($xyz);
}
public function removeXYZ([..]\Domain\Model\Xyz $xyz) {
if($this->xyzs->contains($xyz))
$this->xyzs->removeElement($xyz);
}
The problem is that I can't add to this collection before I add it to the repository. (That happens because of non-existing foreign keys I guess).
Code snippet of abc controller (doesn't work!):
public function addAction([...]\$newABC)
{
[...]
$this->abcRepository->add($newABC);
//the line below returns "can't use contains() / add() on a non-object"
$newABC->addXYZ($someXYZ);
[...]
}
The xyz collection doesn't exist in the abc controller until the addAction() is finished completely. But how can I add to this collection before the addAction() is done?

It probably returns a no object error because collections are arrays of objects.
public function addXYZ([..]\Domain\Model\Xyz $xyz) {
$this->xyzs[] = $xyz;
}

The final solution needs a little work arround:
I take $newABC, $someXYZ and some reference stuff via a redirect to a PROTECTED! function in the same controller. (if not protected you could call it with url)
There the persistence manager allready persisted the $newABC. So there I easily can add the $someXYZ and finaly redirect it with my reference to the place I like to go.
Done.

Related

How to denormalize an array recursively in Symfony 5?

I am currently trying to denormalize an array, which came out of an API as a JSON response and was JSON decoded.
The problem is, that I want it to be denormalized into a class and one of the properties is another class.
It feels like it should be possible to get such an easy job done with the Symfony denormalizer, but I always get the following exception:
Failed to denormalize attribute "inner_property" value for class "App\Model\Api\Outer": Expected argument of type "App\Model\Api\Inner", "array" given at property path "inner_property".
My denormalizing code looks like that:
$this->denormalizer->denormalize($jsonOuter, Outer::class);
The denormalizer is injected in the constructor:
public function __construct(DenormalizerInterface $denormalizer) {
The array I try to denormalize:
array (
'inner_property' =>
array (
'property' => '12345',
),
)
Finally the both classes I try to denormalize to:
class Outer
{
/** #var InnerProperty */
private $innerProperty;
public function getInnerProperty(): InnerProperty
{
return $this->innerProperty;
}
public function setInnerProperty(InnerProperty $innerProperty): void
{
$this->innerProperty = $innerProperty;
}
}
class InnerProperty
{
private $property;
public function getProperty(): string
{
return $this->property;
}
public function setProperty(string $property): void
{
$this->property = $property;
}
}
After hours of searching I finally found the reason. The problem was the combination of the "inner_property" snake case and $innerProperty or getInnerProperty camel case. In Symfony 5 the camel case to snake case converter is not enabled by default.
So I had to do this by adding this config in the config/packages/framework.yaml:
framework:
serializer:
name_converter: 'serializer.name_converter.camel_case_to_snake_case'
Here is the reference to the Symfony documentation: https://symfony.com/doc/current/serializer.html#enabling-a-name-converter
Alternatively I could have also add a SerializedName annotation to the property in the Outer class:
https://symfony.com/doc/current/components/serializer.html#configure-name-conversion-using-metadata
PS: My question was not asked properly, because I didn't changed the property and class names properly. So I fixed that in the question for future visitors.

Out of range Ids in Symfony route

I have a common structure for Symfony controller (using FOSRestBundle)
/**
* #Route\Get("users/{id}", requirements={"userId" = "(\d+)"})
*/
public function getUserAction(User $user)
{
}
Now if I request http://localhost/users/1 everything is fine. But if I request http://localhost/users/11111111111111111 I get 500 error and Exception
ERROR: value \"11111111111111111\" is out of range for type integer"
Is there a way to check id before it is transferred to database?
As a solution I can specify length of id
/**
* #Route\Get("users/{id}", requirements={"userId" = "(\d{,10})"})
*/
but then Symfony will say that there is no such route, instead of showing that the id is incorrect.
By telling Symfony that the getUserAction() argument is a User instance, it will take for granted that the {id} url parameter must be matched to the as primary key, handing it over to the Doctrine ParamConverter to fetch the corresponding User.
There are at least two workarounds.
1. Use the ParamConverter repository_method config
In the controller function's comment, we can add the #ParamConverter annotation and tell it to use the repository_method option.
This way Symfony will hand the url parameter to a function in our entity repository, from which we'll be able to check the integrity of the url parameter.
In UserRepository, let's create a function getting an entity by primary key, checking first the integrity of the argument. That is, $id must not be larger than the largest integer that PHP can handle (the PHP_INT_MAX constant).
Please note: $id is a string, so it's safe to compare it to PHP_INT_MAX, because PHP will automatically typecast PHP_INT_MAX to a string and compare it to $id. If it were an integer, the test would always fail (by design, all integers are less than or equal to PHP_INT_MAX).
// ...
use Symfony\Component\Form\Exception\OutOfBoundsException;
class UserRepository extends ...
{
// ...
public function findSafeById($id) {
if ($id > PHP_INT_MAX) {
throw new OutOfBoundsException($id . " is too large to fit in an integer");
}
return $this->find($id);
}
}
This is only an example: we can do anything we like before throwing the exception (for example logging the failed attempt).
Then, in our controller, let's include the ParamConverter annotation:
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
and modify the function comment adding the annotation:
#ParamConverter("id", class="App:User", options={"repository_method" = "findSafeById"})
Our controller function should look like:
/**
* #Get("users/{id}")
* #ParamConverter("id", class="App:User", options={"repository_method" = "findSafeById"})
*/
public function getUserAction(User $user) {
// Return a "OK" response with the content you like
}
This technique allows customizing the exception, but does not give you control over the response - you'll still get a 500 error in production.
Documentation: see here.
2. Parse the route "the old way"
This way was the only viable one up to Symfony 3, and gives you a more fine-grained control over the generated response.
Let's change the action prototype like this:
/**
* #Route\Get("users/{id}", requirements={"id" = "(\d+)"})
*/
public function getUserAction($id)
{
}
Now, in the action we'll receive the requested $id and we'll be able to check whether it's ok. If not, we throw an exception and/or return some error response (we can choose the HTTP status code, the format and anything else).
Below you find a sample implementation of this procedure.
use FOS\RestBundle\Controller\Annotations\Get;
use FOS\RestBundle\Controller\FOSRestController;
use Symfony\Component\Form\Exception\OutOfBoundsException;
use Symfony\Component\HttpFoundation\JsonResponse;
class MyRestController extends FOSRestController {
/**
* #Get("users/{id}", requirements={"id" = "(\d+)"})
*/
public function getUserAction($id) {
try {
if ($id > PHP_INT_MAX) {
throw new OutOfBoundsException($id . " is too large to fit in an integer");
}
// Replace App\Entity\User with your actual Entity alias
$user = $this->getDoctrine()->getRepository('App\Entity\User')->find($id);
if (!$user) {
throw new \Doctrine\ORM\NoResultException("User not found");
}
// Return a "OK" response with the content you like
return new JsonResponse(['key' => 123]);
} catch (Exception $e) {
return new JsonResponse(['message' => $e->getMessage()], 400);
}
}

Doctrine Mongodb getOriginalDocumentData on embedded document

In my symfony application i've got my event_subscriber
CoreBundle\EventSubscriber\CloseIssueSubscriber:
tags:
- { name: doctrine_mongodb.odm.event_subscriber, connection: default }
My subscriber simply listen to postPersist and postUpdate events:
public function getSubscribedEvents()
{
return array(
'postPersist',
'postUpdate',
);
}
public function postPersist(LifecycleEventArgs $args)
{
$this->index($args);
}
public function postUpdate(LifecycleEventArgs $args)
{
$this->index($args);
}
In my index function what I need to do is to get if certain field has changed in particular the issue.status field.
public function index(LifecycleEventArgs $args)
{
$document = $args->getEntity();
$originalData = $uow->getOriginalDocumentData($document);
$originalStatus = $originalData && !empty($originalData['issue']) ? $originalData['issue']->getStatus() : null;
var_dump($originalStatus);
var_dump($document->getIssue()->getStatus());die;
}
In my test what I do is change the issue.status field so I expect to receive 2 different values from the var_dump but instead I got the last status from both.
My document is simply something like that:
class Payload
{
/**
* #ODM\Id
*/
private $id;
/**
* #ODM\EmbedOne(targetDocument="CoreBundle\Document\Issue\Issue")
* #Type("CoreBundle\Document\Issue\Issue")
*/
protected $issue;
}
In the embedded issue document status is simply a text field.
I've also try to use the changeset:
$changeset = $uow->getDocumentChangeSet($document);
foreach ($changeset as $fieldName => $change) {
list($old, $new) = $change;
}
var_dump($old->getStatus());
var_dump($new->getStatus());
Also this two var_dumps returns the same status.
By the time of postUpdate changes in the document are already done so originalDocumentData is adjusted and ready for new calculations. Instead you should hook into preUpdate event and use $uow->getDocumentChangeSet($document); there.
I guess that you want to run index once changes have been written to the database, so on preUpdate you can accumulate changes in the listener and additionally hook into postFlush event to re-index documents.
I found the solution to my problem.
What malarzm said in the other answer is correct but not the solution to my problem.
I suppose that I get only one postUpdate/preUpdate postPersist/prePersist just for the Document (Payload) instead I notice that it get called event for the embedded document (don't know why doctrine consider it a persist).
So the main problem is that I'm waiting for a Payload object instead I have to wait for a Issue object.
In other hands I was unable to use the getOriginalDocumentData work right even in the postUpdate and in the preUpdate so I have to use the getDocumentChangeSet().

how to use Symfony methods Action excluding the "Action" word

I am currently migrating an existent application to Symfony2 that has about 100 controllers with approximately 8 actions in each controller. All the current Actions are named as follow:
public function index(){}
However the default naming convention for Symfony is indexAction().
Is it possible to keep all my current actions and tell Symfony to use as it is without the "Action" word after the method name?
thank you.
Yes, this is possible. You should be able to define routes as normal, but you need to change the way the kernel finds the controller. The best way to do this is to replace/decorate/extends the service 'controller_name_converter'. This is a private service and is injected into the 'controller_resolver' service.
The source code of the class you want to replace is at 'Symfony\Bundle\FrameworkBundle\Controller\ControllerNameParser'.
Basically, the code runs like this. The 'bundle:controller:action' you specified when creating the route is saved in the cache. When a route is matched, that string is given back to the kernel, which in turn calls 'controller_resolver' which calls 'controller_name_resolver'. This class convert the string into a "namespace::method" notation.
Take a look at decorating services to get an idea of how to do it.
Here is an untested class you can work with
class ActionlessNameParser
{
protected $parser;
public function __construct(ControllerNameParser $parser)
{
$this->parser = $parser;
}
public function parse($controller)
{
if (3 === count($parts = explode(':', $controller))) {
list($bundle, $controller, $action) = $parts;
$controller = str_replace('/', '\\', $controller);
try {
// this throws an exception if there is no such bundle
$allBundles = $this->kernel->getBundle($bundle, false);
} catch (\InvalidArgumentException $e) {
return $this->parser->parse($controller);
}
foreach ($allBundles as $b) {
$try = $b->getNamespace().'\\Controller\\'.$controller.'Controller';
if (class_exists($try)) {
// You can also try testing if the action method exists.
return $try.'::'.$action;
}
}
}
return $this->parser->parse($controller);
}
public function build($controller)
{
return $this->parser->build($controller);
}
}
And replace the original service like:
actionless_name_parser:
public: false
class: My\Namespace\ActionlessNameParser
decorates: controller_name_converter
arguments: ["#actionless_name_parser.inner"]
Apparently the Action suffix is here to distinguish between internal methods and methods that are mapped to routes. (According to this question).
The best way to know for sure is to try.
// src/AppBundle/Controller/HelloController.php
namespace AppBundle\Controller;
use Symfony\Component\HttpFoundation\Response;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
class HelloController
{
/**
* #Route("/hello/{name}", name="hello")
*/
public function indexAction($name)
{
return new Response('<html><body>Hello '.$name.'!</body></html>');
}
}
Try to remove the Action from the method name and see what happens.

preUpdate() siblings manage into tree: how to break ->persist() recursion?

Let's say I've got an entity like this
class FooEntity
{
$id;
//foreign key with FooEntity itself
$parent_id;
//if no parent level =1, if have a parent without parent itself = 2 and so on...
$level;
//sorting index is relative to level
$sorting_index
}
Now I would like on delete and on edit to change level and sorting_index of this entity.
So I've decided to take advantage of Doctrine2 EntityListeners and I've done something similar to
class FooListener
{
public function preUpdate(Foo $entity, LifecycleEventArgs $args)
{
$em = $args->getEntityManager();
$this->handleEntityOrdering($entity, $em);
}
public function preRemove(Foo $entity, LifecycleEventArgs $args)
{
$level = $entity->getLevel();
$cur_sorting_index = $entity->getSortingIndex();
$em = $args->getEntityManager();
$this->handleSiblingOrdering($level, $cur_sorting_index, $em);
}
private function handleEntityOrdering($entity, $em)
{
error_log('entity to_update_category stop flag: '.$entity->getStopEventPropagationStatus());
error_log('entity splobj: '.spl_object_hash($entity));
//code to calculate new sorting_index and level for this entity (omitted)
$this->handleSiblingOrdering($old_level, $old_sorting_index, $em);
}
}
private function handleSiblingOrdering($level, $cur_sorting_index, $em)
{
$to_update_foos = //retrieve from db all siblings that needs an update
//some code to update sibling ordering (omitted)
foreach ($to_update_foos as $to_update_foo)
{
$em->persist($to_update_foo);
}
$em->flush();
}
}
The problem here is pretty clear: if I persist a Foo entity, preUpdate() (into handleSiblingOrdering function) trigger is raised and this cause an infinite loop.
My first idea was to insert a special variable inside my entity to prevent this loop: when I started a sibling update, that variable is setted and before executing the update code is checked. This works like a charm for preRemove() but not for preUpdate().
If you notice I'm logging spl_obj_hash to understand this behaviour. With a big surprise I can see that obj passed to preUpdate() after a preRemove() is the same (so setting a "status flag" is a fine) but the object passed to preUpdate() after a preUpdate() isn't the same.
So ...
First question
Someone could point me in the right direction to manage this situation?
Second question
Why doctrine needs to generate different objects if two similar events are raised?
I've founded a workaround
Best approach to this problem seem to create a custom EventSubscriber with a custom Event dispatched programmatically into controller update action.
That way I can "break" the loop and having a working code.
Just to make this answer complete I will report some snippet of code just to clarify che concept
Create custom events for your bundle
//src/path/to/your/bundle/YourBundleNameEvents.php
final class YourBundleNameEvents
{
const FOO_EVENT_UPDATE = 'bundle_name.foo.update';
}
this is a special class that will not do anything but provide some custom events for our bundle
Create a custom event for foo update
//src/path/to/your/bundle/Event/FooUpdateEvent
class FooUpdateEvent
{
//this is the class that will be dispatched so add properties useful for your own logic. In my example two properties could be $level and $sorting_index. This values are setted BEFORE dispatch the event
}
Create a custom event subscriber
//src/path/to/your/bundle/EventListener/FooSubscriber
class FooSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(YourBundleNameEvents::FooUpdate => 'handleSiblingsOrdering');
}
public function handleSiblingsOrdering(FooUpdateEvent $event)
{
//I can retrieve there, from $event, all data I setted into event itself. Now I can run all my own logic code to re-order siblings
}
}
Register your Subscriber as a service
//app/config/config.yml
services:
your_bundlename.foo_listener:
class: Your\Bundle\Name\EventListener\FooListener
tags:
- { name: kernel.event_subscriber }
Create and dispatch events into controller
//src/path/to/your/bundle/Controller/FooController
class FooController extends Controller
{
public function updateAction()
{
//some code here
$dispatcher = $this->get('event_dispatcher');
$foo_event = new FooEvent();
$foo_event->setLevel($level); //just an example
$foo_event->setOrderingIndex($ordering_index); //just an examle
$dispatcher->dispatch(YourBundleNameEvents::FooUpdate, $foo_event);
}
}
Alternative solution
Of course above solution is the best one but, if you have a property mapped into db that could be used as a flag, you could access it directly from LifecycleEventArgs of preUpdate() event by calling
$event->getNewValue('flag_name'); //$event is an object of LifecycleEventArgs type
By using that flag we could check for changes and stop the propagation
You are doing wrong approach by calling $em->flush() inside preUpdate, I even can say restricted by Doctrine action: http://doctrine-orm.readthedocs.org/en/latest/reference/events.html#reference-events-implementing-listeners
9.6.6. preUpdate
PreUpdate is the most restrictive to use event, since it is called
right before an update statement is called for an entity inside the
EntityManager#flush() method.
Changes to associations of the updated entity are never allowed in
this event, since Doctrine cannot guarantee to correctly handle
referential integrity at this point of the flush operation.

Resources