3

I'm trying to implement Symfonys dependency injection container.

I have 2 containers set, one for the database, and one for the system user.

and I'm using addArgument() to both the App class and SystemUser class, pushing to App class a SystemUser object, and for the SystemUser class a Database object.

index.php:

require_once 'vendor/autoload.php';

use TestingDI\App;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

$containerBuilder = new ContainerBuilder();
$containerBuilder->register('database', '\TestingDI\Database');

$containerBuilder->register('system.user', '\TestingDI\SystemUser')
                 ->addArgument(new Reference('database'));

$containerBuilder->register('app', '\TestingDI\App')
                 ->addArgument(new Reference('system.user'));


$database       = $containerBuilder->get('database');
$systemUser     = $containerBuilder->get('system.user');
$app            = $containerBuilder->get('app');

# Initialize App class
$app = new App();

App.php:

<?php 

namespace TestingDI;

use TestingDI\SystemUser; 


class App {

    public $systemUser; 

    public function __construct(SystemUser $systemUser)
    {
        var_dump($systemUser);
    }
}

I do see my var_dump result, with the object, but keep getting this error:

PHP Fatal error: Uncaught ArgumentCountError: Too few arguments to function TestingDI\App::__construct(), 0 passed in /www/potato/symfony-di/index.php on line 28 and exactly 1 expected in /www/potato/symfony-di/testingdi/App.php:12

Stack trace:

0 /www/potato/symfony-di/index.php(28): TestingDI\App->__construct()

1 {main} thrown in /www/potato/symfony-di/testingdi/App.php on line 12

These are my other classes:

SystemUser.php

<?php 
namespace TestingDI;

use TestingDI\Database;

class SystemUser {

    public $db; 

    public function __construct( Database $database )
    {
        $this->db = $database;
    }
}

Database.php

<?php 
namespace TestingDI;

class Database {

    public function __construct()
    {

    }
}
yivi
  • 42,438
  • 18
  • 116
  • 138
Imnotapotato
  • 5,308
  • 13
  • 80
  • 147
  • 2
    `$app = new App();` isn't going to use DI, is it? Not sure why this is unexpected. – Jonnix Jan 14 '19 at 14:15
  • no, it should just accept the SystemUser type variable. (see app class). If i did something wrong please notify me, im very new to this subject @JonStirling – Imnotapotato Jan 14 '19 at 14:17
  • 1
    Right? Which you're not passing in? `new App()` <-- you pass no args, but you class requires at least 1. – Jonnix Jan 14 '19 at 14:17
  • Oh, I thought that the object is created only when called. In that case, if I want to load the database object in my SystemUser object, how do i do that? @JonStirling – Imnotapotato Jan 14 '19 at 14:20
  • It is a very common conceptual mistake. The php new operator knows nothing about Symfony containers. Pull the app from the container instead of newing it. In fact it looks like your are already doing just that. So get rid of $app = new App(); completely. – Cerad Jan 14 '19 at 14:21
  • `$containerBuilder->register('system.user', '\TestingDI\SystemUser') ->addArgument(new Reference('database'));` – Imnotapotato Jan 14 '19 at 14:21
  • @RickSanchez Is `$app` not what you want it to be /before/ you call `new`? – Jonnix Jan 14 '19 at 14:21
  • It is, looks the same, does the same, it's just weird for me to call a class like this. which still confuses me because I'm trying to do the same with SystemUser and Database and I do not get any response from the $database variable set in the SystemUser `__construct()` – Imnotapotato Jan 14 '19 at 14:23
  • @JonStirling So just to make sure that I expect things to work as I imagine... If I connect to my database (via Database class) from my SystemUser class.. and then create a `new Controller()` class and inject the Database class (exactly like I did with the system user class) - The connection instance will not get terminated? Up until now I created a static method and called `getDbInstance` in every class that used the database until I was told this is a bad practice. – Imnotapotato Jan 14 '19 at 15:00
  • Additionally: you do not have "two" containers. There is only one container, that you initialize when you do `$containerBuilder = new ContainerBuilder();`. All the rest is _configuring_ the container. – yivi Jan 14 '19 at 15:27

1 Answers1

6

There seems to be a basic misconception on what dependency injection is and how to use it.

Since you are using a dependency container to build the different parts of your app, you should be getting those objects from the container, not instantiate them directly.

And, if you are going to be instantiating them directly (e.g. new App()) you need to respect the regular language rules. In this case, the constructor's signature:

public function __construct(SystemUser $systemUser)

If you call the constructor without passing it an object of the SystemUser class, it will fail, in the same way any other method/function will fail if you call it in a way that doesn't conform with its signature.

By the time you did

$app            = $containerBuilder->get('app');

Your app was already "initialized", and you didn't need to call new at all.

By doing this:

$containerBuilder = new ContainerBuilder();
$containerBuilder->register('database', '\TestingDI\Database');

$containerBuilder->register('system.user', '\TestingDI\SystemUser')
                 ->addArgument(new Reference('database'));

$containerBuilder->register('app', '\TestingDI\App')
                 ->addArgument(new Reference('system.user'));


$database       = $containerBuilder->get('database');
$systemUser     = $containerBuilder->get('system.user');
$app            = $containerBuilder->get('app');

You were doing the equivalent of:

$database   = new Database();
$systemUser = new SystemUser($database);
$app        = new App($systemUser);

Only you were letting the DI container do the work for you. Which of course, given how verbose your container definition is, seems kinda pointless.

But you can let the power of autowiring do most of the heavy-lifting for you, and do the following:

$container = new ContainerBuilder();
$container->autowire( Database::class );
$container->autowire( SystemUser::class );
$container->autowire( App::class )
          ->setPublic( true );

$container->compile();

// this will get you an application instance with all the dependencies
// resolved and injected in the class.
$app = $container->get( App::class );

I now notice you already had a similar problem while trying a different DI library, and I realize now that the whole problem revolves about a simple misunderstanding about what problem a DI container deals with

yivi
  • 42,438
  • 18
  • 116
  • 138
  • yup! I did a small research and decided it's better to go with Symfonys DI https://rawgit.com/kocsismate/php-di-container-benchmarks/master/var/benchmark.html – Imnotapotato Jan 14 '19 at 15:13
  • Just to make sure that I expect things to work as I imagine and that I understand this DIC... If I connect to my database (via Database class) from my SystemUser class.. and then lets say I create a `new Controller()` class and inject the Database class (exactly like I did with the SystemUser class, where in the construct I added `__construct(Database $database)`) - The connection instance will not get terminated? Up until now I created a static method and called `getDbInstance()` in every class that used the database until I was told this is a bad practice – Imnotapotato Jan 14 '19 at 15:13
  • What does autowrite do exactly, i don't really get it from the example – Imnotapotato Jan 14 '19 at 15:15
  • @Rick I do not understand exactly what you mean. But if you are using DI properly, you almost never will need to write a `new` anything. You need to get your services' instances from the container, not instantiate them manually. See my latest edit on how to do go about it using auto-wiring. – yivi Jan 14 '19 at 15:16
  • 1
    Autowiring resolves the dependencies for you, 'guessing" them from the constructors signatures. – yivi Jan 14 '19 at 15:17
  • Before worrying about _benchmarks_, I believe it will be better for you if you properly understand the concepts you are trying to apply. DI and IOC are not immediately obvious, but once you understand it it's not black magic. Try to investigate more on these subjects. But if you use the code of this answer, you should be up and running. – yivi Jan 14 '19 at 15:21
  • So when using dependency injections in my case, i need to "initialize" all classes at the beginning of my code to use them later? :S I'm gonna open a new post with my new questions + examples. I'll post my link here and I hope you'll understand it with the examples. – Imnotapotato Jan 14 '19 at 15:32
  • You need to configure your container, that's what you were doing on these examples. It may be best if you spend some time reading about the concepts before posting a new question. _Conceptual_ questions do not always fare well in Stack Overflow. – yivi Jan 14 '19 at 15:34
  • When I register classes to containers, are they "initialized"? Or they get initialized only when I create a class that depends on them? If the answer is no, is there a way to do that? Just to prevent initialization of non-used objects on certain controllers. – Imnotapotato Jan 17 '19 at 08:58
  • @Rick Objects are not instantiated until the are actually needed. – yivi Jan 17 '19 at 09:06
  • Specifically when using the `autowire` syntax, am I right? When I used in my prev example `$database = $containerBuilder->get('database'); $systemUser = $containerBuilder->get('system.user'); $app = $containerBuilder->get('app');` I got echos I set in all of these classes constructs. and `$app = $containerBuilder->get('app');` didn't work without setting `$systemUser = $containerBuilder->get('system.user');` which is a must in it's construct. – Imnotapotato Jan 17 '19 at 09:13
  • @Rick If you do `$container->get()` **you are creating a new instance**, hence all necessary objects will be instantiated. `get()` is not for configuration, but to actually retrieve an object from the containe. `get()` == `new`. – yivi Jan 17 '19 at 09:15