Unit testing services annotations without Symfony - php

I got a few bundles that are not installed in Symfony yet.
These bundles have a services.yml file in them:
mybundle/src/Bundle/Resources/config/services.yml
The services.yml contains classes and arguments from the bundle that are later used by Symfony, but not by the bundle itself:
mybundle.data.download.get:
class: mybundle\data\download\getinfo\get
arguments:
- "#bundle.myDepdendency.generate"
- "#bundle.myDepdendency.dosomething"
- "#bundle.helloThere"
I have working unit tests in Symfony for services.yml that checks that all classes are loaded correctly, however since I am developing the bundles independently outside of Symfony, I'd like to have a test to know if services.yml contains all the classes and their arguments.
So the question is:
Is there a way to test if services.yml contains given classes and their arguments without using Symfony?

I would start with this snippet:
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
$containerBuilder = new ContainerBuilder();
$loader = new YamlFileLoader($containerBuilder, new FileLocator(__DIR__.'/../BundlePath/Resources/config'));
$loader->load('services.yml');
$containerBuilder->compile();
Of course you need the symfony/dependency-injection and symfony/config components of symfony. But here you would test if any exception will thrown. If not, then every service was found and could be wired.
With
$containerBuilder->get('service_id') instanceof Bundle\Service\SomeService
you can even test if the service class was realy loaded.

Related

autowire Predis Interface in symfony

i wanna use ClientInterface in my class constructor and i give an error :
Cannot autowire service "App\Service\DelayReportService": argument "$client" of method "__construct()" references interface "Predis\ClientInterface" but no such service exists. Did you create a class that implements this interface?
seems to be i should add it manually to services.yml i added it like :
Predis\ClientInterface: '#Predis\Client'
and now i give this error:
You have requested a non-existent service "Predis\Client".
what is the solution and why symfony itself dont handle it?
you seem to be confused about how to define a service... which isn't surprising tbh
look here
https://symfony.com/doc/5.4/service_container.html#explicitly-configuring-services-and-arguments
for example
services:
App\Service\Concerns\IDevJobService:
class: App\Tests\Service\TestDevJobService
autowire: true
public: true
where
IDevJobService is an INTERFACE
and
TestDevJobService
is the actual implementation that will be auto injected
using # inside the yaml files is done to reference a service that has already been defined ELSEWHERE
https://symfony.com/doc/5.4/service_container.html#service-parameters
you probably want to watch symfonycasts services tutorial (I am not affiliated and I havent watched it myself yet (sure wish I did)).
EDIT
Predis\Client is a 3rd party class. It isn't in your App namespace or in your src folder. Symfony checks the src folder for class that it will then make to a service. See services.yaml there is a comment there, look for exclude and resource. And I'm not sure, even if you autoload it, that you can then just do #Predis\Client to reference an existing service.
be sure as well to debug your config using
php bin/console debug:autowiring
under linux you could do as well php bin/console debug:autowiring | grep Predis to find it more quickly (if it is there at all)

Integrate Symfony's Dependency Injection (autowiring) into legacy app

I have a legacy application which is built upon an old custom MVC framework which I'd like to eventually move away from. This framework does not rely on a single front controller, so most pages still have dedicated php files to call the respected controller, others are mixed php/html. I've read up on migrating applications to symfony using various methods (https://symfony.com/doc/current/migration.html), but I've had issues with both methods and have come to the realization that I don't really need symfony's route handling.
Symfony currently exists in our application, but is only used by various commands. All of our core logic is still in the legacy application, so Symfony can access it no problem as the classes are all in the global namespace. However, the problem is, the legacy application cannot utilize any of the new Symfony classes as it does not support dependency injection. This ability would be needed in order to start moving some of our core logic and features to Symfony.
Ideally what I'd like to be able to accomplish is loading in the container to our legacy application, which has all of our autowired services available. Allowing me to access our new Symfony based services, in our legacy application.
Any help is greatly appriciated.
Thank you very much.
Update 1
So I tried what #Cerad said, just access the kernel as it's global. I copied over the bootstrap.php logic to my main config for my legacy application (so it loads up the existing .env* files), then booted the kernel (instantiated it and called boot in my legacy config). It works, I can reference $kernel (using global $kernel) in my php files and access the container. However, which this is inline with #Dmitry Solovov's response, the services must be public.
Must I set all services I want available as public? If I manually define the service in services.yaml, set it to public, it works.
But this isn't really ideal as I would like to autoload my services, so I can use services the correct way and not have to explicitly define each service I want available in my legacy app.
How might I inject services into my legacy controllers, without making the service public? Just like how Symfony's controller allows you to inject services into Controller methods?
Thanks a lot.
To use Dependency Injection independently:
install the package:
composer require symfony/dependency-injection
define your services in configuration file (i.e. src/Resources/config/services.yaml). Example:
services:
_defaults:
autowire: true
autoconfigure: true
public: false
App\Services\MyService:
class: App\Services\MyService
public: true
You can also use service auto-import feature https://symfony.com/doc/current/service_container.html#importing-many-services-at-once-with-resource
compile the DI-container with a following code:
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
$container = new ContainerBuilder();
$loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/src/Resources/config'));
$loader->load('services.yaml');
$container->compile();
inject this container instance into your application. Or extend the ContanerBuilder class and make it a singleton.
Services to use in your application should be public, so you can get them directly from container:
$service = $container->get(\App\Services\MyService::class);
You can also make all of your services public by default:
services:
_defaults:
public: true
I have a single config which is loaded on all pages, that's where I copied the bootstrap logic and loaded the kernel:
// ***** legacy config code above
// This probably could just be loaded using require, but kept it here for completeness
if (is_array($env = #include dirname(__DIR__).'/.env.local.php')) {
foreach ($env as $k => $v) {
$_ENV[$k] = $_ENV[$k] ?? (isset($_SERVER[$k]) && 0 !== strpos($k, 'HTTP_') ? $_SERVER[$k] : $v);
}
} elseif (!class_exists(Dotenv::class)) {
throw new RuntimeException('Please run "composer require symfony/dotenv" to load the ".env" files configuring the application.');
} else {
// load all the .env files
(new Dotenv(false))->loadEnv(dirname(__DIR__).'/config/.env');
}
$_SERVER += $_ENV;
$_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? null) ?: 'dev';
$_SERVER['APP_DEBUG'] = $_SERVER['APP_DEBUG'] ?? $_ENV['APP_DEBUG'] ?? 'dev' == $_SERVER['APP_ENV'];
$_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = (int) $_SERVER['APP_DEBUG'] || filter_var($_SERVER['APP_DEBUG'], FILTER_VALIDATE_BOOLEAN) ? '1' : '0';
// End Symfony's bootstrap
// Load Symfony's kernel
$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);
$kernel->boot();
From there I was able to access the specific service I wanted - as long as it was public:
global $kernel;
$service = $kernel->getContainer()->get(\App\Services\MyService::class);
The real idea that got this working well for my project was not setting all services public and instead creating a legacy service, which was manually defined in services.yaml and set to public:
services:
App\Services\Legacy\AWSLegacy:
public: true
AWSLegacy would look something like:
namespace App\Services\Legacy;
use App\Services\AWS\S3;
class AWSLegacy
{
/** #var S3 */
public $s3;
public function __construct(
S3 $s3
)
{
$this->s3 = $s3;
}
}
This allowed me to group similar services together that I wanted available in my legacy application, without manually creating a reference for each in services.yaml and setting them public.
The Symfony bridge methods didn't work for me as I didn't want Symfony to handle routing (request and responses) in my legacy app, I just wanted access to the new services.
Thank you #Cerad and #Dmitry Solovov for the help.

Symfony 3.4 controller registered as service throws deprecation warning

I have a controller, let's say Acme\ShopBundle\Controller\ProductListController
And its definition in services.yml is as follows:
services:
Acme\ShopBundle\Controller\ProductListController:
class: Acme\ShopBundle\Controller\ProductListController
arguments: ['#product_service']
Which throws this in my log file:
User Deprecated: The "Acme\ShopBundle\Controller\ProductListController" service is private, checking for its existence is deprecated since Symfony 3.2 and will fail in 4.0.
Followed by
User Deprecated: The "Acme\ShopBundle\Controller\ProductListController" service is private, getting it from the container is deprecated since Symfony 3.2 and will fail in 4.0. You should either make the service public, or stop using the container directly and use dependency injection instead.
The stack trace list of files is completely inside vendor/symfony so I'm assuming something is misconfigured, but stumped as to what. Any help appreciated.
Controller service must be public:
services:
Acme\ShopBundle\Controller\ProductListController:
public: true
arguments: ['#product_service']
Why aren't you using autowiring anyway? You could register all of your controllers then:
Acme\ShopBundle\Controller\:
resource: '../src/Acme/ShopBundle/Controller' # mutatis mutandis
tags: ['controller.service_arguments']
Kindly read about new features regarding dependency management in Symfony 3.

Resolving Controller Services in Sylius/Symfony

Hoes does Symfony resolve the Sylius service sylius.controller.shop_user service to the controller class file Sylius\Bundle\UserBundle\Controller\UserController.
My understanding is that sylius.controller.shop_user is a service, and that in Symfony there will be a corresponding service configuration. This service configuration will tell Symfony which class to use when it needs to instantiate the service.
However, I can't seem to find a sylius.controller.shop_user configuration in the Sylius source configuration anywhere. There's just references to this service in routing files
#File: src/Sylius/Bundle/ShopBundle/Resources/config/routing/ajax/user.yml
sylius_shop_ajax_user_check_action:
path: /check
methods: [GET]
defaults:
_controller: sylius.controller.shop_user:showAction
_format: json
_sylius:
repository:
method: findOneByEmail
arguments:
email: $email
serialization_groups: [Secured]
or in on-disk container cache files.
var/cache/dev/srcKernelDevDebugContainer.xml
1798: <parameter key="sylius.controller.shop_user.class">Sylius\Bundle\UserBundle\Controller\UserController</parameter>
15230: <service id="sylius.controller.shop_user" class="Sylius\Bundle\UserBundle\Controller\UserController" public="true">
So how does Symfony know to instantiate the right class for this service?
Is there configuration I'm not seeing? Some Symfony magic that auto-generates the class? Some other mysterious third thing where I don't know what I don't know?
I don't have any specific task in mind, I'm just trying to get a feel for how Sylius and Symfony work under the hood.
The controller service is defined based on ResourceBundle's configuration in Sylius\Bundle\ResourceBundle\DependencyInjection\Driver\AbstractDriver::addController. This driver is called when loading a bundle.
Services with the name sylius.controller.[entity-name] are part of the
Sylius
entity resource system. As best I can tell, when you define your new doctrine entities
in a specific way and
register them as a Sylius resource, Sylius will
automatically generate these controller services based on your
configuration.
The actual line of code that defines these services
is here.
#File: src/Sylius/Bundle/ResourceBundle/DependencyInjection/Driver/AbstractDriver.php
/* ... */
$container->setDefinition($metadata->getServiceId('controller'), $definition);
/* ... */
The
Sylius\Bundle\ResourceBundle\DependencyInjection\Driver\AbstractDriver
class is a (as of 1.3) a base class for the
Sylius\Bundle\ResourceBundle\DependencyInjection\Driver\Doctrine\DoctrineORMDriver
class. How this class ends up being used is by Symfony is unclear, but is
fortunately beyond the scope of this answer.

Symfony2 LiipFunctionalTestBundle overriding #validator service

I am trying to inject #validator into my service but LiipFunctionalTestBundle is overriding that service when it gets injected.
admin.image_service:
class: AdminBundle\Service\ImageService
arguments: ["#validator", "#doctrine.orm.admin_entity_manager", "#image_storage_filesystem"]
Which results in the error
must be an instance of Symfony\Component\Validator\Validator\RecursiveValidator, instance of Liip\FunctionalTestBundle\Validator\DataCollectingValidator given
running php bin/console debug:container results in
liip_functional_test.validator: Liip\FunctionalTestBundle\Validator\DataCollectingValidator
validator: alias for "liip_functional_test.validator"
Is there a way to get around this over than remove liip and refactor all of my tests?
In Your service you should typehint Interface not exact class.
Instdead Symfony\Component\Validator\Validator\RecursiveValidator use Symfony\Component\Validator\Validator\ValidatorInterface - which is implemented by both classes (Symfony and Liip).

Categories