Integrating Symfony Routing and Twig using Symfony Twig Bridge - symfony

I use in my code Twig and Symfony routing which I would like to integrate with Twig using Symfony Twig Bridge.
I have them both installed and what I need to do is to add to Twig extensions Symfony\Bridge\Twig\Extension\RoutingExtension which requires Symfony\Component\Routing\Generator\UrlGenerator.
UrlGenerator requires 2 arguments:
routes collection
request context
So in my yaml services file I have:
router:
class: Symfony\Component\Routing\Router
arguments:
- '#yaml.file.loader'
- '%routing.file%'
- { 'cache_dir' : '%cache.dir%' }
- '#request.context'
twig:
class: Twig_Environment
calls:
- ['addExtension', ['#twig.extensions.debug']]
- ['addExtension', ['#twig.extensions.translate']]
- ['addExtension', ['#twig.extensions.routing']]
arguments:
- '#twig.loader'
- '%twig.options%'
twig.extensions.routing:
class: Symfony\Bridge\Twig\Extension\RoutingExtension
public: false
arguments:
- '#twig.url.generator'
And finally UrlGenerator:
twig.url.generator:
class: Symfony\Component\Routing\Generator\UrlGenerator
public: false
arguments:
- '#router'
- '#request.context'
Unfortunatelly #router is not route collection type. It has method getRouteCollection which allows to get data required by UrlGenerator and it works if I add extension manually eg. from controller. But I don't want to split services definition between different files and prefer to keep them in yaml services definition.
So the question is: how to pass as an argument to UrlGenerator not the raw object Router but result of getRouteCollection?

There are multiple ways to do this:
Using Symfony expression language
If you have Symfony Expression Language component installed, you can do this in your service definition:
twig.url.generator:
class: Symfony\Component\Routing\Generator\UrlGenerator
public: false
arguments:
- "#=service('router').getRouteCollection()"
- "#request.context"
Using factory
If for some reason you don't want to use Symfony Expression Language, you can do it using a factory class which is responsible for instantiating your url generator.
class UrlGeneratorFactory
{
private $router;
private $requestContext;
public function __construct($router, $requestContext)
{
$this->router = $router;
$this->requestContext = $requestContext;
}
public function create()
{
return new UrlGenerator($this->router->getRouteCollection(), $this->requestContext);
}
}
And in your yaml set url generator definition to:
twig.url.generator.factory:
class: UrlGeneratorFactory
arguments: ["#router", "#request.context"]
twig.url.generator:
class: Symfony\Component\Routing\Generator\UrlGenerator
factory: ["#twig.url.generator.factory", create]

Related

Symfony service FileUploader not autowiring

I've followed the Symfony 5.2 tutorial to add a FileUploader as a service (https://symfony.com/doc/current/controller/upload_file.html).
So this is my service.yaml
parameters:
targetDirectory: '%kernel.project_dir%/public/uploads/'
previews_video: '%kernel.project_dir%/public/uploads/previews'
brochures_directory: '%kernel.project_dir%/public/uploads/brochures'
services:
App\Service\FileUploader:
arguments:
$targetDirectory: '%previews_video%'
And this is my FileUploader.php
<?php
namespace App\Service;
use Symfony\Component\HttpFoundation\File\Exception\FileException;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\String\Slugger\SluggerInterface;
class FileUploader
{
private $targetDirectory;
private $slugger;
public function __construct($targetDirectory, SluggerInterface $slugger)
{
$this->targetDirectory = $targetDirectory;
$this->slugger = $slugger;
}
public function upload(UploadedFile $file)
{
$originalFilename = pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME);
$safeFilename = $this->slugger->slug($originalFilename);
$fileName = $safeFilename.'-'.uniqid().'.'.$file->guessExtension();
try {
$file->move($this->getTargetDirectory(), $fileName);
} catch (FileException $e) {
// ... handle exception if something happens during file upload
}
return $fileName;
}
public function getTargetDirectory()
{
return $this->targetDirectory;
}
}
But I'm having this common error :
Cannot resolve argument $fileUploader of "App\Controller\VideoController::edit()": Cannot autowire service "App\Service\FileUploader": argument "$targetDirectory" of method "__construct()" has no type-hint, you should configure its value explicitly.
Called by this controller :
/**
* #Route("/{id}/edit", name="video_edit", methods={"GET","POST"})
* #param Request $request
* #param Video $video
* #param FileUploader $fileUploader
* #return Response
*/
public function edit(Request $request, Video $video, FileUploader $fileUploader): Response
{...}
How do I fix this ? I trying by remove the string type, adding the string type, removing the $ from the targetDirectory parameters in services.yaml... Struggling with that for hours now...
Take a look at my working services.yaml. I've changed the namespace
App\Service
to
App\Services
And I also added the service declaration at the end of the file.
Looks like the order of the lines in services matter. First, I've added the declaration at the top of the services part, but the autowiring is declared after, guess the error was here...
# This file is the entry point to configure your own services.
# Files in the packages/ subdirectory configure your dependencies.
# Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
parameters:
previews_directory: '%kernel.project_dir%/public/uploads/previews'
services:
#i've added my service here at first...
app.menu_builder:
class: App\Menu\MenuBuilder
arguments: ["#knp_menu.factory"]
tags:
- { name: knp_menu.menu_builder, method: createMainMenu, alias: main }
# default configuration for services in *this* file
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
App\:
resource: '../src/'
exclude:
- '../src/DependencyInjection/'
- '../src/Entity/'
- '../src/Kernel.php'
- '../src/Tests/'
# controllers are imported separately to make sure services can be injected
# as action arguments even if you don't extend any base controller class
App\Controller\:
resource: '../src/Controller/'
tags: ['controller.service_arguments']
# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones
App\Services\FileUploader:
arguments:
$targetDirectory: '%previews_directory%'
You should have autowiring configuration added to your services file:
parameters:
targetDirectory: '%kernel.project_dir%/public/uploads/'
previews_video: '%kernel.project_dir%/public/uploads/previews'
brochures_directory: '%kernel.project_dir%/public/uploads/brochures'
services:
# To add:
_defaults:
autowire: true
autoconfigure: true
# You service
App\Service\FileUploader:
arguments:
$targetDirectory: '%previews_video%'
Add a type-hint string for $targetDirectory in the contructor
public function __construct(string $targetDirectory, SluggerInterface $slugger)
{
$this->targetDirectory = $targetDirectory;
$this->slugger = $slugger;
}
I had the same issue.
It was related to the indentation of that specific service. I wasn't getting any indentation error but also the auto wiring wasn't working.
What i did was to add 4 spaces as indentation
App\Service\FileUploader:
arguments:
$targetDirectory: '%TEAM_LOGO_DIRECTORY%'
Yes I got the problem too and I managed to solve it by replacing indentation by spaces in the services.yaml file, I added all of these properties at the same root and then I did that and it works for me:
services:
App\Services\FileUploader: #(added 4 spaces, NOT 1 tab)
arguments: #(added 8 spaces, NOT 2 tabs)
$targetDirectory: '%cats_directory%' #(added 12 spaces, NOT 3 tabs)
If you struggle you (and other people who got this problem) can try this solution. I don't guarantee it will work 100%.

How to use libphonenumber.phone_number_util in Symfony 4

To parse phone number I need to use libphonenumber.phone_number_util in my controller ( Symfony 4) as like as :
$parsed = $this->get('libphonenumber.phone_number_util')->parse($phoneNo);
as we have libphonenumber.phone_number_util in private I wanted to make it public by adding this helper in service as below:
services:
libphonenumber\PhoneNumberUtil:
alias: libphonenumber.phone_number_util
public: true
But this returns Exception and message:
"message": "The \"libphonenumber.phone_number_util\" service or alias has been removed or inlined when the container was compiled. You should either make it public, or stop using the container directly and use dependency injection instead.",
"class": "Symfony\\Component\\DependencyInjection\\Exception\\ServiceNotFoundException",
If you are using this in a controller method (which I presume you do based on $this->get(...)), you need to
1) Declare your controller as a service and tag it with controller.service_arguments tag
2) Make sure your util service id matches the class name (I suppose it does already). You don't need it to be public - that's and ancient approach
3) Require the util as a parameter to your controller's action method.
E.g.
services:
libphonenumber\PhoneNumberUtil:
alias: libphonenumber.phone_number_util
AppBundle\Controller\MyController:
tags: ['controller.service_arguments']
and
public function validatePhoneAction(Request $request, PhoneNumberUtil $phoneNumberUtil)
{
...
$phoneNumberUtil->parse($request->request->get('phone_number');
...
}
There is a nice Symfony blog post about these changes in dependency management: https://symfony.com/blog/new-in-symfony-3-4-services-are-private-by-default

monolog.logger.db service has been removed

I'm trying to refactor some Symfony 3 code to Symfony 4.
I am getting the following error when attempting to log:
The "monolog.logger.db" service or alias has been removed or inlined
when the container was compiled. You should either make it public, or
stop using the conta iner directly and use dependency injection
instead.
My logging code:
$logger = $container->get('monolog.logger.db');
$logger->info('Import command triggered');
Monolog config:
monolog:
channels: ['db']
handlers:
db:
channels: ['db']
type: service
id: app.monolog.db_handler
app.monolog.db_handler config (Note, I tried public: true here and it had no affect:
app.monolog.db_handler:
class: App\Util\MonologDBHandler
arguments: ['#doctrine.orm.entity_manager']
How can I get this wired up correctly in Symfony 4?
By default all services in Symfony 4 are private (and is the recommended pratice) so you need to "inject" in each Controller each needed service (personally I use a custom CommonControllerServiceClass).
You can also create a public service "alias" to continue accessing the service as you did, but it's not the best pratice to follow (also because I guess you will have many other services to fix).
mylogger.db:
alias: monolog.logger.db
public: true
then you can get the service from the container:
$logger = $container->get('mylogger.db');
Alister's answer is a good start, but you can utilise service arguments binding instead of creating a new service for each logger:
services:
_defaults:
autowire: true
bind:
$databaseLogger: '#monolog.logger.db'
Then just change the argument name in your class:
// in App\Util\MonologDBHandler.php
use Psr\Log\LoggerInterface;
public function __construct(LoggerInterface $databaseLogger = null) {...}
It appears that App\Util\MonologDBHandler may be the only thing that is actively using monolog.logger.db - via a container->get('...') call. (If not, you will want to use this technique to tag the specific sort of logger into more services).
You would be better to allow the framework to build the app.monolog.db_handler service itself, and use the container to help to build it. Normally, to inject a logger service, you will just need to type-hint it:
// in App\Util\MonologDBHandler.php
use Psr\Log\LoggerInterface;
public function __construct(LoggerInterface $logger = null) {...}
However, that will, by default, setup with the default #logger, so you need to add an extra hint in the service definition of the handler that you want a different type of logger:
services:
App\Log\CustomLogger:
arguments: ['#logger']
tags:
- { name: monolog.logger, channel: db }
Now, the logger in CustomLogger should be what you had previously known as monolog.logger.db.
You can also alias a different interface (similar to how the LoggerInterface is aliased to inject '#logger') to the allow for the tagging.

Injecting service with JMS\DiExtraBundle

I have service PgHistService in subdirectory Service in DbExtensionBundle:
namespace Iba\DbExtensionBundle\Service;
class PgHistService { ...}
This service is defined in bundles's services.yml and can be sucessfully included in a controller via $this->get('pghist.service'):
parameters:
pghist.service.class: Iba\DbExtensionBundle\Service\PgHistService
services:
pghist.service:
class: %pghist.service.class%
arguments:
entityManager: "#doctrine.orm.entity_manager"
Now I want to inject it with JMS\DIExtraBundle in doctrine entity listener:
namespace Iba\DbExtensionBundle\Entity;
use JMS\DiExtraBundle\Annotation as DI;
class BaseEntityListener {
/** #DI\Inject("pghist.service") */
public $pgHist;
}
Variable pgHist is always null. What am I doing wrong, please? I tried to set this in config.yml but it doesn't work either:
jms_di_extra:
locations:
all_bundles: false
bundles: [DbExtensionBundle]
directories: ["%kernel.root_dir%/../vendor/iba/db-extension-bundle/Iba/DbExtensionBundle/Service"]
Jason Roman is right, thank you.
If you want to use JMS\DiExtraBunde together with entity listener, you have to use DIExtraBundle own system of invoking listener via annotation #DoctrineListener in listener instead of Doctrine standard one #EntityListeners in the entity.

Symfony 2: Creating a service from a Repository

I'm learning Symfony and I've been trying to create a service, using a repository.
I've created my repositories and entities from generate:entity, so they should be fine.
So far what I got in my services.yml is:
parameters:
mytest.entity: TestTestBundle:Brand
mytest.class: Test\TestBundle\Entity\Brand
default_repository.class: Doctrine\ORM\EntityRepository
services:
myservice:
class: %default_repository.class%
factory-service: doctrine.orm.default_entity_manager
factory-method: getRepository
arguments:
- %mytest.entity%
But when I try to call the service, I get this error:
Catchable Fatal Error: Argument 2 passed to Doctrine\ORM\EntityRepository::__construct() must be an instance of Doctrine\ORM\Mapping\ClassMetadata, none given, called in
Then I tried to create the service just using an entity. My services.yml would look like:
services:
myservice:
class: %mytest.class%
factory-service: doctrine.orm.default_entity_manager
factory-method: getRepository
arguments:
- %mytest.entity%
But for this, I get:
Error: Call to undefined method
Test\TestBundle\Entity\Brand::findAll
Does anybody know what am I doing wrong?
Thanks
DEPRECATION WARNING: No more factory_service and factory_method. This is how you should do it since Symfony 2.6 (for Symfony 3.3+ check below):
parameters:
entity.my_entity: "AppBundle:MyEntity"
services:
my_entity_repository:
class: AppBundle\Repository\MyEntityRepository
factory: ["#doctrine", getRepository]
arguments:
- %entity.my_entity%
The new setFactory() method was introduced in Symfony 2.6. Refer to older versions for the syntax for factories prior to 2.6.
http://symfony.com/doc/2.7/service_container/factories.html
EDIT: Looks like they keep changing this, so since Symfony 3.3 there's a new syntax:
# app/config/services.yml
services:
# ...
AppBundle\Email\NewsletterManager:
# call the static method
factory: ['AppBundle\Email\NewsletterManagerStaticFactory', createNewsletterManager]
Check it out: http://symfony.com/doc/3.3/service_container/factories.html
Here is how we did it in KnpRadBundle: https://github.com/KnpLabs/KnpRadBundle/blob/develop/DependencyInjection/Definition/DoctrineRepositoryFactory.php#L9
Finally it should be:
my_service:
class: Doctrine\Common\Persistence\ObjectRepository
factory_service: doctrine # this is an instance of Registry
factory_method: getRepository
arguments: [ %mytest.entity% ]
UPDATE
Since 2.4, doctrine allows to override the default repositor factory.
Here is a possible way to implement it in symfony: https://gist.github.com/docteurklein/9778800
You may have used the wrong YAML-Keys. Your first configuration works fine for me using
factory_service instead of factory-service
factory_method instead of factory-method
Since 2017 and Symfony 3.3+ this is now much easier.
Note: Try to avoid generic commands like generate:entity. They are desined for begginers to make project work fast. They tend to bare bad practises and take very long time to change.
Check my post How to use Repository with Doctrine as Service in Symfony for more general description.
To your code:
1. Update your config registration to use PSR-4 based autoregistration
# app/config/services.yml
services:
_defaults:
autowire: true
Test\TestBundle\:
resource: ../../src/Test/TestBundle
2. Composition over Inheritance - Create own repository without direct dependency on Doctrine
<?php
namespace Test\TestBundle\Repository;
use Doctrine\ORM\EntityManagerInterface;
class BrandRepository
{
private $repository;
public function __construct(EntityManagerInterface $entityManager)
{
$this->repository = $entityManager->getRepository(Brand::class);
}
public function findAll()
{
return $this->repository->findAll();
}
}
3. Use in any Service or Controller via constructor injection
use Test\TestBundle\Repository\BrandRepository;
class MyController
{
/**
* #var BrandRepository
*/
private $brandRepository;
public function __construct(BrandRepository $brandRepository)
{
$this->brandRepository = $brandRepository;
}
public function someAction()
{
$allBrands = $this->brandRepository->findAll();
// ...
}
}
I convert service.yml to service.xml, and update DependencyInjection Extension, everything is working for me. I don't know why, but yml config will thrown Catchable Fatal Error. You can try using xml config for service config.
service.yml:
services:
acme.demo.apikey_userprovider:
class: Acme\DemoBundle\Entity\UserinfoRepository
factory-service: doctrine.orm.entity_manager
factory-method: getRepository
arguments: [ AcmeDemoBundle:Userinfo ]
acme.demo.apikey_authenticator:
class: Acme\DemoBundle\Security\ApiKeyAuthenticator
arguments: [ "#acme.demo.apikey_userprovider" ]
service.xml:
<services>
<service id="acme.demo.apikey_userprovider" class="Acme\DemoBundle\Entity\UserinfoRepository" factory-service="doctrine.orm.entity_manager" factory-method="getRepository">
<argument>AcmeDemoBundle:Userinfo</argument>
</service>
<service id="acme.demo.apikey_authenticator" class="Acme\DemoBundle\Security\ApiKeyAuthenticator">
<argument type="service" id="acme.demo.apikey_userprovider" />
</service>
</services>
Symfony 3.3 and doctrine-bundle 1.8 there is a Doctrine\Bundle\DoctrineBundle\Repository\ContainerRepositoryFactory
which helps to create repository as service.
Example
What we want
$rep = $kernel->getContainer()
->get('doctrine.orm.entity_manager')
->getRepository(Brand::class);
ORM description
# Brand.orm.yaml
...
repositoryClass: App\Repository\BrandRepository
...
Service description
# service.yaml
App\Repository\BrandRepository:
arguments:
- '#doctrine.orm.entity_manager'
- '#=service("doctrine.orm.entity_manager").getClassMetadata("App\\Entity\\Brand")'
tags:
- { name: doctrine.repository_service }
calls:
- method: setDefaultLocale
arguments:
- '%kernel.default_locale%'
- method: setRequestStack
arguments:
- '#request_stack'
sf 2.6+
parameters:
mytest.entity: TestTestBundle:Brand
mytest.class: Test\TestBundle\Entity\Brand
default_repository.class: Doctrine\ORM\EntityRepository
services:
myservice:
class: %default_repository.class%
factory: ["#doctrine.orm.default_entity_manager", "getRepository"]
arguments:
- %mytest.entity%

Resources