Symfony onKernetRequest detect which table - symfony

I have the following Listener in my code:
class BeforeRequestListener
{
/**
* #var EntityManager
*/
private $em;
/**
* #var SessionInterface
*/
private $session;
/**
* BeforeRequestListener constructor.
* #param EntityManager $em
* #param SessionInterface $session
*/
public function __construct(EntityManager $em,SessionInterface $session)
{
$this->em = $em;
$this->session =$session;
}
/**
* #param GetResponseEvent $event
*/
public function onKernelRequest(GetResponseEvent $event){
//if() HERE
$filter = $this->em->getFilters()
->enable(Utils::CLIENT_FILTER_NAME);
$filter->setParameter(Utils::CLIENT_ID_NAME, $this->session->get(Utils::CLIENT_ID_NAME));
}
}
And this listener is made to enable some filter. i need to detect on which table/tables this is request id done so i can disable it when there is no column called client_id which is btw the CLIENT_ID_NAME const.
the reason is mentioned here : Doctrine and symfony filter, debug the filter
i need to apply the filter only after a user logs in and some tables doesnt have the client_id field so i want to disable that check on these tables.
Thanks!

If your request contains some information about tables, you can retrieve it like this:
$tables = $event->getRequest()->get('tables');
or you can assume affected entity/table by requested route:
$request = $event->getRequest();
// Matched route
$_route = $request->attributes->get('_route');
// Matched controller
$_controller = $request->attributes->get('_controller');
By default Request object doesn't contain any information about table because it just passes some parameters to some action attached to some route, and the action interacts with some data model which presents some database table.

I made it (a bit dirty) not the way i wanted it but it works on the current db scale i have.
just added the following code to the addFilterConstraint
if(
$targetEntity->getTableName() == 'table' ||
$targetEntity->getTableName() == 'table2' ||
$targetEntity->getTableName() == 'table3' ||
$targetEntity->getTableName() == 'table4' ||
$targetEntity->getTableName() == 'table5'
){
return true;
}
return $targetTableAlias.'.'.Utils::CLIENT_ID_NAME.' = '. $this->getParameter(Utils::CLIENT_ID_NAME);
p.s in case someone got into this and noticed some strange behavior when using return ""; just replace it with return true; sometimes it happens sometimes not i am still not sure why.

Related

What is correct way of injecting a nullable entity parameter in symfony5?

Whenever I want to use an entity as a parameter (in this case SupportType) this is the quickest way:
/**
* #Route("/new/{type}", name="app_support_case_new", methods={"GET", "POST"})
*/
public function new(Request $request, SupportCaseRepository $supportCaseRepository, SupportType $type = null): Response
{
$user = $this->getUser();
$now = new \DateTime();
$supportCase = new SupportCase();
$supportCase->setEmail($user->getEmail());
$supportCase->setDateCreated($now);
$supportCase->setMinutesSpent(0);
$supportCase->setPublic(0);
if($type != null) {
$supportCase->setSupportType($type);
}
however there is a security issue here, because whenever no parameter is passed, symfony just picks the first random instance of the SupportType (as if it did a ->findAll() on the repository and picked the first one).
What is going on here?
It will never turn up as null even when I expect it to, i e when my route stops at /new and there is definately no parameter passed.
Instead I end up having to go through this hassle below whenever I want a parameter to actually be null if nothing is passed:
/**
* #Route("/new/{type}", name="app_support_case_new", methods={"GET", "POST"})
*/
public function new(Request $request, SupportCaseRepository $supportCaseRepository, SupportTypeRepository $supportTypeRepository, $type = null): Response
{
....................................
if($type != null) {
$supportType = $supportTypeRepository->findOneBy(array('id'=>$type));
if (is_null($supportType)) {
die('No type with that ID exist. Hey, are you trying to hack me?');
} else {
$supportCase->setSupportType($supportType);
}
}

Trigger Doctrine Event

I have an event listener for preUpdate Doctrine Event which does the job just fine, but if request data is empty except image_data, it is not triggered. And it's logically correct, because there is no image_data column in an Entity, thus it doesn't see a field to change. The idea is to process image_data array, then do an image upload and finally store filename to image ORM column which works if one of the fields present in request. Let me show my code:
Controller
public function patch($id, Request $request)
{
$data = $request->request->all();
$company = $this->repo->find($id);
$form = $this->createForm(CompanyType::class, $company);
$form->submit($data, false);
if (false === $form->isValid()) {
// error
}
$this->em->flush();
// success
}
Form
// ...
->add('image_data', Types\TextType::class)
->add('image', Types\TextType::class)
// ...
Config
# ...
- { name: doctrine.event_listener, event: preUpdate }
Entity
trait UploadableTrait
{
/**
* #ORM\Column(type="string", length=255, nullable=true)
*/
private $image;
/**
* #Exclude()
*/
private $image_data = [];
How I solved it so far - in BeforeActionSubscriber I set image property to 1 therefore preUpdate is fired, upload is handled and real image filename is stored in result. I believe there is smarter way of doing it. Formerly, I used single image parameter for input/output and it worked for preUpdate because there is such ORM column, however I didn't like this approach because incoming image data is an array (image_name, image_body, content_type and image_size), while output data type is string (filename) and I decided to separate it (image_data for POST|PATCH|PUT and image is just result filename). How may I trigger preUpdate? :)
You can use an updatedAt field like this: https://github.com/dustin10/VichUploaderBundle/blob/master/Resources/doc/known_issues.md#the-file-is-not-updated-if-there-are-not-other-changes-in-the-entity
class MyEntitty
{
// ...
/**
* #ORM\Column(type="datetime")
*
* #var \DateTime|null
*/
private $updatedAt;
// ...
public function setSomething($something): void
{
$this->something= $something;
$this->updatedAt = new \DateTime('now');
}
}

Symfony: Create new entity inside doctrine EntityListener with ManyToOne association

I'm trying to record every change in quantity of a given item. For that purpose, I listen for a change of an Item entity and wish to create a new Transaction instance with details about the action. So I'm creating an entity inside a listener.
I've set up everything according to the documentation and created the listener based on this example.
The code (I believe) is relevant for my problem is following.
ItemListener
// ...
private $log;
/** #ORM\PreUpdate */
public function preUpdateHandler (Item $item, PreUpdateEventArgs $args)
{
$changeSet = $args->getEntityManager()->getUnitOfWork()->getEntityChangeSet($item)['quantity'];
$quantityChange = $changeSet[1] - $changeSet[0];
$transaction = new Transaction();
$transaction->setItem($item);
$transaction->setQuantityChange($quantityChange);
$this->log = $transaction;
}
/** #ORM\PostUpdate */
public function postUpdateHandler(Item $item, LifecycleEventArgs $args)
{
$em = $args->getEntityManager();
$em->persist($this->log);
$em->flush();
}
This works perfectly. However, the problem is when I add another field to the transaction entity. The user field inside Transaction entity has ManyToOne relation. Now when I try to set the user inside the preUpdateHandler, it leads to and undefined index error inside the UnitOfWork function of the Entity Manager.
Notice: Undefined index: 000000003495bf92000000001108e474
The listener is now like this. I retreive the user based on the token that was sent with the request. Therefore, I inject the request stack and my custom user provider in the listener's constructor. I do not think this is the source of the problem. However, if necessary, I'll edit the post and add all the remaining code (rest of the listener, services.yaml and user provider).
ItemListener
// ...
private $log;
/** #ORM\PreUpdate */
public function preUpdateHandler (Item $item, PreUpdateEventArgs $args)
{
$changeSet = $args->getEntityManager()->getUnitOfWork()->getEntityChangeSet($item)['quantity'];
$quantityChange = $changeSet[1] - $changeSet[0];
$transaction = new Transaction();
$transaction->setItem($item);
$transaction->setQuantityChange($quantityChange);
$request = $this->requestStack->getCurrentRequest();
$company = $this->userProvider->getUserByRequest($request);
$this->log = $transaction;
}
/** #ORM\PostUpdate */
public function postUpdateHandler(Item $item, LifecycleEventArgs $args)
{
$em = $args->getEntityManager();
$em->persist($this->log);
$em->flush();
}
I do not understand why retreiving the flush with retrieval of another entity leads to that error. When searching for an answer I found that that many recommend not to use flush() inside the postUpdate cycle but rather in postFlush. However, this method is not defined for Entity listeners according to the documentation and if possible, I'd like to stick to such a listener and not an event listener.
Thank you for any help. I also include the transaction entity code just in case.
Transaction Entity
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use App\DoctrineUtils\MagicAccessors;
use App\Entity\T\TIdentifier;
/**
* #ORM\Entity
* #ORM\Table(name="transaction")
*/
class Transaction
{
use TIdentifier;
use MagicAccessors;
/**
* #ORM\ManyToOne(targetEntity="Item")
* #ORM\JoinColumn(name="item_id", referencedColumnName="id", nullable=false)
*/
public $item;
/**
* #ORM\Column(type="decimal", length=14, precision=4, nullable=false)
*/
public $quantityChange;
/**
* #ORM\Column(type="datetime", nullable=true)
*/
private $createdTime;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\User")
* #ORM\JoinColumn(nullable=false)
*/
private $user;
public function __construct()
{
$this->createdTime = new \DateTime();
}
/**
* #param mixed $quantityChange
*/
public function setQuantityChange(int $quantityChange): void
{
$this->quantityChange = $quantityChange;
}
/**
* #param mixed $createdTime
*/
public function setCreatedTime($createdTime): void
{
$this->createdTime = $createdTime;
}
/** #ORM\PrePersist **/
public function onCreate() : void
{
$this->setCreatedTime(new \DateTime('now'));
}
public function setUser(?User $user): self
{
$this->user= $user;
return $this;
}
}
I found out that the problem was that another instance of the entity manager was instantiated in the getUserByRequest() function, where I log that the user's token was used. Apart others, I created inside it a new manager, persisted the entry and flushed the result. However, the new entity manager does not know about the unit of work inside the other entity manager inside the listener. Hence the undefined index error.
I tried to omit the persist and the flush part inside the user getter function, but that was not enough. In the end I solved the problem by passing the given instance entity manager from inside the listener to the getter function. So basically, I ended up calling this from the preUpdateHandler function inside the listener.
$em = $args->getEntityManager();
$company = $this->userProvider->getUserByRequest($request, $em);
Hope this helps if you find yourself in a similar pickle.

Add dynamic property on entity to be serialized

I have this REST API. Whenever request comes to get a resource by id ( /resource/{id}) I want to add a permissions array on that object on the fly (entity itself does not have that field).
What I came up with is this event listener. It checks the result the controller has returned:
class PermissionFinderListener {
...
public function onKernelView(GetResponseForControllerResultEvent $event) {
$object = $event->getControllerResult();
if (!is_object($object) || !$this->isSupportedClass($object)) {
return;
}
$permissions = $this->permissionFinder->getPermissions($object);
$object->permissions = $permissions;
$event->setControllerResult($object);
}
....
}
The problem is that the JMS Serializer opts out this dynamic property on serialization. I tried making the onPostSerialize event subscriber on JMS serializer, but then there are no clear way to check if this is a GET ONE or GET COLLECTION request. I don't need this behaviour on GET COLLECTION and also it results a huge performance hit on collection serialization. Also I don't want to create any base entity class with permission property.
Maybe there is some other way to deal with this scenario?
What I could imagine is a combination of Virtual Property and Serialization Group:
Add a property to your entity like:
/**
* #Serializer\VirtualProperty
* #Serializer\SerializedName("permissions")
* #Serializer\Groups({"includePermissions"}) */
*
* #return string
*/
public function getPermissions()
{
return $permissionFinder->getPermissions($this);
}
Only thing you need to do then is to serialize 'includePermissions' group only in your special case (see http://jmsyst.com/libs/serializer/master/cookbook/exclusion_strategies)
If you don't have access to $permissionFinder from your entity you could as well set the permission attribute of an entity from a Controller/Service before serializing it.
EDIT:
This is a bit more code to demonstrate what I mean by wrapping your entity and using VirtualProperty together with SerializationGroups. This code is not tested at all - it's basically a manually copied and stripped version of what we're using. So please use it just as an idea!
1) Create something like a wrapping class for your entity:
<?php
namespace Acquaim\ArcticBundle\Api;
use JMS\Serializer\Annotation as JMS;
/**
* Class MyEntityApi
*
* #package My\Package\Api
*/
class MyEntityApi
{
/**
* The entity which is wrapped
*
* #var MyEntity
* #JMS\Include()
*/
protected $entity;
protected $permissions;
/**
* #param MyEntity $entity
* #param Permission[] $permissions
*/
public function __construct(
MyEntity $entity,
$permissions = null)
{
$this->entity = $entity;
$this->permissions = $permissions;
}
/**
* #Serializer\VirtualProperty
* #Serializer\SerializedName("permissions")
* #Serializer\Groups({"includePermissions"})
*
* #return string
*/
public function getPermissions()
{
if ($this->permissions !== null && count($this->permissions) > 0) {
return $this->permissions;
} else {
return null;
}
}
/**
* #return object
*/
public function getEntity()
{
return $this->entity;
}
}
2) In your controller don't return your original Entity, but get your permissions and create your wrapped class with entity and permissions.
Set your Serialization Context to include permissions and let the ViewHandler return your serialized object.
If you don't set Serialization Context to includePermissions it will be excluded from the serialized result.
YourController:
$myEntity = new Entity();
$permissions = $this->get('permission_service')->getPermissions();
$context = SerializationContext::create()->setGroups(array('includePermissions'));
$myEntityApi = new MyEntityApi($myEntity,$permissions);
$view = $this->view($myEntityApi, 200);
$view->setSerializationContext($context);
return $this->handleView($view);

Symfony2 and ParamConverter(s)

Accessing my route /message/new i'm going to show a form for sending a new message to one or more customers. Form model has (among others) a collection of Customer entities:
class MyFormModel
{
/**
* #var ArrayCollection
*/
public $customers;
}
I'd like to implement automatic customers selection using customers GET parameters, like this:
message/new?customers=2,55,543
This is working now by simply splitting on , and do a query for getting customers:
public function newAction(Request $request)
{
$formModel = new MyFormModel();
// GET "customers" parameter
$customersIds = explode($request->get('customers'), ',');
// If something was found in "customers" parameter then get entities
if(!empty($customersIds)) :
$repo = $this->getDoctrine()->getRepository('AcmeHelloBundle:Customer');
$found = $repo->findAllByIdsArray($customersIds);
// Assign found Customer entities
$formModel->customers = $found;
endif;
// Go on showing the form
}
How can i do the same using Symfony 2 converters? Like:
public function newAction(Request $request, $selectedCustomers)
{
}
Answer to my self: there is not such thing to make you life easy. I've coded a quick and dirty (and possibly buggy) solution i'd like to share, waiting for a best one.
EDIT WARNING: this is not going to work with two parameter converters with the same class.
Url example
/mesages/new?customers=2543,3321,445
Annotations:
/**
* #Route("/new")
* #Method("GET|POST")
* #ParamConverter("customers",
* class="Doctrine\Common\Collections\ArrayCollection", options={
* "finder" = "getFindAllWithMobileByUserQueryBuilder",
* "entity" = "Acme\HelloBundle\Entity\Customer",
* "field" = "id",
* "delimiter" = ",",
* }
* )
*/
public function newAction(Request $request, ArrayCollection $customers = null)
{
}
Option delimiter is used to split GET parameter while id is used for adding a WHERE id IN... clause. There are both optional.
Option class is only used as a "signature" to tell that converter should support it. entity has to be a FQCN of a Doctrine entity while finder is a repository method to be invoked and should return a query builder (default one provided).
Converter
class ArrayCollectionConverter implements ParamConverterInterface
{
/**
* #var \Symfony\Component\DependencyInjection\ContainerInterface
*/
protected $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
function apply(Request $request, ConfigurationInterface $configuration)
{
$name = $configuration->getName();
$options = $this->getOptions($configuration);
// Se request attribute to an empty collection (as default)
$request->attributes->set($name, new ArrayCollection());
// If request parameter is missing or empty then return
if(is_null($val = $request->get($name)) || strlen(trim($val)) === 0)
return;
// If splitted values is an empty array then return
if(!($items = preg_split('/\s*'.$options['delimiter'].'\s*/', $val,
0, PREG_SPLIT_NO_EMPTY))) return;
// Get the repository and logged user
$repo = $this->getEntityManager()->getRepository($options['entity']);
$user = $this->getSecurityContext->getToken()->getUser();
if(!$finder = $options['finder']) :
// Create a new default query builder with WHERE user_id clause
$builder = $repo->createQueryBuilder('e');
$builder->andWhere($builder->expr()->eq("e.user", $user->getId()));
else :
// Call finder method on repository
$builder = $repo->$finder($user);
endif;
// Edit the builder and add WHERE IN $items clause
$alias = $builder->getRootAlias() . "." . $options['field'];
$wherein = $builder->expr()->in($alias, $items);
$result = $builder->andwhere($wherein)->getQuery()->getResult();
// Set request attribute and we're done
$request->attributes->set($name, new ArrayCollection($result));
}
public function supports(ConfigurationInterface $configuration)
{
$class = $configuration->getClass();
// Check if class is ArrayCollection from Doctrine
if('Doctrine\Common\Collections\ArrayCollection' !== $class)
return false;
$options = $this->getOptions($configuration);
$manager = $this->getEntityManager();
// Check if $options['entity'] is actually a Dcontrine one
try
{
$manager->getClassMetadata($options['entity']);
return true;
}
catch(\Doctrine\ORM\Mapping\MappingException $e)
{
return false;
}
}
protected function getOptions(ConfigurationInterface $configuration)
{
return array_replace(
array(
'entity' => null,
'finder' => null,
'field' => 'id',
'delimiter' => ','
),
$configuration->getOptions()
);
}
/**
* #return \Doctrine\ORM\EntityManager
*/
protected function getEntityManager()
{
return $this->container->get('doctrine.orm.default_entity_manager');
}
/**
* #return \Symfony\Component\Security\Core\SecurityContext
*/
protected function getSecurityContext()
{
return $this->container->get('security.context');
}
}
Service definition
arraycollection_converter:
class: Acme\HelloBundle\Request\ArrayCollectionConverter
arguments: ['#service_container']
tags:
- { name: request.param_converter}
It's late, but according to latest documentation about #ParamConverter, you can achieve it follow way:
* #ParamConverter("users", class="AcmeBlogBundle:User", options={
* "repository_method" = "findUsersByIds"
* })
you just need make sure that repository method can handle comma (,) separated values

Resources