How to generate an URL for a controller in Symfony? - symfony

I know how to generate an URL for a route. However now I need to generate an URL for a controller or for a controller with a method. I checked the sourced of UrlGenerator but did not find any relevant information. No information in Symfony docs as well.
The method of the controller has an associate url. This url will be used in controller but I need the generator to be a service.

Basically you need to:
1. Add a route to the controller
<?php
declare(strict_types=1);
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
final class BlogController extends AbstractController
{
/**
* #Route(path="blog", name="blog")
*/
public function __invoke(): Response
{
return $this->render('blog/blog.twig');
}
}
2. Generate url route for a route
See Symfony docs
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
class SomeService
{
/**
* #var UrlGeneratorInterface
*/
private $urlGenerator;
public function __construct(UrlGeneratorInterface $urlGenerator)
{
$this->urlGenerator = $urlGenerator;
}
public function go()
{
// ...
// generate a URL with no route arguments
$signUpPage = $this->urlGenerator->generateUrl('sign_up');
// generate a URL with route arguments
$userProfilePage = $this->urlGenerator->generateUrl('user_profile', [
'username' => $user->getUsername(),
]);
// generated URLs are "absolute paths" by default. Pass a third optional
// argument to generate different URLs (e.g. an "absolute URL")
$signUpPage = $this->urlGenerator->generateUrl('sign_up', [], UrlGeneratorInterface::ABSOLUTE_URL);
// when a route is localized, Symfony uses by default the current request locale
// pass a different '_locale' value if you want to set the locale explicitly
$signUpPageInDutch = $this->urlGenerator->generateUrl('sign_up', ['_locale' => 'nl']);
}
}

So, here is the service. At least an example of how it could implemented. SF4
namespace App\Route;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\RouterInterface;
class UrlGenerator
{
/**
* #var RouteCollection
*/
private $collection;
public function __construct(RouterInterface $router)
{
$this->collection = $router->getRouteCollection();
}
public function generate(string $controllerClass, string $method): string
{
foreach ($this->collection as $item) {
$defaults = $item->getDefaults();
$controllerPath = $defaults['_controller'];
$parts = explode('::', $controllerPath);
if ($parts[0] !== $controllerClass) {
continue;
}
if ($parts[1] !== $method) {
continue;
}
return $item->getPath();
}
throw new \RuntimeException(
'Route for such combination of controller and method is absent'
);
}
}
Poorly tested but working solution.

Related

Routing Symfony : Locale is ignored

Here is the context : I have a multi country website and i have an url which is the same between France and Belgium website but I want them to redirect to different actions.
Here is a simple sample of my controller :
/**
* #Route({
* "fr": "/over-ons",
* "be": "/about-us"
* }, name="about_us")
*/
public function about()
{
die("about");
}
/**
* #Route({
* "fr": "/about-us",
* "be": "/over-ons"
* }, name="about_us_2")
*/
public function about2()
{
die("about 2");
}
Then, i created a LocaleSubscriber (based on https://symfony.com/doc/current/session/locale_sticky_session.html) :
<?php
namespace App\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
class LocaleSubscriber implements EventSubscriberInterface
{
private $defaultLocale;
public function __construct($defaultLocale = 'en')
{
$this->defaultLocale = 'fr';
}
public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
$request->setDefaultLocale($this->defaultLocale);
$request->setLocale($this->defaultLocale);
$request->attributes->set('_locale', $this->defaultLocale);
$routeParams = $request->attributes->get('_route_params');
$routeParams['_locale'] = $this->defaultLocale;
$request->attributes->set('_route_params', $routeParams);
}
public static function getSubscribedEvents()
{
return [
// must be registered before (i.e. with a higher priority than) the default Locale listener
KernelEvents::REQUEST => [['onKernelRequest', 20]],
];
}
}
Then i opened http://localhost/about-us and wanted to see the message "about 2" but i have "about".
So the road "about-us" with locale "fr" shoud be matched with about2 action but it matches with about action.
Do you kown if it is possible that the Router match a route with a specific locale please ?
Thanks for your help !
It won't work like that.
The router will match request to the first matching route. So at that point it has very little to do with locale.
A request comes in with the path of /about-us, and it is matched to the about action with the be locale, because that route is defined first.
If you want to use the same route names for multiple locales you will have to add the locale to the URL. Subdomain, prefix, etc, doesn't really matter.
For example:
fr/about-us
be/about-us
(Of course you don't need to do it one by one, define it as a prefix in YAML)

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 Undefined index

I am creating an application that fetches and search for product name from different sources (DB, XML, JSON, ...)(for this code Im testing only with the DB), my idea was to create an interface for that.
I created the interface ProductRepositoryInterface and the class DoctrineProductRepository then I declared them both as services.
In my controller, I call the search function with the product name as param.
Here is my interface ProductRepositoryInterface :
namespace Tyre\TyreBundle\Repository;
interface ProductRepositoryInterface
{
function search(string $needle);
}
My interface DoctrineProductRepository:
namespace Tyre\TyreBundle\Repository;
class DoctrineProductRepository implements ProductRepositoryInterface
{
public function __constructor(EntityManager $em)
{
$this->em = $em;
}
public function search(string $needle)
{
$repository = $this->em->getRepository('TyreTyreBundle:Products');
$query = $repository->createQueryBuilder('u')
->where("u.name LIKE '%".$needle."%' or u.manufacturer LIKE '%".$needle."%'")
->getQuery();
return $query->getArrayResult();
}
}
My Service.yml
services:
Tyre\TyreBundle\Repository\DoctrineProductRepository:
class: Tyre\TyreBundle\Repository\DoctrineProductRepository
Tyre\TyreBundle\Repository\ProductRepositoryInterface:
class: Tyre\TyreBundle\Repository\ProductRepositoryInterface
and finally my controller :
namespace Tyre\TyreBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Tyre\TyreBundle\Repository\DoctrineProductRepository;
use Tyre\TyreBundle\Repository\ProductRepositoryInterface;
class DefaultController extends Controller
{
public function indexAction()
{
return $this->render('TyreTyreBundle:Default:search.html.twig');
}
public function searchAction(Request $request) {
$repositoryMap = [
'db' => DoctrineProductRepository::class,
];
$serviceName = $repositoryMap[$request->get('db')]; /***This is Line 56 ***/
/** #var ProductRepositoryInterface */
$repository = $this->get($serviceName);
$results = $repository->search($request->get('search_for'));
return $this->render('TyreTyreBundle:Default:detail.html.twig', array('results' => $results));
}
public function detailAction()
{
//forward the user to the search page when he tries to access directly to the detail page
return $this->render('TyreTyreBundle:Default:search.html.twig');
}
}
But I get an error :
EDIT
When I try http://localhost:8000/search?db=db , I get other error (I var_dumped $repositoryMap) :
click to view
Am I missing anything?
The reason for your 'ContextErrorException' is :
$request->get('search_for')
is empty because you are passing nothing in the url for that key. Pass 'search_for' also in addition with 'db' like:
http://localhost:8000/search?db=db&search_for=myvalue

Add test controller and route to test event listener

How can I test event listener in the Symfony bundle?
I plan to test it with client (send request and get response). But I have no controllers in my bundle. Can I add 'special' controllers and routes from functional test and test output from them?
I've found way how to add controllers for test.
First - create new controller class (I created it in %BundleName%/Tests/Controller)
%BundleName%/Tests/Controller/TestController.php
namespace %BundleName%\Tests\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;
class TestController extends Controller
{
public function rootAction()
{
return new Response('This is home page');
}
public function galleryAction($id)
{
return new Response(sprintf('This is gallery %s', $id));
}
}
Then I used this controller in test.
%BundleName%/Tests/Controller/PageControllerTest.php
namespace %BundleName%\Tests\Controller;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\Routing\Route;
class PageControllerTest extends WebTestCase
{
/**
* #var \Symfony\Bundle\FrameworkBundle\Client
*/
private $client;
public function setUp()
{
$this->client = static::createClient();
$this->client->followRedirects(true);
$this->setUpRoutes();
}
public function testFirst()
{
$crawler = $this->client->request('GET', '/gallery/42');
}
protected function setUpRoutes()
{
$container = $this->client->getContainer();
/** #var \Symfony\Bundle\FrameworkBundle\Routing\Router $router */
$router = $container->get('router');
$collection = $router->getRouteCollection();
foreach ($collection->all() as $routeId => $route) {
//Leave some routes if you need...
$collection->remove($routeId);
}
$controllerClassName = '\%BundleName%\Tests\Controller\TestController';
$rootRoute = new Route('/', array('_controller' => sprintf('%s::%s', $controllerClassName, 'rootAction')));
$galleryRoute = new Route('/gallery/{id}', array('_controller' => sprintf('%s::%s', $controllerClassName, 'galleryAction')));
$collection->add('_test_root_route', $rootRoute);
$collection->add('_test_gallery_route', $galleryRoute);
}
}
On each test start setUpRoutes method clears route list and registers new routes. Each route _controller param value is \%BundleName%\Tests\Controller\TestController::nameOfAction'.

Resources