How to use a cookie in routing configuration in Symfony2? - symfony

I have a City parameter stored in a cookie. I would like to include its value as a pattern prefix in my routing configuration like so:
# MyBundle/Resources/config/routing.yml
MyBundle_hotel:
resource: "#MyBundle/Resources/config/routing/hotel.yml"
prefix: /%cityNameFromCookie%/hotel
How can I achieve that?

Give us a use case on how you would want this to work because I don't see the difficulty. Routes are made of parameters that you can specify through the generateUrl function, the url twig function or the path twig function.
In Twig you can do this
{{ path('MyBundle_hotel', {cityNameFromCookie: app.request.cookies.get('cityNameFromCookie')}) }}
In a controller action
$cookieValue = $this->get('request')->cookies->get('cityNameFromCookie');
$url = $this->generateUrl('MyBundle_hotel', array('cityNameFromCookie' => $cookieValue));
Or from any places that have access to the container
$cookieValue = $this->container->get('request')->cookies->get('cityNameFromCookie');
$url = $this->container->get('router')->generate('MyBundle_hotel', array('cityNameFromCookie' => $cookieValue));
In the last example, you will probably want to change how the container is being accessed.
If you are concerned about how complicated it looks like, you can abstract this logic and put it inside a service or extend the router service.
You can find documentation about services and the service container in the Symfony's documentation.
You can also list the services via the command php app/console container:debug and will find the router service and its namespace and from this you can try to figure out how to extend the router service (a very good way to learn how services work).
Otherwise, here is simple way to create a service.
In your services.yml (either in your Bundle or in app/config/config.yml)
services:
city:
class: MyBundle\Service\CityService
arguments: [#router, #request]
In your CityService class
namespace MyBundle\Service
class CityService
{
protected $router;
protected $request;
public function __construct($router, $request)
{
$this->router = $router;
$this->request = $request;
}
public function generateUrl($routeName, $routeParams, $absoluteUrl)
{
$cookieValue = $this->request->cookies->get('cityNameFromCookie');
$routeParams = array_merge($routeParams, array('cityNameFromCookie' => $cookieValue));
return $this->router->generateUrl($routeName, $routeParams, $absoluteUrl);
}
}
Anywhere you have access to the container, you will be able to do the following
$this->container->get('city')->generateUrl('yourroute', $params);
If you still think that it isn't a great solution; you will have to extend the router service (or find a better way to extend the router component to make it behave the way you are expecting it to).
I personally use the method above so I can pass an entity to a path method in Twig. You can find an example in my MainService class and PathExtension Twig class defined in the services.yml.
In Twig, I can do forum_path('routename', ForumEntity) and in a container aware environment I can do $this->container->get('cornichon.forum')->forumPath('routename', ForumEntity).
You should have enough information to make an informed decision

Related

Lazy load services dynamically

After watching the Laravell Nova presentation I wanted to create similar functionality to Lenses in my own app.
I have the following concepts:
Entity: Standard Doctrine Entity
Resource: A class that describes a resource including the target entity and available lenses.
Lens: Has an method apply(Request $request, QueryBuilder $qb) that allow you to modify the QueryBuilder based on the Request.
The goal is to define all Lenses as a service and then somehow assign them to a Resource. This is the problem I'm trying to solve.
Attempt 1: Directly inject the Lenses into the resource
ProjectResource.php
<?php
class ProjectResource
{
protected $lenses = [];
public function __construct(
ProjectRepository $repository,
LensInterface $activeProjectLens,
LensInterface $starredProjectLens
) {
$this->lenses = [
$activeProjectLens,
$starredProjectLens
];
}
public function getLenses() {
return $this->lenses;
}
}
The downside of this is that each Lens service is instantiated and needs to be defined manually
Attempt 2: Inject tagged Lenses into the resource
In my services.yaml tag the services and assign them as an argument to the resource:
App\Lens\ActiveProjectLens:
tags: ['resource.project.lens']
App\Lens\StarredProjectLens:
tags: ['resource.project.lens']
App\Resource\ProjectResource:
arguments:
$lenses: !tagged resource.project.lens
ProjectResource.php
<?php
class ProjectResource
{
protected $lenses = [];
public function __construct(
ProjectRepository $repository,
iterable $lenses
) {
$this->lenses = $lenses;
}
public function getLenses() {
return $this->lenses;
}
}
The downside of this approach is every Lens service and Resource must be tagged and cannot be an auto-configured service.
**Attempt 3: Add a compiler pass **
I attempted to add the process() method to the Kernel but I didn't get too far with that.
My goal is to define a list of services somehow in the Resource and have them injected. Is there any established pattern for this?
Your approach with the tags seems good. Symfony provides a way to automatically add tags to classes that implement a certain interface: Interface-based service configuration.
To use that you have to do the following:
If you don't already have one, create an interface (e.g. App\Lens\LensInterface) and let your lens classes implement the interface.
In your services.yaml file add this config:
services:
// ...
_instanceof:
App\Lens\LensInterface:
tags: ['resource.project.lens']
App\Resource\ProjectResource:
arguments:
$lenses: [!tagged resource.project.lens]
// ...
Then every class implementing your LensInterface would be injected into the ProjectResource without having to explicitly configure every single lens.

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.

generateUrl outside controller

Is there a possibility to use generateUrl() method outside of controllers?
I tried to use it in a custom repository class with $this->get('router'), but it didn't work.
update
I've found a temporary solution here:
http://www.phamviet.net/2012/12/09/symfony-2-inject-service-as-dependency-in-to-repository/
I injected the whole service container into my repository, although it's "not recommended".
But it works for now.
update2
Injecting router instead of the whole container is probably a better idea :)
If you take a look in the source code of Controller::generateUrl(), you see how it's done:
$this->container->get('router')->generate($route, $parameters, $referenceType);
Basically you just enter the name of the route ($route here); if exists, some parameters ($parameters) and the type of reference (one of the constants of the UrlGeneratorInterface)
Don't inject the container into your repository... Really, don't !
If I were you, I would create a service and injects the router in it. In this service, I would create a method, that uses the repository and adds the needed code using the router.
That's way less dirty and easy to use/understand for another developer.
Inject the router itself into your EntityRepsitory (like described on Development Life blog's post Symfony 2: Injecting service as dependency into doctrine repository), then you can use $this->router->generate('acme_route');
in symfony 4 and Sylius when the FormType extends an (ex.) AbstractResourceType
class PostType extends AbstractResourceType
{
private $router;
public function __construct(RouterInterface $router, $dataClass, $validationGroups = [])
{
$this->router = $router;
parent::__construct($dataClass, $validationGroups);
}
}
Services.yaml :
app.post.form.type:
class: App\Form\Admin\Post\PostType
tags:
- { name: form.type }
arguments: ['#router.default', '%app.model.post.class%' ]

Symfony2-How to use access a service from outside of a controller

In my Symfony2 controller, this works fine:
$uploadManager = $this->get('upload.upload_manager');
but when I move it to a custom Listener:
use Doctrine\ORM\Event\LifecycleEventArgs;
use Acme\UploadBundle\Upload\UploadManager;
class PersonChange
{
public function postRemove(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
$entityManager = $args->getEntityManager();
$uploadManager = $this->get('ep_upload.upload_manager');
echo "the upload dir is " . $uploadManager->getUploadDir();
}
}
I get an error:
Fatal error: Call to undefined method Acme\MainBundle\Listener\PersonChange::get() in /home/frank/...
I know I must need a use statement but don't know what to use.
Update: Defining controllers as services is no longer officially recommended in Symfony.
The get() method in the Controller class is just a helper method to get services from the container, and it was meant to get new Symfony2 developers up to speed faster. Once people get comfortable with the framework and dependency injection, it's recommended to define controllers as services and inject each required service explicitly.
Since your PersonChange class is not a controller and doesn't extend the Controller class, you don't have that get() helper method. Instead, you need to define your class as a service and inject needed services explicitly. Read the Service Container chapter for details.
As I ran into the exact same problem maybe I can help
What Elnur said is perfectly fine and I'll just try to pop up a real life example.
In my case I wanted to access
$lucenemanager = $this->get('ivory.lucene.manager')
Even by extending the controller I couldn't get it to work while the controller does access the container (I still did not understand why)
In config.yml my listener (searchindexer.listener) is declared as follow :
services:
searchindexer.listener:
class: ripr\WfBundle\Listener\SearchIndexer
arguments:
luceneSearch: "#ivory_lucene_search"
tags:
- { name: doctrine.event_listener, event: postPersist }
A service (ivory.lucene.search) is passed as argument in my service/listener.
Then in my class
protected $lucenemanager;
public function __construct($luceneSearch)
{
$this->lucenemanager = $luceneSearch;
}
Then you can use the get method against $this
An approach that always works, despite not being the best practice in OO
global $kernel;
$assetsManager = $kernel->getContainer()->get('acme_assets.assets_manager');‏
If you need to access a Service, define it in the class constructor:
class PersonChange{
protected $uploadManager;
public function __construct(UploadManager $uploadManager){
$this->uploadManager = $uploadManager;
}
// Now you can use $this->uploadManager.
}
Now you can pass the Service as argument when calling the class (example 1) or define the clas itself as a Service (recommended, example 2)
Example 1:
use Acme\PersonChange;
class appController{
function buzzAction(){
$uploadManager = $this->get('upload.upload_manager');
$personChange = new PersonChange($uploadManager);
Example 2 (better):
Define PersonChange as a Service itself, and define the other Service as an argument in services.yml file:
# app/config/services.yml
services:
upload.upload_manager:
class: AppBundle\uploadManager
PersonChange:
class: AppBundle\PersonChange
arguments: ['#upload.upload_manager']
In this way, you don't have to bother with the upload_manager service in the Controller, since it's implicitely passed as an argument for the constructor, so your Controller can be:
class appController{
function buzzAction(){
$personChange = $this->get('PersonChange');

Resources