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

Viewed 989

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.

2 Answers

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