The right way of using a pagination class in Symfony - symfony

I'm trying to use this Symfony bundle:
https://github.com/KnpLabs/KnpPaginatorBundle
In the docs, they use it a controller. So they have easy access to service container or the request object.
But as far as I understand, the Doctrine query should be in a repository, not a controller, right? And I already do have a function returning records. It's just that the pagination service doesn't expect "results" upon instantiating. It wants the query. So I can't return the "results" to the controller, but rather in middle of this function use a paginator.
On the other hand, stuff like playing with services or requests indeed belong to controllers.
So how this should be done? At first I thought about injecting the "knp_paginator" service and the request object into the repository. But I don't think this is the right way.

I'd say that the Request object should not go further down the stack than from the Controller.
Nothing prevents you from injecting the paginator directly into your custom repository, so why not doing that?
your.repository.service.definition:
class: Your\Repository\Class
# for symfony 2.3
factory_service: doctrine
factory_method: getRepository
# for symfony 2.8 and higher
factory: ["#doctrine.orm.entity_manager", getRepository]
arguments:
- YourBundle:YourEntity
calls:
- [setPaginator, ["#knp_paginator"]]
In the repository, you then should have the paginator available for use with the QueryBuilder:
public function setPaginator($paginator)
{
$this->paginator = $paginator;
}
...
$this->paginator->paginate($qb->getQuery(), $page, $limit);
In order to get your $page and $limit variables into the repository, you don't need the Request object. Simply pass them as a parameter to the repository call:
// In your controller
// You can use forms here if you want, but for brevity:
$criteria = $request->get('criteria');
$page = $request->get('page');
$limit = $request->get('limit');
$paginatedResults = $myCustomRepository->fetchPaginatedData($criteria, $page, $limit);
Passing the request object further down the Controller means that you have a leak in your abstractions. It's no concern of your application to know about the Request object. Actually, the request might well come from other sources such as the CLI command. You don't want to be creating a Request object from there because of a wrong level of abstraction.

Assuming that you have a Custom Repository Class, you can have a method in that Repository, which returns a Query or a valid instance of Query Builder and then you call that method from the controller and pass it to the paginate() method.

For example where $qb is returned by the custom repository (not return result but just the querybuilder of it)
$paginator = $this->get('knp_paginator');
$pagination = $paginator->paginate(
$qb->getQuery(),
$request->query->getInt($pageParameterName, 1),
$perPage,
array('pageParameterName' => $pageParameterName)
);

Related

How to inject Symfony Serializer to the controller properly?

I have a problem with injecting Symfony Serializer into the controller.
Here is a working example of what behavior I want to achive:
public function someAction(): Response
{
$goodViews = //here I get an array of Value Objects or DTO's ;
$serializer = new Serializer([new ObjectNormalizer()], [new JsonEncoder()]);
// normalize gives me a proper array for serialization
return $this->json(
$serializer->normalize($goodViews)
);
}
But now I need to change a direct creation of Serializer with dependency injection to controllers constructor or action. Other way, I think, is to create service which will get ObjectNormalizer and JsonEncoder as arguments, then create a Serializer and then normalize arrays of objects in special method and return result. But I can not figure out how to create serializer in service.yml or describe service dependencies properly. Symfony docs also get simple serializer service or create it manually as I did in the code example.
I thought to get serializer service from a service container in the action (with $this->get('serializer')) or inject it into the controllers constructor with NormalizerInterface (I need to normalize arrays of objects mostly), but this injected serializer fell with such an error:
"message": "Cannot normalize attribute \"options\" because the
injected serializer is not a normalizer", "class":
"Symfony\Component\Serializer\Exception\LogicException",
so I suppose, that its not configured in a way I've done with manual Serializer creation.
Our version of Symfony is 3.4 Thanks for your attention.
The decision of my problem is a little bit tricky. ObjectNormalizer was overriden by custom normalizer (with decorate part in the custom service defenition - see https://symfony.com/doc/3.4/service_container/service_decoration.html). Thats why in the framework preconfigured Symfony Serializer I got our custom one, which produced a mistake:
Cannot normalize attribute \"options\" because the injected serializer
is not a normalizer
So I've created a new serializer service with ObjectNormalizer:
new_api.serializer_with_object_normalizer:
class: Symfony\Component\Serializer\Serializer
arguments:
0:
- "#our_custom_serviec_which_override_object_normalizer.inner"
1:
- "#serializer.encoder.json"
public function someAction(SerializerInterface $serializer): Response // Add serializer as an argument
{
$goodViews = //here I get an array of Value Objects or DTO's ;
// normalize gives me a proper array for serialization
return $this->json(
$serializer->normalize($goodViews)
);
}
In services.yml
# *Bundle/Resources/services.yaml
services:
YourNamespace/*Bundle/Controller/YourController:
tags: [ 'controller.service_arguments' ]
Try it and let us know. This should get you going, but there are better ways you can configure your project. Check here for more info on using controllers as services and how to autowire them.
https://symfony.com/doc/3.4/service_container.html
https://symfony.com/doc/3.4/serializer.html

Call a Command from a Controller not work

i have this code to run schema update command from controller i got help from symfony document
i have this code:
namespace AdminBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\KernelInterface;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
class DefaultController extends Controller
{
/**
* #Route("/admin")
*/
public function indexAction(KernelInterface $kernel)
{
$application = new Application($kernal);
$input = new ArrayInput(array(
'command' => 'doctrine:schema:update'
));
$output = new NullOutput();
$application->run($input, $output);
return new Response("");
}
}
it's not work for me i get this error after open this url (http://127.0.0.1:8000/admin):
Controller "AdminBundle\Controller\DefaultController::indexAction()" requires that you provide a value for the "$kernel" argument. Either the argument is nullable and no null value has been provided, no default value has been provided or because there is a non optional argument after this one.
how can i do?
Instead of injecting the KernelInterface $kernel directly into your action (I guess, you're not using it as a declared service), call your container directly for asking the kernel:
public function indexAction()
{
$kernel = $this->get('kernel');
$application = new Application($kernel); // btw: you have an typo in here ($kernal vs $kernel)
$input = new ArrayInput(array(
'command' => 'doctrine:schema:update'
));
$output = new NullOutput();
$application->run($input, $output);
// tip: use "204 No Content" to indicate "It's successful, and empty"
return new Response("", 204);
}
While Michael's answer works, it is not the preferred method in Symfony 3.3, which had several changes to dependency injection. Your code will actually work just fine with some changes to your services configuration.
As the documentation states, the Dependency Injection Container changed in Symfony 3.3, and by default your controllers are registered as services:
# app/config/services.yml
services:
# ...
# controllers are imported separately to make sure they're public
# and have a tag that allows actions to type-hint services
AppBundle\Controller\:
resource: '../../src/AppBundle/Controller'
public: true
tags: ['controller.service_arguments']
This allows you to autowire the kernel through arguments in your controller action method, like you tried. The reason yours isn't working is because your AdminBundle is likely not set up the way your AppBundle is by default in app/config/services.yml. To truly solve the issue in the way that Symfony 3.3 wants, you should add AdminBundle to your services configuration like so:
# app/config/services.yml
services:
# add this below your AppBundle\Controller definition
AdminBundle\Controller\:
resource: '../../src/AdminBundle/Controller'
public: true
tags: ['controller.service_arguments']
With that, you no longer have to call $this->get('kernel');, and your original code will work as you have it, with KernelInterface as a parameter to your action method.
Furthermore, you can extend the new AbstractController instead of the regular Controller, and then calls to $this->get() will not work anymore, which is the way Symfony is going.
So again while Michael's answer will work just fine, I would advise you to implement the answer I've given simply because Symfony 3.3 prefers that method going forward.

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.

Symfony2 - Dynamic Doctrine Database Connections at Runtime

I am looking for a good solution to on-the-fly connection of databases within Symfony utilizing Doctrine for entity management.
The scenario I have is that all inbound users to our service will be visiting *.website.com addresses, like client1.website.com.
We would like to use one Doctrine entity for the Client table to then look up their database credentials based on the URL of their account on the fly.
So far I have found the following topics here on stackoverflow that discuss dynamically changing the database credentials--but no clear workable solutions.
I'd like to propose collaborating to put together a working solution, and I'll put together a blog/tutorial post for other folks looking to modify database connection parameters within Symfony.
Here are some related posts:
Dynamic database connection symfony2
Symfony2, Dynamic DB Connection/Early override of Doctrine Service
Thanks!
If $em is existing entity manager and you want to reuse it's configuration, you can use this:
$conn = array(
'driver' => 'pdo_mysql',
'user' => 'root',
'password' => '',
'dbname' => 'foo'
);
$new = \Doctrine\ORM\EntityManager::create(
$conn,
$em->getConfiguration(),
$em->getEventManager()
);
I needed to do something similar - runtime discovery of an available database server. I did it by overriding the doctrine.dbal.connection_factory.class parameter and substituting my own derivation of the Doctrine bundle's ConnectionFactory class
My services.yml provides the parameter, pointing at my custom class
parameters:
doctrine.dbal.connection_factory.class: Path\To\Class\CustomConnectionFactory
Then fill in your discovery logic in Path\To\Class\CustomConnectionFactory.php
<?php
namespace Path\To\Class;
use Doctrine\Bundle\DoctrineBundle\ConnectionFactory;
use Doctrine\Common\EventManager;
use Doctrine\DBAL\Configuration;
class CustomConnectionFactory extends ConnectionFactory
{
public function createConnection(array $params, Configuration $config = null, EventManager $eventManager = null, array $mappingTypes = array())
{
// Discover and override $params array here.
// A real-world example might obtain them from zookeeper,
// consul or etcd for example. You'll probably want to cache
// anything you obtain from such a service too.
$params['driver'] = 'pdo_mysql';
$params['host'] = '10.1.2.3';
$params['port'] = 3306;
$params['dbname'] = 'foo';
$params['user'] = 'myuser';
$params['password'] = 'mypass';
//continue with regular connection creation using new params
return parent::createConnection($params, $config, $eventManager,$mappingTypes);
}
}
Note also that Symfony 3.2 features the ability to use environment variables in container configurations, and to use their values on-demand (rather than fixing them when the container is compiled). See the blog announcement for more details.

How to use a cookie in routing configuration in Symfony2?

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

Resources