Skip to content

Releases: spiral/framework

3.14.6

22 Oct 21:58
Compare
Choose a tag to compare

What's Changed

  • Fix ServerRequestInterface resolving in HTTP scope by @roxblnfk in #1160
  • Inject optional EventDispatcher into CacheManager by @MartkCz in #1157

New Contributors

Full Changelog: 3.14.5...3.14.6

3.14.5

30 Sep 19:10
Compare
Choose a tag to compare

What's Changed

New Contributors

Full Changelog: 3.14.4...3.14.5

3.14.4

23 Sep 09:36
Compare
Choose a tag to compare

What's Changed

  • Container scope related fixes by @roxblnfk in #1149:
    • [spiral/router] Router now uses proxied container to create middlewares in a right scope.
    • [spiral/router] Better binding for the interceptor handler.
    • DebugBootloader now uses a Factory Proxy to resolve collectors.
      Unresolved collectors don't break state populating flow.

Full Changelog: 3.14.3...3.14.4

3.14.3

11 Sep 17:45
Compare
Choose a tag to compare

What's Changed

  • Fix GuardScope to operate within Container Scopes by @roxblnfk in #1146
  • Fix Stempler classes to operate within Container Scopes by @roxblnfk in #1147

Full Changelog: 3.14.2...3.14.3

3.14.2

10 Sep 17:00
0c45119
Compare
Choose a tag to compare

What's Changed

Fixes:

  • Fix concurrent writing and reading on workers boot by @wapmorgan in #1137
  • Fix Container Proxy recursion in ContainerScope by @roxblnfk in #1145

Code quality:

New Contributors

Full Changelog: 3.14.1...3.14.2

3.14.1

04 Sep 10:58
Compare
Choose a tag to compare

What's Changed

Fixes

  • Router: fix fallback handler in AbstractTarget by @roxblnfk in #1132
  • Added compatibility constraints with roadrunner-bridge < 3.7 and sapi-bridge < 1.1

Code quality

Full Changelog: 3.14.0...3.14.1

3.14.0

03 Sep 16:53
Compare
Choose a tag to compare

Warning

If you are using spiral/roadrunner-bridge, you need to update it to version ^3.7 or ^4.0.

New Interceptors

The HMVC package has been deprecated. It has been replaced by the new spiral/interceptors package, where we have reworked the interceptors. The basic principle remains the same, but the interface is now more understandable and convenient.

InterceptorInterface

In the old CoreInterceptorInterface, the $controller and $action parameters caused confusion by creating a false association with HTTP controllers. However, interceptors are not tied to HTTP and are used universally. Now, instead of $controller and $action, we use the Target definition, which can point to more than just class methods.

The $parameters parameter, which is a list of arguments, has now been moved to the CallContextInterface invocation context.

/** @deprecated Use InterceptorInterface instead */
interface CoreInterceptorInterface
{
    public function process(string $controller, string $action, array $parameters, CoreInterface $core): mixed;
}

interface InterceptorInterface
{
    public function intercept(CallContextInterface $context, HandlerInterface $handler): mixed;
}

CallContextInterface

The CallContextInterface invocation context contains all the information about the call:

  • Target β€” the definition of the call target.
  • Arguments β€” the list of arguments for the call.
  • Attributes β€” additional context that can be used to pass data between interceptors.

Note

CallContextInterface is immutable.

TargetInterface

TargetInterface defines the target whose call we want to intercept.

If you need to replace the Target in the interceptor chain, use the static methods of the \Spiral\Interceptors\Context\Target class to create a new Target.

The basic set includes several types of Target:

  • Target::fromReflectionMethod(ReflectionFunctionAbstract $reflection, class-string|object $classOrObject)
    Creates a Target from a method reflection. The second argument is mandatory because the method reflection may refer to a parent class.
  • Target::fromReflectionFunction(\ReflectionFunction $reflection, array $path = [])
    Creates a Target from a function reflection.
  • Target::fromClosure(\Closure $closure, array $path = [])
    Creates a Target from a closure. Use PHP 8 syntax for better clarity:
    $target = Target::fromClosure($this->someAction(...));
  • Target::fromPathString(string $path, string $delimiter = '.') and
    Target::fromPathArray(array $path, string $delimiter = '.')
    Creates a Target from a path string or array. In the first case, the path is split by the delimiter; in the second, the delimiter is used when converting the Target to a string. This type of Target without an explicit handler is used for RPC endpoints or message queue dispatching.
  • Target::fromPair(string|object $controller, string $action)
    An alternative way to create a Target from a controller and method, as in the old interface. The method will automatically determine how the Target should be created.

Compatibility

Spiral 3.x will work as expected with both old and new interceptors. However, new interceptors should be created based on the new interface.

In Spiral 4.x, support for old interceptors will be disabled. You will likely be able to restore it by including the spiral/hmvc package.

Building an Interceptor Chain

If you need to manually build an interceptor chain, use \Spiral\Interceptors\PipelineBuilderInterface.

In Spiral v3, two implementations are provided:

  • \Spiral\Interceptors\PipelineBuilder β€” an implementation for new interceptors only.
  • \Spiral\Core\CompatiblePipelineBuilder β€” an implementation from the spiral/hmvc package that supports both old and new interceptors simultaneously.

Note

In Spiral 3.14, the implementation for PipelineBuilderInterface is not defined in the container by default.
CompatiblePipelineBuilder is used in Spiral v3 services as a fallback implementation.
If you define your own implementation, it will be used instead of the fallback implementation in all framework pipelines.

At the end of the interceptor chain, there should always be a \Spiral\Interceptors\HandlerInterface, which will be called if the interceptor chain does not terminate with a result or exception.

The spiral/interceptors package provides several basic handlers:

  • \Spiral\Interceptors\Handler\CallableHandler β€” simply calls the callable from the Target "as is".
  • \Spiral\Interceptors\Handler\AutowireHandler β€” calls a method or function, resolving missing arguments using the container.
use Spiral\Core\CompatiblePipelineBuilder;
use Spiral\Interceptors\Context\CallContext;
use Spiral\Interceptors\Context\Target;
use Spiral\Interceptors\Handler\CallableHandler;

$interceptors = [
    new MyInterceptor(),
    new MySecondInterceptor(),
    new MyThirdInterceptor(),
];

$pipeline = (new CompatiblePipelineBuilder())
    ->withInterceptors(...$interceptors)
    ->build(handler: new CallableHandler());

$pipeline->handle(new CallContext(
    target: Target::fromPair($controller, $action),
    arguments: $arguments,
    attributes: $attributes,
));

Pull Requests

Container Scopes

Container Scopes are integrated even deeper.
In Spiral, each type of worker is handled by a separate dispatcher.
Each dispatcher has its own scope, which can be used to limit the set of services available to the worker.

During request processing, such as HTTP, the context (ServerRequestInterface) passes through a middleware pipeline.
At the very end, when the middleware has finished processing and the controller has not yet been called, there is a moment when the request context is finally prepared.
At this moment, the contextual container scope (in our case, http-request) is opened, and the ServerRequestInterface is placed in the container.
In this scope, interceptors come into play, after which the controller is executed.

As before, you can additionally open scopes in middleware, interceptors, or the business layer, for example, to limit the authorization context in a multi-tenant application.

You can view the names of dispatcher scopes and their contexts in the enum \Spiral\Framework\Spiral.

Pull Requests

Fixes

  • Fix psalm issue: remove internal annotation from LoggerTrait::$logger by @gam6itko in #1118
  • Fix psalm issues related to TracerFactoryInterface by @gam6itko in #1119
  • Fix error when a file from the stacktrace doesn't exist by @roxblnfk in #1114
  • Fix an exception message by @msmakouz in #1115
  • Fix OTEL: cast request URI to string for http.url trace attribute by @devnev in #1126
  • Fix psalm issues about Spiral\Queue\HandlerInterface::handle() by @gam6itko in #1120
  • Fix funding links by @roxblnfk in #1123

New Contributors

Full Changelog: 3.13.0...3.14.0

v3.13.0

22 May 18:33
Compare
Choose a tag to compare

New features

1. Introduced LoggerChannel attribute

We are excited to introduce a new feature that enhances the flexibility of the logging component. With the new LoggerChannel attribute, developers can now specify the logger channel directly in the code.

Example Usage:

class SomeService
{
	public function __construct(
	    // Logger with channel `roadrunner` will be injected
		#[LoggerChannel('roadrunner')] public LoggerInterface $logger
	){}
}

This feature allows for better organization and clarity in logging, helping you maintain and debug your application more efficiently.

by @roxblnfk in #1102

2. Added an ability additionally to scan parent classes.

With this update, you can now scan for attributes in parent classes, making your class discovery process more comprehensive and efficient.

Why This Matters

Previously, the tokenizer could only listen to classes where attributes were found. This limitation did not allow for the automatic (convenient) detection of classes by parent attributes and the effective use of the tokenizer cache. With this update, it will also listen to interfaces that the class with the attribute implements and the classes that it extends. This new feature leverages the full power of the tokenizer without the need to scan all classes and handle them manually, ensuring a more efficient and thorough attribute detection process.

Here is a practical example of how to use this feature:

use Spiral\Tokenizer\Attribute\TargetAttribute;

#[TargetAttribute(attribute: MyAttribute::class, scanParents: true)]
class MyListener implements TokenizationListenerInterface
{
    public function listen(\ReflectionClass $class): void
    {
        // Your logic here
    }

    public function finalize(): void
    {
        // Your logic here
    }
}

by @roxblnfk in #1110

Other

  • [spiral/queue] Added an ability to pass headers to the job handler using SyncDriver by @msmakouz in #1107
  • [spiral/router] Added HEAD and OPTIONS HTTP methods to route:list command by @tairau in #1109
  • [spiral/stempler] Fixed bug in a directive parser by @butschster in #1098
  • [spiral/core] Container Proxy classes are generated now with the mixed type in the methods parameters by @msmakouz in #1092
  • [spiral/core] Optimized the StateBinder::hasInjector() method by @anoxia in #1105
  • [spiral/tokenizer] Fixed registration tokenizer scopes via TokenizerBootloader by @butschster in #1093

Full Changelog: 3.12.0...3.13.0

v3.12.0

29 Feb 17:26
Compare
Choose a tag to compare

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(Sco...
Read more

v3.11.1

29 Dec 10:54
Compare
Choose a tag to compare

What's Changed

Full Changelog: 3.11.0...3.11.1