Dependency Injection

RestFn is using a custom Dependency Injection (DI) system where you inject dependencies by specifying them as public properties of your file.

DI system handles all auto-wiring and sending configuration to your classes by looking at the class metadata.

Usage

Injector is initialized by specifying:

$injector = new \ArekX\RestFn\DI\Injector();

In order to initalize a class by using an injector you need to call make() function.

$instance = $injector->make(\MyClass::class);
// Do something with your instance.

Calling function make() creates new instance of the desired class, processes auto-wiring and shares the class if necessary. You can see below for more info on specific parts of injection process.

Auto-wiring

Auto-wiring is a process of instantiating and loading all of the necessary dependencies which one class needs automatically without it being manually created. This means that if you have a class called Class1 which needs Class2 to function, you would usually first create Class2 and then pass it to Class1.

Example:

class Class2 { }
class Class1 { 
    public function __construct(Class2 $class2) {
        // do something with $class2
    }
}

$class2 = new Class2();
$class1 = new Class1($class2);

Auto-wiring handles this for you automatically so you would not need to do this manually.

This DI system only auto-wires classes which implement \ArekX\RestFn\DI\Contracts\Injectable interface. This is due to the nature of how this auto-wiring works. Other DI systems usually inject dependencies into the constructor itself because that is the best place available for them to ensure nothing can be done before they are available.

This DI system does not inject dependencies into a constructor. This system uses a feature of PHP 7.4 called property types. When you set a property type of a class this DI system resolves it to a valid dependency class, calls Injector::make() to make that dependency or load a shared instance if needed and wires it in.

These dependencies are injected in public properties before the __construct() is called so you will get the flexibility of having constructor arguments while making sure everything is auto-wired properly before your class can do any work.

By Implementing Injectable interface you ensure that only classes which need to have this functionality will actually be auto-wired.

Full Example:

class Class2 {
    public function test() {
        return "Auto-wiring works!";
    }
}

class Class1 implements \ArekX\RestFn\DI\Contracts\Injectable {
    public Class2 $class2;

    public function __construct($arg1) {
        echo $this->class2->test(); // Outputs: Auto-wiring works!
        echo $arg1; // Outputs: valuePassedToArg1
    }
}

$injector = new \ArekX\RestFn\DI\Injector();
$class1 = $injector->make(Class1::class, 'valuePassedToArg1');

Injector first creates the instance of this class, wires the dependencies then calls __construct() on the created the class, passing any arguments necessary.

Shared instances (Singletons)

Instances can be shared on the Injector across all calls to Injector::make() which includes time when you are auto-wiring dependencies. These classes are instantiated only once and their reference is shared across all subsequent calls to Injector::make().

Sharing as an instance

You can share an instance by calling:

$instance = new \MyClass();
$injector->share($instance);


$shared = $injector->make(\MyClass::class);

echo $shared === $instance ? 'Same classes' : 'Not same'; // Will output: Same classes

This class will be automatically shared across all calls to Injector::make()

Sharing as a class name

If you share a class as string (by calling MyClass::class or passing a string), this function will first instantiate this class by calling the Injector::make() function before making this class shared.

$injector->share(\MyClass::class);


$sharedA = $injector->make(\MyClass::class);
$sharedB = $injector->make(\MyClass::class);

echo $sharedA === $sharedB ? 'Same classes' : 'Not same'; // Will output: Same classes

Sharing by using an interface

If your class implements \ArekX\RestFn\DI\Contracts\SharedInstance your class will always automatically be defined as a shared class and instantiated only once. Using this method is useful when you want to create your own services such as Database service or other api services which need to be only instantiated once.

class MyClass implements \ArekX\RestFn\DI\Contracts\SharedInstance {}

$sharedA = $injector->make(\MyClass::class);
$sharedB = $injector->make(\MyClass::class);

echo $sharedA === $sharedB ? 'Same classes' : 'Not same'; // Will output: Same classes

Configurable Instances

Instances can have configurations passed to them by the injector itself. In order for instances to get the configuration passed to them they need to implement ArekX\RestFn\DI\Contracts\Configurable interface.

To these interfaces, injector will pass the array config to them before their __construct() is called. This is done in a way to ensure that the class you instantiate has everything ready for it before it can do any work.

Configuration is passed per class in an array during Injector creation or by a call to Injector::configure():

$injector = new \ArekX\RestFn\DI\Injector([
    'configurations' => [
         \MyConfigurableClass::class => [
                'key1' => 'value',
                'key2' => 'value2',
            ]
    ]
]);

class MyConfigurableClass implements ArekX\RestFn\DI\Contracts\Configurable {
    public function configure(array $config) {
        /**
          * $config here will contain 
          * [
          *    'key1' => 'value',
          *    'key2' => 'value2',
          * ] 
          */
    }

    public function __construct($arg1, $arg2) {
        // This is called after configure in this case, so all dependencies
        // and configuration is available here.
    }
}

$instance = $injector->make(MyConfigurableClass::class);

If there is no configuration specified for the instance in the injector itself. Injector will throw an error.

You can also manually pass the configuration for a specific class by calling Injector::configure().

Aliasing

Classes can be aliased so that you can set an interface and inject the implementation of that interface to every class which implements Injectable interface.

Aliasing can be setup by calling Injector::alias() or by passing the constructor configuration.

interface MyInterface {}

class MyClass implements MyInterface {}

class MyClass2 implements \ArekX\RestFn\DI\Contracts\Injectable {
    public MyInterface $interface;
}

$injector = new \ArekX\RestFn\DI\Injector([
    'aliases' => [
        \MyInterface::class => \MyClass::class
    ]
]);

$instance = $injector->make(MyClass2::class); // Creates instance of MyClass2 with MyClass injected into $interface.

Factories

Factory classes are classes to which injector delegates instance creation. When a make() function is called, injector first checks if there are factory classes set for that specific class and if there are it instantiates the factory classes using make() method and then calls the factory's create() method, passing the desired class and arguments.

For a class to be a factory class it must implement the \ArekX\RestFn\DI\Contracts\Factory interface.

Example:

class MyFactory implements \ArekX\RestFn\DI\Contracts\Factory {

  public function create(string $definition,array $args) {
      // Your logic for handling $definitionClass here.
      return new $definition(...$args);
  }
}

class MyClass {}

$injector = new \ArekX\RestFn\DI\Injector([
    'factories' => [
        \MyClass::class => \MyFactory::class
    ]
]);

// Instance will be created by using MyFactory::create() method.
$instance = $injector->make(MyClass::class);

Please note that instances created through factory's create() method will not go through the injection process which means that they will not be auto-wired or shared by default. If you need this functionality then you need to inject the injector in the factory function.

Example:

class MyFactory implements \ArekX\RestFn\DI\Contracts\Factory {

  public \ArekX\RestFn\DI\Injector $injector;

  public function create(string $definition,array $args) {
      // Injector calls disableFactory() for this class before calling this create() function
      // so its safe to directly call make() here.
      return $this->injector->make($definition, ...$args);
  }
}

class MyClass {}

$injector = new \ArekX\RestFn\DI\Injector([
    'factories' => [
        \MyClass::class => \MyFactory::class
    ]
]);

$injector->share($this);


// Instance will be created by using MyFactory::create() method.
$instance = $injector->make(MyClass::class);

Considerations

Injector is NOT a service locator

For most of your use-cases RestFn will handle all injection for you. However you can request na injector by specifying the Injector as a public property in your classes which implement Injectable interface.

But this injector should never be used for loading services directly by passing definitions.

This makes the DI system a Service Locator, which is an anti-pattern and you should only use the injector directly when you need to resolve or map classes manually through some logic such as mapping a request value to a specific class, or to handle some specific injection use cases.