Symfony Invokable controllers called with render_esi - symfony

With Symfony, there is a possibility to create invokable controllers.
For some blocks like header or footer, i call my controller with twig function like this render(controller('App\\Controller\\MyInvokableController')) and it works.
But if i do the same with render_esi(controller('App\\Controller\\MyInvokableController')) i've this following error :
Cannot use object of type App\\Controller\\MyInvokableController as array
Is it possible to use invokable controller with render_esi ?
My controller :
declare(strict_types=1);
namespace App\Controller;
use ScemBundle\Helpers\EsiHelper;
use Symfony\Bridge\Twig\TwigEngine;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Twig\Error\Error;
/**
*
*/
final class MyInvokableController
{
use RepositoriesTrait;
/**
* #param Request $request
* #param EsiHelper $esiHelper
* #param TwigEngine $twig
*
* #return Response
* #throws Exception
*/
public function __invoke(Request $request, EsiHelper $esiHelper, TwigEngine $twig): Response
{
$params = $this->getSomeContent();
$content = $twig->render('#path/to/template.html.twig', $params);
$response = new Response();
$response
->setContent($content)
->setPublic()
->setSharedMaxAge(65536);
return $esiHelper->builEsiHeader($listItems, $response);
}
}

Related

FOSHttpCacheBundle cache invalidation with Symfony built-in reverse proxy doesn't work

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.

Symfony 4 How to autowire strings when making a general class?

I would first like to say that I saw the other questions on here relating to this error I'm having and none solved my problems.
I have the following code for a controller to check an APIkey before sending data from the backend to the frontend.
file1Controller.php
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
class file1Controller extends AbstractController
{
/**
* #Route("/Some/URI", methods={"GET"}) // "/Some/URI" here
* #param Request $request
* #return JsonResponse
*/
public function list(Request $request)
{
if (empty($request->headers->get('api-key'))) {
return new JsonResponse(['error' => 'Please provide an API_key'], 401);
}
if ($request->headers->get('api-key') !== $_ENV['API_KEY']) {
return new JsonResponse(['error' => 'Invalid API key'], 401);
}
return new JsonResponse($this->getDoctrine()->getRepository('App:Something')->findAll()); //Something here
}
}
Which works exactly as intended (tested it with Postman and with my browser) for my simple learning example. I would like to generalize it so that I can use it in other places. Almost everything should stay the same except the parts where there are comments. This is what it becomes when making it general:
General.php
<?php
namespace App;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
class General extends AbstractController
{
private $route;
private $entity;
/**
* General constructor.
* #param String $route
* #param String $entity
*/
function __construct(String $route, String $entity)
{
$this->route = $route;
$this->entity = $entity;
}
/**
* #Route({$this->route}, methods={"GET"})
* #param Request $request
* #return JsonResponse
*/
public function list(Request $request)
{
if (empty($request->headers->get('api-key'))) {
return new JsonResponse(['error' => 'Please provide an API_key'], 401);
}
if ($request->headers->get('api-key') !== $_ENV['API_KEY']) {
return new JsonResponse(['error' => 'Invalid API key'], 401);
}
return new JsonResponse($this->getDoctrine()->getRepository('App:{$this->entity}')->findAll());
}
}
And the file file1Controller.php changes to:
<?php
namespace App\Controller;
use App\General;
use Symfony\Component\HttpFoundation\Request;
class SubscriptionController
{
/**
* #return General
*/
public function AuthenticateAPI()
{
$generalObject = new General("/Some/URI", 'Something');
return $generalObject;
}
}
This new setup gives no compiler errors but of course, do give the following error (when testing it):
Cannot autowire service "App\General": argument "$route" of method "__construct()" is type-hinted "string", you should configure its value explicitly.
I understand that this error occurs because Symfony doesn't know which String to inject. But there must be a way to get around this? Because I can't specify the value explicitly in my case because I'll be making another file file2Controller.php which will be the exact same but with different $route and $entity.

How can I generalize an ApiKeyAuthenticator in Symfony 4?

I have the following code that checks whether the API-key is the correct one before sending data to the front end.
file1Controller.php
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
class file1Controller extends AbstractController
{
/**
* #Route("/Some/URI", methods={"GET"}) // "/Some/URI" here
* #param Request $request
* #return JsonResponse
*/
public function list(Request $request)
{
if (empty($request->headers->get('api-key'))) {
return new JsonResponse(['error' => 'Please provide an API_key'], 401);
}
if ($request->headers->get('api-key') !== $_ENV['API_KEY']) {
return new JsonResponse(['error' => 'Invalid API key'], 401);
}
return new JsonResponse($this->getDoctrine()->getRepository('App:Something')->findAll()); //Something here
}
}
Which works excatly as intended (tested it with Postman) for my simple learning example. I would like to generalize it so that I can use it in other places. Almost everything should stay the same except the parts where there are comments. I have tried the following:
General.php
<?php
namespace App;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
class General extends AbstractController
{
private $request;
private $route;
private $entity;
/**
* ApiKeyAuthenticator constructor.
* #param Request $request
* #param String $route
* #param String $entity
*/
function __construct(Request $request, String $route, String $entity)
{
$this->request = $request;
$this->route = $route;
$this->entity = $entity;
}
/**
* #Route({$route}, methods={"GET"}) //notice here
* #return JsonResponse
*/
public function list()
{
if (empty($this->request->headers->get('api-key'))) {
return new JsonResponse(['error' => 'Please provide an API_key'], 401);
}
if ($this->request->headers->get('api-key') !== $_ENV['API_KEY']) {
return new JsonResponse(['error' => 'Invalid API key'], 401);
}
return new JsonResponse($this->getDoctrine()->getRepository('App:{$this->entity}')->findAll()); //notice here
}
}
Then I change the code of file1Controller.php to:
<?php
namespace App\Controller;
require(__DIR__.'/../General.php'); //note that there's no error accessing the file here
use Symfony\Component\HttpFoundation\Request;
class file1Controller
{
/**
* #param Request $request
*/
public function AuthenticateAPI(Request $request)
{
$AuthenticatorObject = new ApiKeyAuthenticator($request, "/Some/URI", 'Something'); //getting undefiend class
return $AuthenticatorObject;
}
}
This is unfortunately not working when testing it with Postman and I'm getting an undefiend class error on this line $AuthenticatorObject = new ApiKeyAuthenticator($request, "/Some/URI", 'Something'); in file1Controller.php
What did I do wrong and how could I fix it?
You shouldn't call your controllers like this in Symfony:
require(__DIR__.'/../General.php'); //note that there's no error accessing the file here
Please check out defining and accessing controllers as service in Symfony documentation:
How to Define Controllers as Services
How to Forward Requests to another Controller

Symfony Route. Can't set annotations

I can't understand, if i create crud controller with
bin/console make:crud all routes work from controller
like
/**
* #Route("/", name="product_index", methods="GET")
*/
public function index(ProductRepository $productRepository): Response
{
return $this->render('product/index.html.twig', ['products' => $productRepository->findAll()]);
}
.
If i create controller with bin/console make:controller
and define controller with annotation by myself they don't work
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use JMS\Serializer\SerializerBuilder;
use Symfony\Component\HttpFoundation\JsonResponse;
use App\Entity\Product;
use App\Repository\ProductRepository;
use JMS\Serializer\SerializationContext;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
class FirstApiController extends AbstractController
{
/**
*
* #Route("/first_api", name="first_api")
*/
public function index(ProductRepository $productRepository)
{
$data = $productRepository->findAll();
$serializer = SerializerBuilder::create()->build();
# $jsonContent = $serializer->serialize($data, 'json');
$jsonContent = $serializer->serialize($data, 'json', SerializationContext::create()->setGroups(array('details')));
$response = JsonResponse::fromJsonString($jsonContent);
return $response;
}
/*
* #Route("/first_api/send", name="send")
*
*/
public function send()
{
$a = "text";
return $a;
}
}
Why that route doesn't work
#Route("/first_api/send", name="send") ?
In routes.yaml i wrote nothing, just empty file.
I used wrong syntax !I used
/* <-- the error is here
* #Route("/first_api/send", name="send")
*
*/
I need to use
/** <-- i nee two "*"
* #Route("/first_api/send", name="send")
*
*/
public

FOSRestBundle : I can't see my new route

I've created a new controller (testController) which extends FOSRestController. I can't see the route "/test" i've created in this controller when I run symfony command "debug:router". With Postman, I've a 404 error...
I'm sure I forgot something but I don't know what. Here my code :
testController.php
<?php
namespace Project\ApiBundle\Controller;
use FOS\RestBundle\Controller\Annotations;
use FOS\RestBundle\Controller\Annotations as Rest;
use FOS\RestBundle\Controller\Annotations\Delete;
use FOS\RestBundle\Controller\Annotations\Get;
use FOS\RestBundle\Controller\Annotations\Post;
use FOS\RestBundle\Controller\Annotations\Put;
use FOS\RestBundle\Controller\FOSRestController;
use FOS\RestBundle\View\RouteRedirectView;
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use JMS\Serializer\SerializationContext;
/**
* Class testController.
*/
class testController extends FOSRestController
{
/**
* List .
* #ApiDoc(
* resource = true,
* statusCodes = {
* 200 = "Returned when successful"
* }
* )
*
* #Rest\View()
* #Get("/test")
*
* #param Request $request the request object
*
* #return array
*/
public function getTest(Request $request)
{
return "hello world";
}
}
And, here my routing.yml file :
api_test:
type: rest
resource: Project\ApiBundle\Controller\TestController
What I've forgot ?
Thank you very much !
I finally found the answer... so easy but I didn't see it ! The method name :
=> getTestAction

Resources