I am trying to serialise data as JSON with the default Symfony Serializer.
To do that I'm trying to use #Groups() as explained here:
https://symfony.com/doc/current/serializer.html
After adding the #Groups annotation as shown below:
class User implements UserInterface
{
// ...
/**
* #ORM\OneToMany(targetEntity=PortfolioItem::class, mappedBy="user", orphanRemoval=true)
* #ORM\OrderBy({"id" = "DESC"})
* #Groups({"show_user"})
*/
private $portfolioItems;
}
On my controller I have the following:
/**
* #param Request $request
* #return JsonResponse
* #Route("/async/portfolio/brands/get_chart", name="portfolio.brands.chart.data", options={"expose"=true}, methods={"POST", "GET"})
* #IsGranted("ROLE_USER")
*/
public function getDataForBrandsChart(Request $request): JsonResponse
{
$user = $this->getUser();
$portfolioItems = $user->getPortfolioItems();
$output = $this->serializer->serialize($portfolioItems, "json", ["groups" => "show_user"]);
return new JsonResponse($output, 200);
}
This always gives the following output:
[[]]
Why is it always empty?
The reason I am using the Groups is because without them I have the following error:
A circular reference has been detected when serializing the object of class "App\Entity\PortfolioItem" (configured limit: 1).
The problem was cache.
Restarting the server after the extra-bundle composer installation and running bin/console cache:clear solved the issue.
Related
I have a ArgumentValueResolverInterface that creates and validates DTOs.
I have also setup a firewall to protect routes and additionally use IsGranted attribute for fine grained access control.
Problem is that the value resolver and validation runs before the security firewall and show validation errors even if the request is unauthenticated.
How can I change the value resolver to run after security is resolved?
Is this even possible?
class RequestDTOValueResolver implements ArgumentValueResolverInterface
{
/**
* RequestDTOValueResolver constructor.
* #param ValidatorInterface $validator
*/
public function __construct(protected ValidatorInterface $validator)
{}
/**
* #inheritDoc
*/
public function supports(Request $request, ArgumentMetadata $argument): bool
{
return is_subclass_of($argument->getType(), RequestDTOInterface::class);
}
/**
* #inheritDoc
* #throws ValidationException
* #throws Exception
*/
public function resolve(Request $request, ArgumentMetadata $argument): iterable
{
$className = $argument->getType();
/** #var AbstractRequestDTO $dto */
$dto = new $className($request); //$this->parseRequest($request, $argument);
$groups = $dto->getGroups();
$errors = $this->validator->validate($dto, null, !empty($groups) ? $groups : null);
if ($errors->count()) {
throw ValidationException::create($errors, "One or more fields are invalid.");
}
yield $dto;
}
}
According to the official documentation, which is available here (it's not so different across different SF versions) : https://symfony.com/doc/5.2/controller/argument_value_resolver.html
You could probably achieve your goal by setting the proper priority
App\ArgumentResolver\UserValueResolver:
tags:
- { name: controller.argument_value_resolver, priority: 50 }
I would also advice to check in which order each service is being run. Here you can see how it's done by SF:
https://symfony.com/doc/current/introduction/http_fundamentals.html
I'm trying to do a hard thing: implementing cache invalidation with Symfony 4.4.13 using FOSHttpCacheBundle 2.9.0 and built-in Symfony reverse proxy.
Unfortunately, I can't use other caching solution (like Varnish or Nginx) because my hosting service doesn't offer them. So, the Symfony built-in reverse proxy is the only solution I have.
I've installed and configured FOSHttpCacheBundle (following the documentation). Also created a CacheKernel class and modified Kernel to use it (following Symfony official documentation, FOSHttpCache documentation and FOSHttpCacheBundle documentation).
After few tests (with my browser), the HTTP caching works and GET responses are cached (seen in browser network analyzer). But, when I update a resource with PUT/PATCH/POST, the GET responses still come from the cache and are unchanged until the expiration. My deduction is the invalidation doesn't work.
Have I do something wrong? Can you help me to troubleshoot?
See my code and configuration below.
config/packages/fos_http_cache.yaml
fos_http_cache:
cache_control:
rules:
-
match:
path: ^/
headers:
cache_control:
public: true
max_age: 15
s_maxage: 30
etag: "strong"
cache_manager:
enabled: true
invalidation:
enabled: true
proxy_client:
symfony:
tags_header: My-Cache-Tags
tags_method: TAGPURGE
header_length: 1234
purge_method: PURGE
use_kernel_dispatcher: true
src/CacheKernel.php
<?php
namespace App;
use FOS\HttpCache\SymfonyCache\CacheInvalidation;
use FOS\HttpCache\SymfonyCache\CustomTtlListener;
use FOS\HttpCache\SymfonyCache\DebugListener;
use FOS\HttpCache\SymfonyCache\EventDispatchingHttpCache;
use FOS\HttpCache\SymfonyCache\PurgeListener;
use FOS\HttpCache\SymfonyCache\RefreshListener;
use FOS\HttpCache\SymfonyCache\UserContextListener;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\HttpCache\HttpCache;
use Symfony\Component\HttpKernel\HttpCache\Store;
use Symfony\Component\HttpKernel\HttpKernelInterface;
class CacheKernel extends HttpCache implements CacheInvalidation
{
use EventDispatchingHttpCache;
// Overwrite constructor to register event listeners for FOSHttpCache.
public function __construct(HttpKernelInterface $kernel, SurrogateInterface $surrogate = null, array $options = [])
{
parent::__construct($kernel, new Store($kernel->getCacheDir()), $surrogate, $options);
$this->addSubscriber(new CustomTtlListener());
$this->addSubscriber(new PurgeListener());
$this->addSubscriber(new RefreshListener());
$this->addSubscriber(new UserContextListener());
if (isset($options['debug']) && $options['debug'])
$this->addSubscriber(new DebugListener());
}
// Made public to allow event listeners to do refresh operations.
public function fetch(Request $request, $catch = false)
{
return parent::fetch($request, $catch);
}
}
src/Kernel.php
<?php
namespace App;
use FOS\HttpCache\SymfonyCache\HttpCacheAware;
use FOS\HttpCache\SymfonyCache\HttpCacheProvider;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
use Symfony\Component\Routing\RouteCollectionBuilder;
class Kernel extends BaseKernel implements HttpCacheProvider
{
use MicroKernelTrait;
use HttpCacheAware;
private const CONFIG_EXTS = '.{php,xml,yaml,yml}';
public function __construct(string $environment, bool $debug)
{
parent::__construct($environment, $debug);
$this->setHttpCache(new CacheKernel($this));
}
...
public/index.php
<?php
use App\Kernel;
use Symfony\Component\ErrorHandler\Debug;
use Symfony\Component\HttpFoundation\Request;
require dirname(__DIR__).'/config/bootstrap.php';
...
$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);
$kernel = $kernel->getHttpCache();
$request = Request::createFromGlobals();
$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response);
One of mine controller, src/Controller/SectionController.php (NOTE: routes are defined in YAML files)
<?php
namespace App\Controller;
use App\Entity\Section;
use App\Entity\SectionCollection;
use App\Form\SectionType;
use FOS\HttpCacheBundle\Configuration\InvalidateRoute;
use FOS\RestBundle\Controller\AbstractFOSRestController;
use FOS\RestBundle\Controller\Annotations as Rest;
use FOS\RestBundle\View\View;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class SectionController extends AbstractFOSRestController
{
/**
* List all sections.
*
* #Rest\View
* #param Request $request the request object
* #return array
*
* Route: get_sections
*/
public function getSectionsAction(Request $request)
{
return new SectionCollection($this->getDoctrine()->getRepository(Section::class)->findAll());
}
/**
* Get a single section.
*
* #Rest\View
* #param Request $request the request object
* #param int $id the section id
* #return array
* #throws NotFoundHttpException when section not exist
*
* Route: get_section
*/
public function getSectionAction(Request $request, $id)
{
if (!$section = $this->getDoctrine()->getRepository(Section::class)->find($id))
throw $this->createNotFoundException('Section does not exist.');
return array('section' => $section);
}
/**
* Get friends of the section's user.
*
* #Rest\View
* #return array
*
* Route: get_friendlysections
*/
public function getFriendlysectionsAction()
{
return $this->get('security.token_storage')->getToken()->getUser()->getSection()->getMyFriends();
}
private function processForm(Request $request, Section $section)
{
$em = $this->getDoctrine()->getManager();
$statusCode = $em->contains($section) ? Response::HTTP_NO_CONTENT : Response::HTTP_CREATED;
$form = $this->createForm(SectionType::class, $section, array('method' => $request->getMethod()));
// If PATCH method, don't clear missing data.
$form->submit($request->request->get($form->getName()), $request->getMethod() === 'PATCH' ? false : true);
if ($form->isSubmitted() && $form->isValid()) {
$em->persist($section);
$em->flush();
$response = new Response();
$response->setStatusCode($statusCode);
// set the 'Location' header only when creating new resources
if ($statusCode === Response::HTTP_CREATED) {
$response->headers->set('Location',
$this->generateUrl(
'get_section', array('id' => $section->getId()),
true // absolute
)
);
}
return $response;
}
return View::create($form, Response::HTTP_BAD_REQUEST);
}
/**
*
* Creates a new section from the submitted data.
*
* #Rest\View
* #return FormTypeInterface[]
*
* #InvalidateRoute("get_friendlysections")
* #InvalidateRoute("get_sections")
*
* Route: post_section
*/
public function postSectionsAction(Request $request)
{
return $this->processForm($request, new Section());
}
/**
* Update existing section from the submitted data.
*
* #Rest\View
* #param int $id the section id
* #return FormTypeInterface[]
* #throws NotFoundHttpException when section not exist
*
* #InvalidateRoute("get_friendlysections")
* #InvalidateRoute("get_sections")
* #InvalidateRoute("get_section", params={"id" = {"expression"="id"}})")
*
* Route: put_section
*/
public function putSectionsAction(Request $request, $id)
{
if (!$section = $this->getDoctrine()->getRepository(Section::class)->find($id))
throw $this->createNotFoundException('Section does not exist.');
return $this->processForm($request, $section);
}
/**
* Partially update existing section from the submitted data.
*
* #Rest\View
* #param int $id the section id
* #return FormTypeInterface[]
* #throws NotFoundHttpException when section not exist
*
* #InvalidateRoute("get_friendlysections")
* #InvalidateRoute("get_sections")
* #InvalidateRoute("get_section", params={"id" = {"expression"="id"}})")
*
* Route: patch_section
*/
public function patchSectionsAction(Request $request, $id)
{
return $this->putSectionsAction($request, $id);
}
/**
* Remove a section.
*
* #Rest\View(statusCode=204)
* #param int $id the section id
* #return View
*
* #InvalidateRoute("get_friendlysections")
* #InvalidateRoute("get_sections")
* #InvalidateRoute("get_section", params={"id" = {"expression"="id"}})")
*
* Route: delete_section
*/
public function deleteSectionsAction($id)
{
$em = $this->getDoctrine()->getManager();
if ($section = $this->getDoctrine()->getRepository(Section::class)->find($id)) {
$em->remove($section);
$em->flush();
}
}
}
After searching few days, I found the solution by myself.
In CacheKernel, I extend Symfony\Component\HttpKernel\HttpCache\HttpCache as described in FOSHttpCache documentation. But, the class must extend Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache instead as described in Symfony documentation. By consequences, the constructor change too.
To be honest, I don't know the difference between these two classes but you must use the second one if you want to have a built-in functional reverse proxy. It works now for me.
I put here the final code of src/CacheKernel.php:
<?php
namespace App;
use FOS\HttpCache\SymfonyCache\CacheInvalidation;
use FOS\HttpCache\SymfonyCache\CustomTtlListener;
use FOS\HttpCache\SymfonyCache\DebugListener;
use FOS\HttpCache\SymfonyCache\EventDispatchingHttpCache;
use FOS\HttpCache\SymfonyCache\PurgeListener;
use FOS\HttpCache\SymfonyCache\RefreshListener;
use FOS\HttpCache\SymfonyCache\UserContextListener;
use Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\HttpKernelInterface;
class CacheKernel extends HttpCache implements CacheInvalidation
{
use EventDispatchingHttpCache;
/**
* Overwrite constructor to register event listeners for FOSHttpCache.
*/
public function __construct(HttpKernelInterface $kernel)
{
parent::__construct($kernel, $kernel->getCacheDir());
$this->addSubscriber(new CustomTtlListener());
$this->addSubscriber(new PurgeListener());
$this->addSubscriber(new RefreshListener());
$this->addSubscriber(new UserContextListener());
if (isset($options['debug']) && $options['debug'])
$this->addSubscriber(new DebugListener());
}
/**
* Made public to allow event listeners to do refresh operations.
*
* {#inheritDoc}
*/
public function fetch(Request $request, $catch = false)
{
return parent::fetch($request, $catch);
}
}
The rest of the code don't change.
Hope it helps. See you.
I'm using API Platform v2.2.5, and in the process of writing tests for my resources I've discovered that, when null is provided for a field of type string, an error response is being returned during the denormalization process, which includes a non client-friendly message and a stack trace. This is different to if an empty string is provided or the field is omitted completely, which returns a structured validation response. How can I instead return a validation error response as when an empty string is provided?
Entity
class MyEntity
{
/**
* #var string|null
*
* #ORM\Column(type="string", length=255)
*
* #Assert\NotBlank
*
* #Groups({"read", "write"})
*/
private $title;
/**
* #return null|string
*/
public function getTitle(): ?string
{
return $this->title;
}
/**
* #param string $title
* #return WorkoutTemplate
*/
public function setTitle(?string $title): self
{
$this->title = $title;
return $this;
}
}
Resource configuration
App\Entity\MyEntity:
collectionOperations
post:
denormalization_context:
groups:
- write
Error response
Example of validation structure
Figured it out thanks to the guys in the Symfony Slack channel #api-platform.
The Doctrine column definitions are used during the serialization process so to fix the issue, nullable=true was required. Once that was added, the serialization process worked and the null value was caught at the validation level, returning the expected response structure.
Have a problem here:
/**
* Deal controller.
*
* #Route("/portfolio/{portfolio_id}/deal")
*/
class DealController extends Controller
{
// … some code here…
/**
* Creates a new Deal entity.
*
* #Route("/", name="mb_deal_create")
* #Method("POST")
* #Template("MBPortfolioBundle:Deal:new.html.twig")
*/
public function createAction(Request $request)
{
}
So this is my question: how to get $portfolio_id route parameter defined in class annotation from within this createAction?
If I'm trying just add this parameter to the parameter list - it's null then:
public function createAction(Request $request, $portfolio_id) // no way
If I'm trying to get it from query parameter bag - it's null then:
public function createAction(Request $request)
{
$portfolio_id = $request->query->get('portfolio_id'); // no way
So what I need to do?
I see you've already found the solution but it doesn't hurt to put here another way to solve it:
$context = new RequestContext();
$context->fromRequest($request);
$portfolio_id = $context->getParameter('portfolio_id');
Edit
Move portfolio_id to actions' annotation
/**
* Deal controller.
*
*/
class DealController extends Controller
{
// … some code here…
/**
* Creates a new Deal entity.
*
* #Route("/portfolio/{portfolio}/deal", name="mb_deal_create")
* #Method("POST")
* #Template("MBPortfolioBundle:Deal:new.html.twig")
*/
public function createAction(Request $request, Portfolio $portfolio)
{
}
Mine solution is right here:
$portfolio_id = $request->attributes->get('_route_params')['portfolio_id'];
In Symfony2 you can work with multiple entity managers and use something like the code below:
$em = $this->get('doctrine')->getManager();
$em = $this->get('doctrine')->getManager('default');
$customerEm = $this->get('doctrine')->getManager('customer');
We can inject the default manager to any service by using:
"#doctrine.orm.entity_manager"
How can you inject non-default entity managers into services?
If your entity managers config name is non_default then you can reference it as #doctrine.orm.non_default_entity_manager
For those who are using Symfony 3+, use the console :
php bin/console debug:container
Then you should see many lines starting with : 'doctrine.orm.MY_CUSTOM_ENTITY_MANAGER_xxxxxxxxxx'
So if you want the entity manager corresponding to your custom entity manager, find the line :
'doctrine.orm.MY_CUSTOM_ENTITY_MANAGER_entity_manager'
You can insert it in your service arguments.
Hope it helps.
You should define your custom entity manager as a service:
services:
name_of_your_custom_manager:
class: %doctrine.orm.entity_manager.class%
factory_service: doctrine
factory_method: getEntityManager
arguments: ["name_of_your_custom_manager"]
Then, you can inject it in the same way as you do with every service:
#name_of_your_custom_manager
Edit:
Pay attention that factory method may differ between symfony's version (it could be getEntityManager or getManager)
Hello first of all create your manager, in my example I create the manager for my Item class that is in a CoreBundle:
<?php
// src/Sybio/Bundle/CoreBundle/Manager/ItemManager.php:
namespace Sybio\Bundle\CoreBundle\Manager;
use Sybio\Bundle\CoreBundle\Entity\Item;
class ItemManager
{
/**
* #var \Doctrine\ORM\EntityManager $em entity manager
*/
protected $em;
/**
* #var \Doctrine\ORM\EntityRepository $em repository
*/
protected $repository;
/**
* #var string $entityName
*/
protected $entityName;
/**
* Constructor
*
* #param EntityManager $em
* #param string $entityName
*
* #return void
*/
public function __construct(EntityManager $em, $entityName)
{
$this->em = $em;
$this->repository = $em->getRepository($entityName);
$this->entityName = $entityName;
}
/**
* Save a entity object
*
* #param Object $entity
*
* #return Object Entity
*/
public function save($entity)
{
$this->persistAndFlush($entity);
return $entity;
}
/**
* Remove a entity object
*
* #param Object $entity
*
* #return Object Entity
*/
public function remove($entity)
{
return $this->removeAndFlush($entity);
}
/**
* Persist object
*
* #param mixed $entity
*
* #return void
*/
protected function persistAndFlush($entity)
{
$this->em->persist($entity);
$this->em->flush();
}
/**
* Remove object
*
* #param mixed $entity entity to remove
*
* #return void
*/
protected function removeAndFlush($entity)
{
$this->em->remove($entity);
$this->em->flush();
}
/**
* Returns entity repository object
*
* #return EntityRepository
*/
public function getRepository()
{
return $this->repository;
}
/**
* Create a new object
*
* #return mixed
*/
public function createNewObject()
{
return new Item();
}
// Create your own methods to manage the object
}
If the manager structure is shared between multiple manager, you can create a BaseManager extended by all other managers !
Then register it in the services.yml (or xml) file of your bundle:
# src/Sybio/Bundle/CoreBundle/Resources/config/services.yml or xml !:
parameters:
# Managers _________________
sybio.item_manager.entity: SybioCoreBundle:Item
sybio.item_manager.class: Sybio\Bundle\CoreBundle\Manager\ItemManager
services:
# Managers _________________
sybio.item_manager:
class: %sybio.item_manager.class%
arguments: [#doctrine.orm.entity_manager, %sybio.item_manager.entity%]
That's it, you can now use it:
// Controller:
$im = $this->get('sybio.item_manager');
$item = $im->createNewObject();
$im->save($item);
You can then improve your manager, here I give an array of config parameters to my manager:
# src/Sybio/Bundle/CoreBundle/Resources/config/services.yml or xml !:
sybio.item_manager:
class: %sybio.item_manager.class%
arguments: [#doctrine.orm.entity_manager, %sybio.item_manager.entity%, {'item_removed_state': %item_removed_state%, 'item_unpublished_state': %item_unpublished_state%, 'item_published_state': %item_published_state%}]
// src/Sybio/Bundle/CoreBundle/Manager/ItemManager.php:
public function __construct(EntityManager $em, $entityName, $params = array()) {
// ...
$this->params = $params;
}
If you create a BaseManager, you can also create a usefull generic method to initialize an object:
// src/Sybio/Bundle/CoreBundle/Manager/BaseManager.php:
/**
* Create a new object
*
* #return mixed
*/
public function createNewObject()
{
$entityName = explode(":", $this->entityName);
$entityName = "Sybio\Bundle\CoreBundle\Entity\\".$entityName[1];
return new $entityName;
}