PHP code example of suhock / php-dependency-injection

1. Go to this page and download the library: Download suhock/php-dependency-injection library. Choose the download type require.

2. Extract the ZIP file and open the index.php.

3. Add this code to the index.php.
    
        
<?php
require_once('vendor/autoload.php');

/* Start to develop here. Best regards https://php-download.com/ */

    

suhock / php-dependency-injection example snippets


$container = new Suhock\DependencyInjection\Container();
$container->addSingletonClass(MyApplication::class)
    // Add the rest of your dependencies...
    ->get(MyApplication::class)
    ->run();

use Suhock\DependencyInjection\Container;

$container = new Container();

$container
    // Autowire the constructor
    ->addSingletonClass(MyApplication::class)

    // Manually construct an instance with factory
    ->addSingletonFactory(Logger::class, fn () => new FileLogger('myapp.log'))

    // Alias an interface to an implementing type
    ->addTransientImplementation(HttpClient::class, CurlHttpClient::class)

    // Add optional values with a mutator after autowiring the constructor
    ->addTransientClass(
        CurlHttpClient::class,
        function (CurlHttpClient $client, Logger $logger): void {
            $client->addLogger($logger);
        }
    );

$container
    ->get(MyApplication::class)
    ->handleRequest();

class MyApplication
{
    // The constructor arguments will be provided by the container
    public function __construct(
        private readonly HttpClient $client,
        private readonly Logger $logger
    ) {
    }
}

$container->addSingletonInstance(Container::class, $container);

class MyRouter
{
    public function __construct(
        private readonly Container $container
    ) {
    }

    public function routeRequest(string $method, string $path): void
    {
        $controllerClassName = $this->getControllerClassName($path);
        $controller = $this->container->get($controllerClassName);
        $controller->handleRequest($method);
    }
}

callable<TClass> Mutator(TClass $instance, [object|null ...]): void;

class Container
{
    function addSingletonClass<TClass>(
        string<TClass> $className,
        Mutator<TClass>|null $mutator = null
    ): static;

    function addTransientClass<TClass>(
        string<TClass> $className,
        Mutator<TClass>|null $mutator = null
    ): static;
}

$container->addSingletonClass(MyService::class);

$container->addTransientClass(
    CurlHttpClient::class,
    function (CurlHttpClient $obj, Logger $logger): void {
        $obj->setLogger($logger);
    }
);

use Suhock\DependencyInjection\Autowire;

class CurlHttpClient
{
    #[Autowire]
    public function setLogger(Logger $logger): void {
        $this->logger = $logger;
    }
    
    // ...
}

class Container
{
    function addSingletonImplementation<TClass, TImpl of TClass>(
        string<TClass> $className,
        string<TImpl> $implementationClassName
    ): static;

    function addTransientImplementation<TClass, TImpl of TClass>(
        string<TClass> $className,
        string<IImpl> $implementationClassName
    ): static;
}

$container
    ->addSingletonImplementation(HttpClient::class, CurlHttpClient::class)
    ->addSingletonClass(CurlHttpClient::class);

$container
    ->addTransientImplementation(Throwable::class, Exception::class)
    ->addTransientImplementation(Exception::class, LogicException::class)
    ->addTransientClass(LogicException::class);

$container->addSingletonClass(HttpClient::class, CurlHttpClient::class);

/*
 * The container will throw an UnresolvedDependencyException because it does
 * not know how to provide an instance of CurlHttpClient.
 */
$container->get(HttpClient::class);

callable<TClass> FactoryMethod([object|null ...]): TClass;

class Container
{
    function addSingletonFactory<TClass>(
        string<TClass> $className,
        FactoryMethod<TClass> $factory
    ): static;

    function addTransientFactory<TClass>(
        string<TClass> $className,
        FactoryMethod<TClass> $factory
    ): static;
}

$container->addSingletonFactory(
    Mailer::class,
    fn (AppConfig $config) => new Mailer($config->mailerTransport)
);

$container->addTransientFactory(
    Logger::class,
    fn (FileWriter $writer) => new class($writer) implements Logger {
        public function __construct(private readonly FileWriter $writer)
        {
        }

        public function log(string $message): void
        {
            $this->writer->writeLine($message);
        }
    }
);

class Container
{
    function addSingletonInstance<TClass>(
        string<TClass> $className,
        TClass $instance
    ): static;
}

$request = new Request($_SERVER, $_GET, $_POST, $_COOKIE);
$container->addSingletonInstance(Request::class, $request);

callable ClassFactory<TClass>(
    string<TClass> $className,
    object|null ...$dependencies
): TClass;

class Container
{
    function addSingletonNamespace(
        string $namespace,
        ClassFactory|null $factory = null
    ): static;

    function addTransientNamespace(
        string $namespace,
        ClassFactory|null $factory = null
    ): static;
}

$container->addSingletonNamespace('Http');

/*
 * The container will provide an instance of CurlHttpClient by autowiring the
 * constructor because the class is in the Http namespace.
 */
$curlClient = $container->get(Http\CurlHttpClient::class);

$container->addSingletonImplementation(
    Http\HttpClient::class,
    Http\CurlHttpClient::class
);

/*
 * The container will know to autowire CurlHttpClient for HttpClient because we
 * specified the interface-implementation mapping.
 */
$httpClient = $container->get(Http\HttpClient::class);

callable<TClass> ImplementationFactory<TImpl of TClass>(
    string<TImpl> $className,
    [object|null ...]
): TImpl;

class Container
{
    function addSingletonInterface<TInterface>(
        string<TInterface> $className,
        ImplementationFactory<TClass>|null $factory = null
    ): static;

    function addTransientInterface<TInterface>(
        string<TInterface> $className,
        ImplementationFactory<TClass>|null $factory = null
    ): static;
}

$container->addSingletonInterface(
    EntityNameProvider::class,
    /**
     * @template T of EntityNameProvider
     * @var class-string<T> $className
     * @return T
     */
    fn (string $className, EntityManager $em) =>
        $em->getRepository($className::getEntityName())
);

/*
 * The container will query the EntityManager for a UserRepository.
 */
$userRepository = $container->get(UserRepository::class);

class UserRepository extends EntityRepository implements EntityNameProvider
{
    public static function getEntityName(): string
    {
        return User::class;
    }
}

callable<TAttr> AttributeClassFactory<TClass>(
    string<TClass> $className,
    TAttr $attributeInstance,
    [object|null ...]
): TClass;

class Container
{
    function addSingletonAttribute<TAttr>(
        string<TAttr> $attributeName,
        AttributeClassFactory<TAttr>|null $factory = null
    ): static;

    function addTransientAttribute<TAttr>(
        string<TAttr> $attributeName,
        AttributeClassFactory<TAttr>|null $factory = null
    ): static;
}

$container->addSingletonAttribute(
    EntityName::class,
    fn (string $className, EntityName $attribute, EntityManager $em) =>
        $em->getRepository($attribute->getName())
);

/*
 * The container will query the EntityManager for a UserRepository.
 */
$userRepository = $container->get(UserRepository::class);

#[EntityName(User::class)]
class UserRepository extends EntityRepository
{
}

#[Attribute(Attribute::TARGET_CLASS)]
class EntityName
{
    public function __construct(
        private readonly string $name
    ) {
    }

    public function getName(): string
    {
        return $this->name;
    }
}

class Container
{
    public function add<TClass>(
        string<TClass> $className,
        LifetimeStrategy<TClass> $lifetimeStrategy,
        InstanceProvider $instanceProvider
    ): static;

    public function addSingleton<TClass>(
        string<TClass> $className,
        InstanceProvider $instanceProvider
    ): static;

    public function addTransient<TClass>(
        string<TClass> $className,
        InstanceProvider $instanceProvider
    ): static;
}

callable LifetimeStrategyFactory<TClass>(
    string<TClass> $className
): LifetimeStrategy<TClass>;

class Container
{
    public function addContainer(
        ContainerInterface $container,
        LifetimeStrategyFactory $lifetimeStrategyFactory
    ): static;

    public function addSingletonContainer(
        ContainerInterface $container
    ): static;

    public function addTransientContainer(
        ContainerInterface $container
    ): static;
}

use Suhock\DependencyInjection\Context\ContextContainerFactory;
use Suhock\DependencyInjection\Context\Context;

/*
 * Strings or enums can be used as identifiers for contexts. To help ease
 * analysis and future refactorings, enums or string-typed constants are
 * recommended.
 */
enum MyContexts {
    case Default;
    case Admin;
}

$container = ContextContainerFactory::createForDefaultContainer();

/*
 * Build the Default context's container.
 */
$container->context(MyContexts::Default)
    ->addSingletonClass(MyApplication::class)
    ->addTransientImplementation(HttpClient::class, CurlHttpClient::class)
    ->addSingletonFactory(
        Settings::class,
        fn () => JsonSettings::fromFile('default.json')
    );

/*
 * Build the Admin context's container.
 */
$container->context(MyContexts::Admin)
    ->addSingletonFactory(
        Settings::class,
        fn () => JsonSettings::fromFile('admin.json')
    );

$container
    /*
     * Make Default the default, fallback context.
     * Stack: Default
     */
    ->push(MyContexts::Default)

    /*
     * Fetch the application and run it.
     */
    ->get(MyApplication::class)
    ->run();

/*
 * Stack: Default, Admin
 *
 * The container will search the Admin context then the Default context for
 * each dependency in the following class.
 */
#[Context(MyContexts::Admin)]
class AdminEditDefaultSettingsController {
    /*
     * Stack: Default, Admin
     *
     * Since no context is explicitly specified, the stack is inherited as-is
     * from the class.
     */
    public function __construct(
        /*
         * Stack: Default, Admin
         *
         * The container will resolve $settings using the Settings factory in
         * the Admin context, since Admin is at the top of the context stack.
         */
        private readonly Settings $settings,

        /*
         * Stack: Default, Admin, Default
         *
         * The container will resolve $defaultSettings using the Settings
         * factory in the Default context, since the attribute below will
         * place Default at the top of the context stack for this parameter.
         */
        #[Context(MyContexts::Default)]
        private readonly Settings $defaultSettings,

        /*
         * Stack: Default, Admin
         *
         * The container will first attempt to resolve $httpClient using the
         * Admin context. However, since HttpClient does not exist in the
         * the Admin context, the container will resolve it using the factory
         * in the Default context.
         */
        private readonly HttpClient $httpClient
    ) {
    }
}

callable<TResult of mixed> InjectableFunction([... mixed]): TResult;

class Injector
{
    public function call<TResult>(
        InjectableFunction<TResult> $function,
        array<int|string, mixed> $params = []
    ): TResult;
    
    public function instantiate<TClass>(
        string<TClass> $className,
        array<int|string, mixed> $params = []
    ): TClass;
}

use Suhock\DependencyInjection\Container;
use Suhock\DependencyInjection\ContainerInjector;

// Create the container and build it
$container = new Container();
// ... build the container ...

// Create an injector backed by the container
$injector = new ContainerInjector($container);

// Fetch the application router from the container
$router = $container->get(Router::class);

// Get the appropriate controller from the request path
$controller = $router->getControllerFromRequest($_SERVER);

// Call the controller's handleGet() method, injecting the indicated parameter
// values in addition to any additional dependencies in the parameter list.
$page = $injector
    ->call(
        $controller->handleGet(...),
        map_query_to_assoc_param_array($_GET)
    )

// Then, call the render() function on the return value.
$page->render();

class ProjectListController
{
    public function handleGet(
        // Parameter below will be injected from the container.
        ProjectRepository $projectRepository,

        // Parameter below will be populated from the value provided in the
        // $injector->call() parameter array. The default value will be used if
        // the key 'filter' is not present in the array.
        string $filter = ''
    ): PageInterface {
        $projects = $projectRepository->query($filter);

        return new ProjectListPage($projects);
    }
}

interface PageInterface {
    public function render(): void;
}

class MyApplication
{
    public function __construct(
        private readonly HttpClient $httpClient
    ) {
    }
}

class MyApplication
{
    public function __construct(
        private readonly ?HttpClient $httpClient
    ) {
    }
}

class MyApplication
{
    public function __construct(
        private readonly HttpClient $httpClient,
        private readonly string $homeUrl = '',
        private readonly int $timeout = 0,
        private readonly array $otherOptions = []
    ) {
    }
}

class MyApplication
{
    public function __construct(
        private readonly HttpClient|GopherClient|string $client
    ) {
    }
}

class MyApplication
{
    public function __construct(
        private readonly HttpClient&Serializable $httpClient
    ) {
    }
}

/* Do NOT do this! */

class MyApiCaller
{
    public function __construct(
        private readonly Container $container
    ) {
    }

    public function callApi(): HttpResponse
    {
        $httpClient = $this->container->get(HttpClient::class);
        return $httpClient->get('https://www.example.com/api');
    }
}

/* Do this instead! */

class MyApiCaller
{
    public function __construct(
        private readonly HttpClient $httpClient
    ) {
    }

    public function callApi(): HttpResponse
    {
        return $this->httpClient->get('https://www.example.com/api');
    }
}

/* Refactor this! */
public function myFragileBloatedFunction(...$args)
{
    // ...

    // $httpClient = new CurlHttpClient();
    // $logger = new FileLogger('myapp.log');
    // $httpClient->setLogger($logger);

    // Replaced the above duplicated construction logic with a call to the
    // container
    $httpClient = getAppContainer()->get(HttpClient::class);

    $result = $httpClient->get('https://www.example.com/api/' . $args[42]);
    // ...
}

/* Eliminate all references to the singleton container and remove this! */
function getAppContainer(): Container
{
    static $container;
    return $container ??= new Container();
}

/*
 * Build your container from the singleton container for now.
 * Replace with direct construction once refactoring is complete.
 */
$container = getAppContainer();
$container->addSingletonClass(MyApplication::class);
// ...