How can I make routing case insensitive in Symfony2? - symfony

Is there any configuration in Symfony2 that allow use of case Insensitive routing?
For example, routes below should be treated as they are the same:
www.exmaple.com/all
www.exmaple.com/ALL
There is a pull request about this, but no reference how to make it happen.

As of Symfony2.4, you can now define condition to your route and use the expression language to do complexe check. See Router: Completely Customized Route Matching with Conditions documentation.
Another solution would be to override the route compiler class to change/extend the php compilation of your route matcher:
contact:
path: /{media}
defaults: { _controller: AcmeDemoBundle:Main:contact, media: organic }
requirements:
media: email|organic|ad
options:
compiler_class: MyVendor\Component\Routing\CaseInsensitiveRouteCompiler
See Symfony\Component\Routing\RouteCompiler class.
Or, as fabpot said in the pull request comment, your could override Request::getPathInfo[1] method to always return a lowercase string (use the setFactory[2] to override default request class).
*1 github.com/symfony/symfony/blob/master/src/Symfony/Component/HttpFoundation/Request.php#L866
*2 github.com/symfony/symfony/blob/master/src/Symfony/Component/HttpFoundation/Request.php#L402

I found a nifty way to do this in pure symfony (no apache mod_rewrite) without having to create a case-insensitive forwarding rule for every route.
This utilizes the twig ExceptionController. Because this occures after the routing has failed to match (or a 404 exception has been thrown for some other reason) it won't break any existing routing urls that use capitals (though that would still be a bad idea).
namespace Application\Symfony\TwigBundle\Controller;
use Symfony\Component\HttpKernel\Exception\FlattenException;
use Symfony\Component\HttpKernel\Log\DebugLoggerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Bundle\TwigBundle\Controller\ExceptionController as BaseExceptionController;
/**
* ExceptionController.
*/
class ExceptionController extends BaseExceptionController
{
/**
* Redirects 404s on urls with uppercase letters to the lowercase verion,
* then uses it's parent class to
* Convert an Exception to a Response.
*
* #param Request $request The request
* #param FlattenException $exception A FlattenException instance
* #param DebugLoggerInterface $logger A DebugLoggerInterface instance
*
* #return Response
*
* #throws \InvalidArgumentException When the exception template does not exist
*/
public function showAction(Request $request, FlattenException $exception, DebugLoggerInterface $logger = null)
{
if ( (string) $exception->getStatusCode() === '404' && preg_match('/[A-Z]/', $request->getPathInfo())) {
$lowercaseUrl = strtolower($request->getPathInfo());
if ($request->isMethod('POST')) {
return new RedirectResponse(
$lowercaseUrl,
'307'//307 status code preserves post information.
);
}
$queryString = $request->getQueryString();
return new RedirectResponse(
$lowercaseUrl.( strlen($queryString)>0 ? '?'.$queryString : '' ),
'301'//301 status code plays nice with search engines
);
}
return parent::showAction($request, $exception, $logger);
}
}
The only other trick is that you need to configure this controller as a service you can inject the correct arguments into the parent class's constructor:
in services.yml
services:
application.custom.exception_controller:
class: Application\Symfony\TwigBundle\Controller\ExceptionController
arguments: [ "#twig", "%kernel.debug%" ]
in config.yml:
twig:
exception_controller: application.custom.exception_controller:showAction
Of course, you can stick this controller and service definition anywhere

As far as I know, this isn't possible with Symfony 2. However, you should be able to accomplish it with Apache's mod_rewrite. See this blog post for details.
Make sure to read the comments, as there are some errors with the initial post.

Related

Call a method on a service created dynamically by a bundle

I am using the m6web_guzzle bundle to register several http clients:
m6web_guzzlehttp:
clients:
myclient:
timeout: 3
headers:
"Accept": "application/json"
delay: 0
verify: false
I want to call a method on a service that it dynamically generates. In this case the generated service name is:
#m6web_guzzlehttp.guzzle.handlerstack.myclient
Here is what I do in my service constructor: (the 3rd parameter injected is '#m6web_guzzlehttp.guzzle.handlerstack.myclient')
/**
* #param array $parameters
* #param Client $client
* #param HandlerStack $handlerStack
*/
public function __construct(array $parameters, Client $client, HandlerStack $handlerStack)
{
$this->parameters = $parameters;
$this->client = $client;
$this->handlerStack->push(Middleware::retry([$this, 'retryDecider']));
}
So far, it works well, but how can I transfer the last line (the push call) in my services.yml file? Or another cleaner method to register this retry handler?
So compiler passes were mentioned before. That is one option.
Use factories to create instances
But you can nearly express this also directly in your services definition. I say nearly, because you will need some kind of code as Symfony service definitions cannot (AFAIK) evaluate to a Closure - which is what we need for the Guzzle Middleware.
I wrote up this services.yml as an example:
m6web_guzzlehttp.guzzle.handlerstack.myclient:
class: GuzzleHttp\HandlerStack
factory: ['GuzzleHttp\HandlerStack', create]
retry_decider:
class: MyBundle\RetryDecider
factory: ['MyBundle\RetryDecider', createInstance]
retry_handler:
class: GuzzleHttp\Middleware
factory: ['GuzzleHttp\Middleware', retry]
arguments:
- '#retry_decider'
handlerstack_pushed:
parent: m6web_guzzlehttp.guzzle.handlerstack.myclient
calls:
- [push, ['#retry_handler']]
What is what?
m6web_guzzlehttp.guzzle.handlerstack.myclient - Your dynamic service - remove from example as you have this already created.
retry_decider - Your decider. We return a Closure in the createInstance method. You can add more parameters if you need, just add arguments to your YML.
retry_handler - Here we compose the middleware using our decider
handlerstack_pushed - Here we push() our handler into the stack, using the dynamic service as a parent service.
Et voilĂ  - we have the stack that the dynamic service defined, but pushed our retry middleware.
Here is the source for our decider:
<?php
namespace MyBundle;
class RetryDecider {
public static function createInstance() {
return function() {
// do your deciding here
};
}
}
--> You now have the service handlerstack_pushed which is the complete Stack.
Configuring more
Please note that you could add m6web_guzzlehttp.guzzle.handlerstack.myclient to parameters.yml:
parameters:
baseHandlerStackService: m6web_guzzlehttp.guzzle.handlerstack.myclient
Then use that on handlerstack_pushed:
handlerstack_pushed:
parent: "%baseHandlerStackService%"
calls:
- [push, ['#retry_handler']]
Just nicer like that ;-)
In your bundle's Extension.php file, you can override the load method and add:
$definition = $container->getDefinition('m6web_guzzlehttp.guzzle.handlerstack.myclient');
$definition->addMethodCall('push', [Middleware::retry([$this, 'retryDecider'])]);
You can write a compiler pass that grabs the definition in question and adds the method call to it.

Symfony Routing "_locale" parameter not accepted

I have a problem with the symfony routing.
For an multilanguage projekt I render a Twig template via a job queue for mailing. In this template is a link to a route that requires the "_locale" parameter with "de" or "en" for example. I use the function "{{ url('route', {'_locale': 'de'}) }}" to generate the url.
By rendering the template, I got the following error message:
[Twig_Error_Runtime]
An exception has been thrown during the rendering of a template ("Some mandatory parameters are missing ("_locale") to generate a URL for route "Route".") in "TemplatePath" at line 5.
Whats my mistake?
Thanks for help
When you are generating URL route, via CLI, the Symfony kernel is missing a HTTP Request, and a Routing->RequestContext.
Thats why, the URL generator cannot find _locale parameter.
To fix it, you must manually create a RequestContext, so, in your command:
$this->getContainer()->get('router')
->setContext(
(new RequestContext())
->setParameter('_locale', 'fr')
);
class BaseCommand extends ContainerAwareCommand
{
protected function getLocale()
{
return $this->getContainer()->get('translator')->getLocale();
}
protected function render($view, $data)
{
return $this->getContainer()->get('templating')->render($view, $data);
}
}
in command
class SomeCommand extends BaseCommand
{
...
$this->render($view, array_merge($data, ['_locale' => $this->getLocale()])
}
in view
{{url('any', {param: 'foo'}|merge(_locale is defined ? {'_locale': _locale } : {}))}}
It's necro time :)
I'm just guessing here, but there's a couple things you can try.
Can you set the locale in your command?
Set the locale for the translator bundle:
$this->getContainer()->get('translator')->setLocale('de');
Set the locale for this session:
$this->getContainer()->get('session')->setLocale('de);
Internationalized routing for Symfony 4.1 and higher
If this applies to you, try this:
url('route.de', {'_locale': 'de'}) }}"
Does your route have 'de' and 'en' set as a requirement?
/**
* Matches /route
* #Route(
* "/route/{_locale}",
* requirements={
* '_locale': 'en|de'
* },
* name="route"
* )
*/
It's an old question but this might help someone out.

Service in symfony2 - how service file should look like?

I am trying to create service in symfony2 which will verify if session contains certain information and if not redirect the user to another controller. I want this piece of code to work as a service as I will be using it in many controllers.
I have problem as manual on Symfony2 book does not provide information how service file should look like. Should it be a normal php class?
Please find below dump of my files with information on error that I receive.
In \AppBundle\Services I create file my_isbookchosencheck.php containing:
<?php
namespace AppBundle\my_isbookchosencheck;
class my_isbookchosencheck
{
public function __construct();
{
$session = new Session();
$session->getFlashBag()->add('msg', 'No book choosen. Redirected to proper form');
if(!$session->get("App_Books_Chosen_Lp")) return new RedirectResponse($this->generateUrl('app_listbooks'));
}
}
My service.yml:
my_isbookchosencheck:
class: AppBundle\Services\my_isbookchosencheck
My conntroller file:
/**
* This code is aimed at checking if the book is choseen and therefore whether any further works may be carried out
*/
$checker = $this->get('my_isbookchosencheck');
Error:
FileLoaderLoadException in FileLoader.php line 125: There is no extension able to load the configuration for "my_isbookchosencheck" (in C:/wamp/www/symfony_learn/app/config\services.yml). Looked for namespace "my_isbookchosencheck", found "framework", "security", "twig", "monolog", "swiftmailer", "assetic", "doctrine", "sensio_framework_extra", "fos_user", "knp_paginator", "genemu_form", "debug", "acme_demo", "web_profiler", "sensio_distribution" in C:/wamp/www/symfony_learn/app/config\services.yml (which is being imported from "C:/wamp/www/symfony_learn/app/config\config.yml").
There are few mistakes that you made, which I am going to explain in short, and I will give you an example of the service you want to create.
You created your service in AppBundle\Services, yet your namespace is registered differently - namespace AppBundle\Services\my_isbookchosencheck;. It should be namespace AppBundle\Services;. I would also advise you to use singular names when creating directories - in this case Service would be better, instead of Services.
You're using your __constructor directly to apply some logic and return the result of it. Better way would be to create a custom method, which could be accessed when necessary.
You're creating new instance of Session which means that you wont be able to access anything that was previously added and stored in session. The right way here, would be to inject RequestStack which holds the current Request and get the session from there.
I believe you also registered your service wrong. In your services.yml file, it should be under services: option. This is why you got the error you pasted.
So, let's see how your service should like.
services.yml
services:
book_service:
class: AppBundle\Service\BookService
arguments:
- #request_stack
- #router
BookService.php
namespace AppBundle\Service;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Routing\RouterInterface;
class BookService {
/* #var $request Request */
private $request;
/* #var $router RouterInterface */
private $router;
public function __construct(RequestStack $requestStack, RouterInterface $router) {
$this->request = $requestStack->getCurrentRequest();
$this->router = $router;
}
public function isBookChoosen() {
$session = $this->request->getSession();
// Now you can access session the proper way.
// If anything was added in session from your controller
// you can access it here as well.
// Apply your logic here and use $this->router->generate()
}
}
Now in your controller you can simply use it like this:
$this->get('book_service')->isBookChoosen()
Well this is a short example, but I hope you got the idea.
try
services:
my_isbookchosencheck:
class: AppBundle\Services\my_isbookchosencheck
in your services.yml, and check that you use the correct namespaces.
Your Class is fine and it should work, however may i suggest that you use
symfony2 session service instead of creating the session object yourself, you can pass it as a constructor argument:
<?php
// namespace edited
namespace AppBundle\Services;
use Symfony\Component\HttpFoundation\Session\Session;
class my_isbookchosencheck
{
public function __construct(Session $session);
{
$session->getFlashBag()->add('msg', 'No book choosen. Redirected to proper form');
if(!$session->get("App_Books_Chosen_Lp")) return new RedirectResponse($this->generateUrl('app_listbooks'));
}
}
and then edit your services.yml accordingly, so the service container will inject the session object:
services:
my_isbookchosencheck:
class: AppBundle\Services\my_isbookchosencheck
arguments: [#session]
Also check out his question on so:
How do you access a users session from a service in Symfony2?
Services are just regular PHP classes, nothing special. But you must register it in order to be recognized by the system. Here are the steps how you do it,
Create a regular PHP class (you can inject other services if it requires)
namespace Acme\DemoBundle\Service;
class MyService
{
private $session;
public function _construct(SessionInterface $session /* here we're injecting the session service which implements the SessionInterface */)
{
$this->session = $session;
}
// other methods go here, which holds the business logic of this class
}
ok, we created a class, we need to register it to be able to use it by service container, here how you do it:
the simplest way is to put it into config.yml file, like this:
services:
my_service:
class: Acme\DemoBundle\Service\MyService
arguments:
- #session
or, another way, is to create a file (e.g. services.yml, may be in config folder), and import it inside the config.yml file (the content of the file is the same as the first way):
imports:
- { resource: services.yml }
or, you can create a services.yml(the content of the file is the same as the first way) file inside you bundle's Resources folder, specify it under the load method of your Extension class (under the DependencyInjection folder), (this way requires some special directory and file structure, read about it in the doc):
class AcmeDemoExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$loader = new YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources'));
$loader->load('services.yml');
}
}
In you case, you're not registering your service, the service container just couldn't find it. Register it by one of the above ways.

Symfony2; domain/host based info in controller and base template

I'm building an symfony2 app that is configurable up to some point based on what domain is used to access the site.
For ease of this question, lets say there is an "Domain" entity in the database containing the hostname and further configuration.
Think about minor template differences, some differences in header/footer. A difference in products being offered.
The routes available would not be different.
There are 2 places where I need this Domain object.
* in a Controller::action
* in a base template (even if the controller didn't need it)
I would not need it somewhere else, if I did, I could simply pass it from the controller.
What would be the best way to get this object without creating too much overhead and not fetching it when we don't actually need it.
Some thoughts I got so far:
* I could override the ControllerResolver and determine the Domain object based on the Request object. Although I don't seem to have access to the ServiceContainer there.
* I could add some method to a BaseController that can retrieve the domain for me when I'm in a Controller:Action.
* For usage in the template I could create a TwigExtension that adds a global variable. But it would need access to the Request object or RequestStack. Also, this would only help me in the template, I might be doing the same thing twice.
Any suggestions what might be a good approach here?
Don't know if this is the best solution, but worked well for me so far.
Since the domain information depends on the request it is NOT a service, so don't try to inject it in services or you'll get a bad headache. The most natural place to set information about the domain is in the request, and allow the controllers to read this information to interact with the services.
So, you can setup a Kernel event listeners which read the information from the database and set a domain Request attribute, like this:
<?php
namespace Acme\SiteBundle\EventListener;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Doctrine\ORM\EntityRepository;
class DomainSubscriber implements EventSubscriberInterface
{
protected $domainRepository;
public static function getSubscribedEvents()
{
return array(
KernelEvents::REQUEST => 'onKernelRequest'
);
}
public function __construct(EntityRepository $domainRepository)
{
$this->domainRepository = $domainRepository;
}
public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
// Console/CLI commands don't have Domain info
if ($request === null)
return;
$domain = $this->domainRepository->find($request->getHost());
if ($domain === null)
throw new \RuntimeException(sprintf("Cannot find domain %s", $request->getHost()));
$request->attributes->set('domain', $domain);
}
}
Which must be registered in services.yml (or XML) with:
acme_site.manager:
class: Doctrine\ORM\EntityManager
factory_service: doctrine
factory_method: getManager
acme_site.domain_repository:
class: Doctrine\ORM\EntityRepository
factory_service: acme_site.manager
factory_method: getRepository
arguments:
- 'AcmeSiteBundle:Domain'
acme_site.domain_subscriber:
class: Acme\SiteBundle\EventListener\DomainSubscriber
arguments:
- "#acme_site.domain_repository"
tags:
- { name: kernel.event_subscriber }
In your Controller you can now access the data by simply doing this:
public function someAction(Request $request) {
$domain = $request->attributes->get('domain');
$domain->getWhatever();
}
And in Twig you can always access the request with this:
{% set domain = app.request.attributes.get('domain') %}
whatever: {{ domain.whatever }}
Hope this help!
DISCLAIMER: the code is copy-pasted and then edited, so it may contain some minor error.
NOTE: If you really need to inject the request in services, then I suggest you to read the docs about the RequestStack (Symfony 2.4+), or use a setRequest method and take care of container scopes.

Symfony 2.1 Doctrine filters (enable/disable)

I'm currently implementing Doctrine filters in my Symfony2.1 project with the following setup:
<?php
namespace Acme\Bundle\Entity;
class Article {
/**
* #ORM\Column(type="string")
*/
private $status;
...
}
//app/config/config.yml
doctrine:
orm:
filters:
status:
class: Acme\Bundle\Filter\StatusFilter
enabled: false
....
//src/Acme/Bundle/Filter/StatusFilter.php
namespace Acme\Bundle\Filter;
use Acme\Bundle\Entity\Status;
class StatusFilter extends SQLFilter {
public function addFilterConstraint(ClassMetadata $target, $alias)
{
$filter =
$target->reflClass->implementsInterface('Acme\Bundle\Entity\Status')?
$alias . '.status = ' . Status::PUBLISHED : '';
return $filter;
}
}
Where Acme\Bundle\Entity\Status is just an interface.
The code is working as expected when the filter is enabled in config.yml.
The problem is that I cannot retrieve all articles for administration!
Is there a way to enable this filter for a certain bundle?
p.s. I know how to enable and disable the filter with the EntityManager,
I just cannot find the proper place to do it for the frontend Bundle.
my admin section is accessible by route prefix myadmin
www.example.com/myadmin/ -> admin section = disable filter (disabled by default in config)
www.example.com/... -> anything else = enable filter.
Looking at the Doctrine code, there are methods to enable and disable filters.
Once you have defined your filter in the config.yml file, you can enable/disable in a controller or service:
// 'status' is the unique name of the filter in the config file
$this->getDoctrine()->getManager()->getFilters()->enable('status');
$this->getDoctrine()->getManager()->getFilters()->disable('status');
Note: this was taken from Symfony 2.3. You would need to test this with previous versions of Symfony/Doctrine.
there is no notion of bundle at Doctrine level. The only way I see would be to detect which controller is used, by parsing its className (reflection, ...) during a kernel.request event, or a kernel.controller event.
Then, if you detect that your controller is in FrontendBundle, just disable/enable your doctrine filter.
If you prefer using routing to detect when to disable/enable, just use kernel.request event. You will have access to all request parameters, via $event->getRequest()->attributes->get('_controller') for example.

Resources