Skip to content

v3.12.0

Compare
Choose a tag to compare
@butschster butschster released this 29 Feb 17:26
· 34 commits to master since this release

What's Changed

New features

1. Improved container injectors

spiral/core Advanced Context Handling in Injector Implementations by @roxblnfk in #1041

This pull request presents a significant update to the injector system, focusing on the createInjection method of the Spiral\Core\Container\InjectorInterface. The key enhancement lies in the augmented ability of the injector to handle context more effectively.

Previously, the createInjection method accepted two parameters: the ReflectionClass object of the requested class and a context, which was limited to being either a string or null. This approach, while functional, offered limited flexibility in dynamically resolving dependencies based on the calling context.

The updated createInjection method can now accept an extended range of context types including Stringable|string|null, mixed, or ReflectionParameter|string|null. This broadening allows the injector to receive more detailed contextual information, enhancing its capability to make more informed decisions about which implementation to provide.

Now you can do something like this:

<?php

declare(strict_types=1);

namespace App\Application;

final class SomeService
{
    public function __construct(
        #[DatabaseDriver(name: 'mysql')]
        public DatabaseInterface $database,

        #[DatabaseDriver(name: 'sqlite')]
        public DatabaseInterface $database1,
    ) {
    }
}

And example of injector

<?php

declare(strict_types=1);

namespace App\Application;

use Spiral\Core\Container\InjectorInterface;

final class DatabaseInjector implements InjectorInterface
{
    public function createInjection(\ReflectionClass $class, \ReflectionParameter|null|string $context = null): object
    {
        $driver = $context?->getAttributes(DatabaseDriver::class)[0]?->newInstance()?->name ?? 'mysql';

        return match ($driver) {
            'sqlite' => new Sqlite(),
            'mysql' => new Mysql(),
            default => throw new \InvalidArgumentException('Invalid database driver'),
        };
    }
}

2. Added ability to suppress non-reportable exceptions

Add non-reportable exceptions by @msmakouz in #1044

The ability to exclude reporting of certain exceptions has been added. By default, Spiral\Http\Exception\ClientException, Spiral\Filters\Exception\ValidationException, and Spiral\Filters\Exception\AuthorizationException are ignored.

Exceptions can be excluded from the report in several different ways:

Attribute NonReportable

To exclude an exception from the report, you need to add the Spiral\Exceptions\Attribute\NonReportable attribute to the exception class.

use Spiral\Exceptions\Attribute\NonReportable;

#[NonReportable]
class AccessDeniedException extends \Exception
{
    // ...
}

Method dontReport

Invoke the dontReport method in the Spiral\Exceptions\ExceptionHandler class. This can be done using the bootloader.

use Spiral\Boot\Bootloader\Bootloader;
use Spiral\Exceptions\ExceptionHandler;

final class AppBootloader extends Bootloader
{
    public function init(ExceptionHandler $handler): void
    {
        $handler->dontReport(EntityNotFoundException::class);
    }
}

Overriding the property nonReportableExceptions

You can override the nonReportableExceptions property with predefined exceptions.

3. Better container scopes

This release marks a foundational shift in how we approach dependency management within our framework, setting the stage for the upcoming version 4.0. With these changes, we're not just tweaking the system; we're laying down the groundwork for more robust, efficient, and intuitive handling of dependencies in the long run. To ensure everyone can make the most out of these updates, we will be rolling out a series of tutorials aimed at helping you navigate through the new features and enhancements.

Context

The context is also extended on other container methods get() (see #1041)

Scopes

Default scope fix

If the container scope is not open, it is assumed by default that dependencies are resolved in the scope named root. Now when calling invoke(), make(), get(), the container will globally register itself with the root scope if no other scope was opened. Before this, the container resolved dependencies as if outside the scope.

Scoped Interface

The experimental ContainerScopeInterface has been removed. The method getBinder(?string $scope = null): BinderInterface has been moved to BinderInterface at the annotation level.

runScope method

The Container::runScoped() method (in the implementation) was additionally marked as @deprecated and will be removed when its use in tests is reduced to zero. Instead of the Container::runScoped(), you should now call the old Container::runScope(), but with passing the DTO Spiral\Core\Scope instead of the list of bindings.

$container->runScope(
    new Scope(name: 'auth', bindings: ['actor' => new Actor()]),
    function(ContainerInterface $container) {
        dump($container->get('actor'));
    },
);

Scope Proxy

Instead of the now removed ContainerScopeInterface::getCurrentContainer() method, the user is offered another way to get dependencies from the container of the current scope - a proxy.

The user can mark the dependency with a new attribute Spiral\Core\Attribute\Proxy.

Warning: The dependency must be defined by an interface.

When resolving dependencies, the container will create a proxy object that implements the specified interface. When calling the interface method, the proxy object will get the container of the current scope, request the dependency from it using its interface, and start the necessary method.

final class Service  
{
    public function __construct(  
        #[Proxy] public LoggerInterface $logger,  
    ) {  
    }

    public function doAction() {
        // Equals to
        // $container->getCurrentContainer()->get(LoggerInterface::class)->log('foo')
        $this->logger->log('foo'); 
    }
}

Important nuances:

  • The proxy refers to the active scope of the container, regardless of the scope in which the proxy object was created.
  • Each call to the proxy method pulls the container. If there are many calls within the method, you should consider making a proxy for the container
    // class
    function __construct(
        #[Proxy] private Dependency $dep,
        #[Proxy] private ContainerInterface $container,
    ) {}
    function handle() {
        // There are four calls to the container under the hood.
        $this->dep->foo();
        $this->dep->bar();
        $this->dep->baz();
        $this->dep->red();
        
        // Only two calls to the container and caching the value in a variable
        // The first call - getting the container through the proxy
        // The second - explicit retrieval of the dependency from the container
        $dep = $this->container->get(Dependency::class);
        $dep->foo();
        $dep->bar();
        $dep->baz();
        $dep->red();
    }
  • The proxied interface should not contain a constructor signature (although this sometimes happens).
  • Calls to methods outside the interface will not be proxied. This option is possible in principle, but it is disabled. If it is absolutely necessary, we will consider whether to enable it.
  • The destructor method call will not be proxied.

Proxy

Added the ability to bind an interface as a proxy using the Spiral\Core\Config\Proxy configuration. This is useful in cases where a service needs to be used within a specific scope but must be accessible within the container for other services in root or other scopes (so that a service requiring the dependency can be successfully created and used when needed in the correct scope).

use Spiral\Boot\Bootloader\Bootloader;
use Spiral\Core\BinderInterface;
use Spiral\Core\Config\Proxy;
use Spiral\Framework\ScopeName;
use Spiral\Http\PaginationFactory;
use Spiral\Pagination\PaginationProviderInterface;

final class PaginationBootloader extends Bootloader
{
    public function __construct(
        private readonly BinderInterface $binder,
    ) {
    }
    
    public function defineSingletons(): array
    {
        $this->binder
            ->getBinder(ScopeName::Http)
            ->bindSingleton(PaginationProviderInterface::class, PaginationFactory::class);
        
        $this->binder->bind(
            PaginationProviderInterface::class,
            new Proxy(PaginationProviderInterface::class, true)  // <-------
        );

        return [];
    }
}

DeprecationProxy

Similar to Proxy, but also allows outputting a deprecation message when attempting to retrieve a dependency from the container. In the example below, we use two bindings, one in scope and one out of scope with Spiral\Core\Config\DeprecationProxy. When requesting the interface in scope, we will receive the service, and when requesting it out of scope, we will receive the service and a deprecation message.

use Spiral\Boot\Bootloader\Bootloader;
use Spiral\Core\BinderInterface;
use Spiral\Core\Config\DeprecationProxy;
use Spiral\Framework\ScopeName;
use Spiral\Http\PaginationFactory;
use Spiral\Pagination\PaginationProviderInterface;

final class PaginationBootloader extends Bootloader
{
    public function __construct(
        private readonly BinderInterface $binder,
    ) {
    }

    public function defineSingletons(): array
    {
        $this->binder
            ->getBinder(ScopeName::Http)
            ->bindSingleton(PaginationProviderInterface::class, PaginationFactory::class);

        $this->binder->bind(
            PaginationProviderInterface::class,
            new DeprecationProxy(PaginationProviderInterface::class, true, ScopeName::Http, '4.0') // <----------
        );

        return [];
    }
}

DispatcherScope

Added the ability to specify the scope name for the dispatcher using the Spiral\Attribute\DispatcherScope attribute.

use Spiral\Attribute\DispatcherScope;
use Spiral\Boot\DispatcherInterface;

#[DispatcherScope(scope: 'console')]
final class ConsoleDispatcher implements DispatcherInterface
{
    // ...
}
Registration of dispatchers

The registration of dispatchers has been changed. The accepted type in the addDispatcher method of the Spiral\Boot\AbstractKernel class has been extended from DispatcherInterface to string|DispatcherInterface. Before these changes, the method accepted a created DispatcherInterface object, now it can accept a class name string or an object. In version 4.0, the DispatcherInterface type will be removed. When passing an object, only its class name will be saved. And when using the dispatcher, its object will be created anew.

Example with ConsoleDispatcher:

public function init(AbstractKernel $kernel): void
{
    $kernel->bootstrapped(static function (AbstractKernel $kernel): void {
        $kernel->addDispatcher(ConsoleDispatcher::class);
    });
}
Using dispatchers

The dispatchers are now created in their own scope and receive dependencies that are specified in this scope. But due to the need to check whether the dispatcher can handle the request or not before creating the dispatcher object, the canServe method in dispatchers must be static:

public static function canServe(EnvironmentInterface $env): bool
{
    return (PHP_SAPI === 'cli' && $env->get('RR_MODE') === null);
}

This method has been removed from the Spiral\Boot\DispatcherInterface, for backward compatibility it can be non-static, as it was before (then an object will be created for its call) or static and accept Spiral\Boot\EnvironmentInterface.

4. Added scaffolder:info console command

Adds scaffolder:info console command by @butschster in #1068

Now you can list available commands.

image

Other

  • Added support of monolog/monolog v3.x by @msmakouz in #1049
  • Added support of cocur/slugify 4.x by @msmakouz in #1048
  • Added support of league/flysystem v3.x by @msmakouz in #1050
  • Removed doctrine/annotations by @msmakouz in #1059
  • spiral/translator Added a check for the existence of a message by @msmakouz in #1062
  • spiral/translator Fixed getLocaleDirectory method by @msmakouz in #1075
  • spiral/core Fixed check for the existence of a binding in parent scopes using Conainer::has by @msmakouz in #1065
  • spiral/router Updated route:list command for customization route class by @iAvenger01 in #1070
  • [spiral/core Added the ability to configure container via options by @msmakouz in https://github.com//pull/1082

New Contributors

Full Changelog: 3.11.1...3.12.0