Integrate Symfony's Dependency Injection (autowiring) into legacy app - php
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.
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)
How to separate Symfony (6.x) service and its configuration (using ContainerConfigurator)?
I have a service (it is not a bundle but might be in the future) which is using complex array as configuration - I would like to create a separate config file for that array in config/packages, let`s say "config/packages/my_service.php". Example which I think is valid from Documentation (https://symfony.com/doc/current/configuration.html#accessing-configuration-parameters) (see below) does not work with error "You have requested a non-existent service ...". My service is available via debug:container and works otherwise. Using Symfony 6.1. I am not sure why this service is not available during config compile time. <?php namespace Symfony\Component\DependencyInjection\Loader\Configurator; use App\Services\MyService; return static function(ContainerConfigurator $container) { $container->services()->get(MyService::class) ->arg('$foo','bar'); }; where 'foo' => 'bar' would be my complex config. Another way to separate service from configuration is by using configurator service which Symfony offers (I have used it in different cases) but I am reluctant because I wanted to have all config under config folder. Any other ways to do it?
The definition for "security.user_password_hasher" has no class, during Symfony 5.3 phpunit tests?
The question is somehow related to: How to access a private service in Symfony 5.3 phpunit tests?. I set up env services_test.yaml by: services: security.user_password_hasher: public: true When I try retrieve security.user_password_hasher I get exception: $encoded = static::getContainer()->get('security.user_password_hasher')->hashPassword($user, $password); Symfony\Component\DependencyInjection\Exception\RuntimeException: The definition for "security.user_password_hasher" has no class. If you intend to inject this service dynamically at runtime, please mark it as synthetic=true. If this is an abstract definition solely used by child definitions, please add abstract=true, otherwise specify a class to get rid of this error. Why I getting this exception? In the same test suite I retrieve EntityManagerInterface::class and everything is okay. When I access UserPasswordHasherInterface (it's alias for security.user_password_hasher) in dev env by injecting it in controller public function homepage(UserPasswordHasherInterface $hash) no error is thrown. Why changing it settings to public: true crashing tests? It's that this service is special, and need special care in test env? Or maybe because test env change some defaults settings? I'm newbie to Symfony. My guess is that container do some hidden magic. Someone can reveal the secret? What does mean: [NOTE] The "security.user_password_hasher" service or alias has been removed or inlined when the container was compiled?
Unit testing services annotations without Symfony
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.
how do I use Symfony console with dependency injection, without the Symfony framework bundle?
I have a command-line application, which so far uses the Symfony dependency injection component. I now find that I want to add command-line options and improve the formatting of the output, and the Symfony console component seems like a good choice for that. However, I can't fathom how to get my Symfony console command classes to receive the container object. The documentation I have found uses the ContainerAwareCommand class, but that is from the FrameworkBundle -- which seems a huge amount of overhead to add to a pure CLI app, as it requires further bundles such as routing, http, config, cache, etc, none of which are of any relevance to me whatsoever here. (Existing SO question How can i inject dependencies to Symfony Console commands? also assumes the FrameworkBundle, BTW.) I've made a test repository here with a basic command that illustrates the problem: https://github.com/joachim-n/console-with-di
Symfony 3/4/5 Way Since 2018 and Symfony 3.4+ DI features, you can make use of commands as services. You can find working demo here, thanks to #TravisCarden In short: 1. App Kernel <?php # app/Kernel.php namespace App; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\Console\Application; use Symfony\Component\Console\Command\Command; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\DependencyInjection\ContainerBuilder; final class AppKernel extends Kernel { public function registerBundles(): array { return []; } public function registerContainerConfiguration(LoaderInterface $loader): void { $loader->load(__DIR__.'/../config/services.yml'); } protected function build(ContainerBuilder $containerBuilder): void { $containerBuilder->addCompilerPass($this->createCollectingCompilerPass()); } private function createCollectingCompilerPass(): CompilerPassInterface { return new class implements CompilerPassInterface { public function process(ContainerBuilder $containerBuilder) { $applicationDefinition = $containerBuilder->findDefinition(Application::class); foreach ($containerBuilder->getDefinitions() as $definition) { if (! is_a($definition->getClass(), Command::class, true)) { continue; } $applicationDefinition->addMethodCall('add', [new Reference($definition->getClass())]); } } }; } } 2. Services # config/services.yml services: _defaults: autowire: true App\: resource: '../app' Symfony\Component\Console\Application: public: true 3. Bin File # index.php require_once __DIR__ . '/vendor/autoload.php'; use Symfony\Component\Console\Application; $kernel = new AppKernel; $kernel->boot(); $container = $kernel->getContainer(); $application = $container->get(Application::class) $application->run(); Run it php index.php If you're interested in a more detailed explanation, I wrote a post Why You Should Combine Symfony Console and Dependency Injection.
Yes, the whole framework isn't required. In your case, first you need to create a kind of entry script. Something like that: <?php require 'just/set/your/own/path/to/vendor/autoload.php'; use Symfony\Component\Console\Application; use Symfony\Component\DependencyInjection\ContainerBuilder; $container = new ContainerBuilder(); $container ->register('your_console_command', 'Acme\Command\YourConsoleCommand') ->addMethodCall('setContainer', [new Reference('service_container')]); $container->compile(); $application = new Application(); $application->add($container->get('your_console_command')); $application->run(); In this example, we create the container, then register the command as a service, add to the command a dependency (the whole container in our case -- but obviously you can create another dependency and inject it) and compile the container. Then we just create app, add the command instance to the app and run it. Sure, you can keep all configurations for container in yaml or xml or even using PHP format.
Four years after, but intended for somebody looking for a similar solution. I had a look alike scenario and implemented my own boilerplate with Dependency Injection ready to go for a standalone symfony console application, supporting auto-wiring and auto-configure for commands and services. composer create-project coral-media/crune crune A sample command receiving the container it's in place. Source code available at Github Happy Coding!