Symfony4 use external class library as a service - php

I have a little external library that expose many classes.
Into my symfony4 project I would like to declare my class from vendor, as a service with autowire and public.
So I have include my library with composer and add psr configuration like this into composer.json:
"autoload": {
"psr-4": {
"App\\": "src/",
"ExternalLibrary\\": "vendor/external-library/api/src/"
}
}
After that I have tried to change my services.yaml into symfony like this:
ExternalLibrary\:
resource: '../vendor/external-library/api/src/*'
public: true
autowire: true
If I launch tests or run the application returns me this error:
Cannot autowire service "App\Domain\Service\MyService": argument "$repository" of method "__construct()" references interface "ExternalLibrary\Domain\Model\Repository" but no such service exists. You should maybe alias this interface to the existing "App\Infrastructure\Domain\Model\MysqlRepository" service.
If I declare into services.yaml the interface this works fine:
ExternalLibrary\Domain\Model\Lotto\Repository:
class: '../vendor/external-library/api/src/Domain/Model/Repository.php'
public: true
autowire: true
But I have many classes and I don't want to declare each class, how can I fix services.yaml without declare every single service?
Thanks

You need to create services by hand:
I did not test it but it should look like this
services.yaml
Some\Vendor\:
resource: '../vendor/external-library/api/src/*'
public: true # should be false
Some\Vendor\FooInterface:
alias: Some\Vendor\Foo # Interface implementation
Some\Vendor\Bar:
class: Some\Vendor\Bar
autowire: true
php
<?php
namespace Some\Vendor;
class Foo implements FooInterface
{
}
class Bar
{
public function __construct(FooInterface $foo)
{
}
}
To be more precise you should have something like
ExternalLibrary\Domain\Model\Repository:
alias: App\Infrastructure\Domain\Model\MysqlRepository

Let's take Dompdf as an example :
When you try to add type-hint Dompdf in your action controller or service method , an error will be occurred saying that auto-wiring isn't possible because Dompdf is an external PHP library
So to solve this problem we'll make a little change in our services.yaml file by adding this short config
Dompdf\: #Add the global namespace
resource: '../vendor/dompdf/dompdf/src/*' #Where can we find your external lib ?
autowire: true #Turn autowire to true
Apply the above example to all external PHP libs :)
That's all !

I had the same problem and someone gave me this solution, which works fine for me:
Use an external repository with symfony4 trouble with autoload and parameters
I copy the other solution by user #DasBen here just in case:
I think that you don't have to import each service separately. Your are already doing that with the "Puc\SapClient" part.
The problem could be that you are importing your models, which should not be imported.
In the symfony example project there is this part vor "services.yaml":
# 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/{Bundle,DependencyInjection,Entity,Model,Migrations,Tests,Kernel.php}'
Then your part would be:
# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
Puc\SapClient\:
resource: '../vendor/puc/sap-client/src/*'
exclude: ''../vendor/puc/sap-client/src/{Entity,Model,"etc."}'
"etc." Would be everything that is not needed as service.

Related

How to fix '...SomeController has no container set' on Controllers defined in Symfony 5 bundle?

I have created a custom Symfony 5.3 bundle to share code between different projects. In src/controller/SomeController.php the bundle implements a controller class which extends from Symfony\Bundle\FrameworkBundle\Controller\AbstractController.
When accessing this controller via a route in my Symfony project I get the following error:
"XY\CommensBundle\Controller\SomeController" has no container set, did
you forget to define it as a service subscriber?
AbstractController has a setContainer method which is used to inject the service container. On controllers implemented directly in my Symfony project this method is called automatically by autowire / autoconfigure.
However, regarding to the Symfony docs autowire / autoconfigure should not be used for bundle services. Instead, all services should be defined explicitly. So I added this to the bundles services.yaml:
# config/services.yaml
services:
xy_commons.controller.some_controller:
class: XY\CommensBundle\Controller\SomeController
public: false
calls:
- [ setContainer, [ '#service_container' ]]
After adding the bundle to my Symfony project using Composer the console shows, that the controller is correctly added as a service. Everything seems fine.
php bin/console debug:container 'xy_commons.controller.some_controller'
Information for Service "xy_commons.controller.some_controller"
=============================================================
---------------- -------------------------------------------------------
Option Value
---------------- -------------------------------------------------------
Service ID xy_commons.controller.some_controller
Class XY\CommensBundle\Controller\SomeController
Tags -
Calls setContainer
Public no
Synthetic no
Lazy no
Shared yes
Abstract no
Autowired no
Autoconfigured no
---------------- -------------------------------------------------------
However, the error is still the same. So how to configure controllers / services in Bundles correctly?
EDIT:
SomeController is just a very basic subclass of AbstractController:
class SomeController extends AbstractController {
public function some(): Response {
return new Response("<html><body>OK</body></html>");
}
}
Fully-qualified class name as Service ID
Usually I use FQCNs as Service ID. However, in this case I followed the advise from the Symfony docs (linked above) which explicitly say not to do so:
If the bundle defines services, they must be prefixed with the bundle
alias instead of using fully qualified class names like you do in your
project services. For example, AcmeBlogBundle services must be
prefixed with acme_blog. The reason is that bundles shouldn’t rely on
features such as service autowiring or autoconfiguration to not impose
an overhead when compiling application services.
In addition, services not meant to be used by the application
directly, should be defined as private.
This is way I used a snake name as ID and made the service private. The controller is not really used as service but only created automatically by Symfony when accessing / calling it via a route.
I recently struggle with the same problem. This is the solution.
Suppose we have the Vendor/FooBundle bundle.
Controller with route annotations
<?php
namespace Vendor\FooBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
class EntryCategoryController extends AbstractController
{
/**
* #Route("/")
*/
public function list()
{
$entryCategories = [];
return $this->render('#VendorFoo/entry_category/list.html.twig', [
'entryCategories' => $entryCategories
]);
}
}
Register controller as service. The service id must be the controller fully-qualified class name, not a snake case string like vendor_foo.controller.entry_category. This is the key thing! Eventually you can create an alias for the controller service, but this is redundant.
# Resources/config/services.yaml
services:
# The service id must be the controller fully-qualified class name.
# If it a snake case string like `vendor_foo.controller.entry_category`,
# then an alias must be created.
# Vendor\FooBundle\Controller\EntryCategoryController:
# alias: '#vendor_foo.controller.entry_category'
# public: false
Vendor\FooBundle\Controller\EntryCategoryController:
# Setting class is redundant, but adds autocompletions for the IDE.
class: Vendor\FooBundle\Controller\EntryCategoryController
arguments:
# Add this tag to inject services into controller actions.
tags: ['controller.service_arguments']
# Call the setContainer method to get access to the services via
# $this->get() method.
calls:
- ['setContainer', ['#service_container']]
Routing file.
# Resources/config/routing.yaml
vendorfoo_entrycategories:
resource: '#VendorFooBundle/Controller/EntryCategoryController.php'
type: annotation
prefix: /entry-categories
Import routing file in your app
# config/routes.yaml
vendor_foo:
resource: '#VendorFooBundle/Resources/config/routing.yml'
prefix: /foo
That's all.
So #ArturDoruch has the correct main points. You should use the fully qualified class name as the service id though I suppose you could use snake case as long as you also used it in your routes file. But there is no particular reason not to use the class name.
Your controller service also needs to be public so the controller resolver can pull it from the DI container. If the service is not public then the resolver just tries to new the controller and will never call set container or inject any constructor args. That is why you get the error about no container set. By the way, a magical side effect of tagging the service with controller.service_arguments is that the service becomes public.
The thing is that using controllers in bundles is just not something you see much anymore mostly because it's a pain if application want to slightly tweak the way controller works.
So if you look in the best practices you see things like bundle controllers should not extend AbstractConroller and bundles should not use autowire. Good advice for most bundles but if you just want to get stuff working then it's easy to get bogged down.
I would suggest starting with the same services.yaml file that comes with the application. You need to tweak the paths slightly:
# Resources/config/services.yaml
services:
_defaults:
autowire: true
autoconfigure: true
MyBundle\:
resource: '../../'
exclude:
- '../../Resources/'
- '../../DependencyInjection/'
- '../../Entity/'
# bin/console debug:container MyController
Service ID MyBundle\Controller\MyController
Class MyBundle\Controller\MyController
Tags controller.service_arguments
container.service_subscriber
Calls setContainer
Public yes
Synthetic no
Lazy no
Shared yes
Abstract no
Autowired yes
Autoconfigured yes
Now you can focus on just getting your bundle to work. Let autowire do the heavy lifting. Once the bundle has stabilized then maybe you can go back in and start to manually define your services as recommended in the best practices. Or maybe not.

Can't declare class symfony 3

i got a little error trying to implement a MaintenanceListener service, who will display a maintenance page
Here's my services.yml
# https://symfony.com/doc/current/service_container.html
services:
# default configuration for services in *this* file
_defaults:
# automatically injects dependencies in your services
autowire: true
# automatically registers your services as commands, event subscribers, etc.
autoconfigure: true
# this means you cannot fetch services directly from the container via $container->get()
# if you need to do this, you can override this setting on individual services
public: false
# makes classes in Cocorico\CoreBundle\DataFixtures\ORM\ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
# Cocorico\CoreBundle\DataFixtures\ORM\:
# resource: '../../src/Cocorico/CoreBundle/DataFixtures/ORM/*'
# makes classes in src/AppBundle available to be used as services
# this creates a service per class whose id is the fully-qualified class name
AppBundle\:
resource: '../../src/AppBundle/*'
# you can exclude directories or files
# but if a service is unused, it's removed anyway
exclude: '../../src/AppBundle/{Entity,Repository,Tests,Event}'
# 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']
# add more services, or override services that need manual wiring
# AppBundle\Service\ExampleService:
# arguments:
# $someArgument: 'some_value'
maintenance_listener:
class: AppBundle\Event\MaintenanceListener
arguments:
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }```
Here's my class:
<?php
namespace MListener;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpFoundation\Response;
class MListener
{
public function onKernelRequest(GetResponseEvent $event)
{
$event->setResponse(new Response('Iziparty is in maintenance mode', Response::HTTP_SERVICE_UNAVAILABLE));
$event->stopPropagation();
}
}
and here's the error i get:
FastCGI sent in stderr: "PHP message: PHP Fatal error: Cannot declare class MListener\MListener, because the name is already in use in /var/www/Symfony/src/AppBundle/Event/MaintenanceListener.php on line 9" while reading response header from upstream
Thanks for the help.
it all comes down to autoload magic. The autoload magic assumes a certain directory structure, that is (among other things) defined in composer.json. It essentially says:
namespace AppBundle\... is in directory src/AppBundle/...
and every class AppBundle\Something\Else is therefore located in src/AppBundle/Something/Else.php
now, symfony starts to load the service that is supposed to handle an event (due to your configuration) AppBundle\Event\MaintenanceListener, which it tries to instantiate, which leads to the auto loader loading the file src/AppBundle/Event/MaintenanceListener.php which only contains the class MListener/MListener.
Since auto-loading is a bit hacky, usually, it will try other approaches / definitions and possibly try to read that file again and it'll then fail to re-declare the MListener/MListener class, since it already exists.
Just to be explicit about this: These approaches work very well if standards are followed (specifically PSR-4 in this case), which bind the directory structure to namespace structure. If you put something in a file, that - according to PSR-4 - doesn't belong there, you gonna get problems, like the one you got.
The fix is easy and obvious: namespace is the directory (with backslash instead of whatever directory separator your OS has), filename is the classname (without .php obviously). So either, rename your file to src/MListener/MListener.php and adapt the services.yaml accordingly: MListener\Mlistener: ... or you rename your namespace and class in that file to AppBundle\Event and MaintenanceListener respectively.
Be sure to create the file src/Event/MaintenanceListener.php
<?php
namespace AppBundle\Event;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpFoundation\Response;
class MaintenanceListener
{
public function onKernelRequest(GetResponseEvent $event)
{
$event->setResponse(new Response('Iziparty is in maintenance mode', Response::HTTP_SERVICE_UNAVAILABLE));
$event->stopPropagation();
}
}
With same name declared in the file service.yaml

How can I override autowired symfony service?

I'm working on a Symfony 4 projet and I have read Symfony documentation about autowiring
My problem is not really about autowiring but importted files.
This Symfony documentation says that you can import your services in many files.
In my project, I have the service.yaml in the config folder which import all services in services folder
imports:
- { resource: './services/admin.yaml'}
- { resource: './services/front.yaml'}
- { resource: './services/core.yaml' }
services:
_defaults:
autowire: true
autoconfigure: true
So i need to override a service autowiring because it has two strings in constructor and I need to wire those arguments manually
extranet.form.data_subscriber.remove_empty_reference_fields:
class: Extranet\Admin\Form\DataSubscriber\RemoveEmptyFieldsSubscriber
arguments:
$collectionName: references
$childName: name
Extranet\Admin\Form\DataSubscriber\RemoveEmptyFieldsSubscriber: '#extranet.form.data_subscriber.remove_empty_reference_fields'
Extranet\Admin\Form\DataSubscriber\RemoveEmptyFieldsSubscriberInterface: '#Extranet\Admin\Form\DataSubscriber\RemoveEmptyFieldsSubscriber'
But now if I put this service directly in my services.yaml at the end of file it works.
My question is, how I can override autowiring with services not configured directly in the services.yaml
Let me know if you can help me, thanks
EDIT
I already tried to put the imports part at the end of services.yaml file

Environment specific services config in Symfony 3.4

I need to access some private services via the container while in the dev environment.
I thought it would be simple - I'd just make them public specifically for dev like so:
app/config/services.yml:
services:
_defaults:
autowire: true
autoconfigure: true
public: false
MyBundle\:
resource: '../../*'
exclude: '../../{Entity,Repository,DoctrineMigrations,DependencyInjection,Tests,Util}'
app/config/services_dev.yml:
imports:
- { resource: services.yml }
services:
_defaults:
autowire: true
autoconfigure: true
public: true
app/config/config_dev.yml
imports:
- { resource: config.yml }
- { resource: services_dev.yml }
This however does not yield the desired result: services behave as if private, config is not merged at all(e.g. when you have parameter injection, sy yells that I must define them explicitly, even if the parent config already does).
There's plenty of examples, in the default config of Sy, where specific parameters get overridden for various environments, but for some reason this does not seem to be the case for services. Is the services config processed in a special way somehow? Have I misunderstood something? What's going on here?
P.S. I know about dependency injection, I know accessing the container is considered bad practice, that's not the point of the question. Please do not suggest that.
It does not work, because your defaults only work on the services defined in your services_dev.yaml. That does not include the imports.
You could try a services_dev.yaml like this:
services:
_defaults:
autowire: true
autoconfigure: true
public: true
MyBundle\Services\MyPublicService: ~
This will overwrite the service and make it public. Obviously you have to replace the class with the actual service.
You do not need to import the other services. The kernel will take care of loading this class.

Symfony 4 _instanceof in Bundle's services.yaml

I have a bundle which has interface Optimax\HealthCheckBundle\Service\HealthInterface
I need set tags to all services which implement this interface. I do it with the following directive:
_instanceof:
Optimax\HealthCheckBundle\Service\HealthInterface:
tags: ['health.service']
It works fine when I put this directive into config/services.yaml. But if I put this code into my bundle's config (which required via composer) vendor/optimax/health-check/src/Resources/config/services.yaml it doesn't work. I don't want copy-paste this directive into services.yaml every time when I require this bundle to a new project.
How can I move this directive into services.yaml which is in my Bundle's directory or at least into another file in config/packages folder of the project?
To expand on the issue for others.
_instanceof functions identically to the _defaults definition. In that the _instanceof definition only applies to the file it is used in.
This prevents third-party bundle definitions from affecting your entire application with definitions like:
_defaults:
public: true
autowire: false
_instanceof:
Psr\Log\LoggerAwareInterface:
- method: setLogger
arguments:
- '#custom_logger'
tags:
- { name: monologer.log, channel: 'custom_channel' }
Therefor if the service you are attempting to tag with _instanceof is not declared in the same services.yml file the tag will not be added.
To tag services that implements an Interface in the entire application, you would need to use the method provided by #Jeroen
Did you try auto tagging all services with this interface in your Bundle extension like this:
$container->registerForAutoconfiguration(CustomInterface::class)
->addTag('app.custom_tag')
;
Taken from the Symfony docs:
https://symfony.com/doc/current/service_container/tags.html

Categories