Skip to content

Releases: spiral/framework

v3.8.1

16 Aug 17:53
Compare
Choose a tag to compare

What's Changed

  • Adding the ability to configure the Attributes cache or disable the cache by @msmakouz in #968
  • Fixes Event Dispatcher rebinding by @butschster in #970
  • Fixes incorrect Concatenation of Route Pattern with Prefix in Route Group by @butschster in #967
  • Fixes loading ENV variables from dotenv in Kernel System section by @butschster in #972

Full Changelog: 3.8.0...3.8.1

v3.8.0

14 Aug 17:56
Compare
Choose a tag to compare

Improvements

1. Added instructions feature for scaffold generator

We are excited to announce a new feature, that enhances the scaffold generation process by providing clear and concise instructions on the next steps to be taken after generating various classes for your application.

With the Instructions feature, you can generate classes for the following components:

  • Bootloader
  • Command
  • Config
  • Controller
  • Request Filter
  • Job Handler
  • Middleware

Each generated class comes with a set of instructions on what to do next, ensuring a smooth and hassle-free development experience.

Let's take a look at an example:

php app.php create:command ScanSite

Will display

Declaration of 'ScanSiteCommand' has been successfully written into 'app/src/Endpoint/Console/ScanSiteCommand.php'.

Next steps:
1. Use the following command to run your command: 'php app.php scan:site'
2. Read more about user Commands in the documentation: https://spiral.dev/docs/console-commands

To experience the Instructions feature, simply use the scaffold generator commands for the respective components and follow the provided instructions for each generated class. The documentation links accompanying the instructions serve as a valuable resource for in-depth explanations and best practices.

By @butschster in #945

2. Adds the ability to specify a custom directory for specific declaration types in the [spiral/scaffolder] component

TheΒ spiral/scaffolderΒ component provides the ability to generate various classes, such as bootloaders, HTTP controllers, console commands, request filters, and Cycle ORM entities. These generated classes are stored in a directory specified in the configuration file. However, there was no way to specify a custom directory for specific declaration types.

This PR added a new configuration option to the declarations array that allows for the specification of a custom directory for a specific declaration type where to store a generated file.

UsingΒ directoryΒ in theΒ ScaffolderBootloader::addDeclarationΒ method:

class ScaffolderBootloader extends Bootloader
{
    //...
    
    public function boot(BaseScaffolderBootloader $scaffolder, DatabaseSeederConfig $config): void
    {
        $scaffolder->addDeclaration(Declaration\FactoryDeclaration::TYPE, [
            'namespace' => $config->getFactoriesNamespace(),
            'postfix' => 'Factory',
            'class' => Declaration\FactoryDeclaration::class,
            'baseNamespace' => 'Database',
            'directory' => $config->getFactoriesDirectory() // <=============
        ]);
    
        $scaffolder->addDeclaration(Declaration\SeederDeclaration::TYPE, [
            'namespace' => $config->getSeedersNamespace(),
            'postfix' => 'Seeder',
            'class' => Declaration\SeederDeclaration::class,
            'baseNamespace' => 'Database',
            'directory' => $config->getSeedersDirectory() // <=============
        ]);
    }
}

Via configuration fileΒ app/config/scaffolder.php

return [
    'directory' => directory('app') . 'src/',
    
    'declarations' => [
        Declaration\BootloaderDeclaration::TYPE => [
            'namespace' => 'Application\Bootloader',
            'postfix' => 'Bootloader',
            'class' => Declaration\BootloaderDeclaration::class,
            'directory' => directpry('app') . '/Infrastructure/Bootloader' // <=============
        ],
    ],
];

This allows users to customize the directory structure of their generated classes more easily, and improves the overall flexibility of the scaffolder component.

by @msmakouz in #925

3. Filters improvements

Value casters for Filter properties

Introducing Spiral\Filters\Model\Mapper\Mapper that sets values for filter properties. It utilizes a collection of casters, each designed to handle a specific type of value.

Default casters include:

  • Spiral\Filters\Model\Mapper\EnumCaster Allows the use ofΒ PHP enumsΒ in a filter properties. This caster verifies the property type as an enum and creates the necessary enumeration from the value passed to the filter.
<?php

declare(strict_types=1);

use Spiral\Filters\Attribute\Input\Post;
use Spiral\Filters\Model\Filter;
use App\User\Type;

final class RegisterUser extends Filter
{
    #[Post]
    public string $name;
    
    #[Post]
    public Type $status = Type::Admin;
    
    // ...
}
  • Spiral\Filters\Model\Mapper\UuidCaster Allows the use ofΒ ramsey/uuidΒ in a filter properties. This caster confirms the property type as UUID and constructs an UUID object from the string provided to the Filter.
<?php

declare(strict_types=1);

use Spiral\Filters\Attribute\Input\Post;
use Spiral\Filters\Model\Filter;
use Ramsey\Uuid\UuidInterface;

final class UpdateUser extends Filter
{
    #[Post]
    public UuidInterface $uuid;
    
    // ...
}
  • Spiral\Filters\Model\Mapper\DefaultCaster: Used when other casters are unable to assign a value. It sets the property value without any transformations.

Custom casters
Casters are extensible and can be created and added by users within the application. To achieve this, it's necessary to create a custom caster object, implement theΒ Spiral\Filters\Model\Mapper\CasterInterfaceΒ interface.

Let's take a look at an example:

<?php

declare(strict_types=1);

namespace App\Application\Filter\Caster;

use Spiral\Filters\Model\Mapper\CasterInterface;

final class BooleanCaster implements CasterInterface
{
    public function supports(\ReflectionNamedType $type): bool
    {
        return $type->isBuiltin() && $type->getName() === 'bool';
    }

    public function setValue(FilterInterface $filter, \ReflectionProperty $property, mixed $value): void
    {
        $property->setValue($filter, (bool) $value);
    }
}

And register it in theΒ Spiral\Filters\Model\Mapper\CasterRegistryInterfaceΒ viaΒ registerΒ method.

<?php

declare(strict_types=1);

namespace App\Application\Bootloader;

use Spiral\Filters\Model\Mapper\CasterRegistryInterface;
use Spiral\Boot\Bootloader\Bootloader;
use App\Application\Filter\Caster\BooleanCaster;

final class FilerBootloader extends Bootloader 
{
    public function init(CasterRegistryInterface $registry)
    {
        $registr->register(new BooleanCaster());
    }
}

by @msmakouz in #961

4. Added TokenStorageScope

This class can be used to access the specific implementation of the token storage that is used in the current container scope.

use Spiral\Auth\TokenStorageScope;

final class SomeController 
{
    public function index(TokenStorageScope $tokenStorage): string
    {
        $tokenStorage->load('some-id');
            
        $tokenStorage->create(['id' => 'some-id']);
            
        $tokenStorage->delete($token);
    }
}

by @msmakouz in #931

5. Container improvements

In this update, our primary focus has been on elevating the overall performance of the container

  1. We have successfully migrated a significant portion of the internal operations from runtime to configuration time.
  2. Furthermore, we've replaced the previous array-based structure that was utilized to store information about bindings within the container. The new approach involves the utilization of Data Transfer Objects (DTOs), which not only provides a more structured and organized manner of storing binding information but also offers developers a seamless way to configure container bindings using these objects.

These changes together make the container work faster and more efficiently while also using less memory. As a result, your applications should perform better overall.

Additionally,

To demonstrate the enhanced container functionality, here's an example showcasing the new binding configuration:

use Spiral\Core\Config\Factory;

$container->bind(LoggerInterface::class, new Factory(
    callable: static function() {
        return new Logger(....);
    }, 
    singleton: true
))

Read more about new binding here

Scopes

We also added a new container scope interface Spiral\Core\ContainerScopeInterface that can be used to run code withing isolated IoC scope.

Note
Use it instead of Spiral\Core\ScopeInterface, which is deprecated now.

A new interface provides also an ability to bind aliases in a specific scope

use Spiral\Core\ScopeInterface;

final class AppBootloader extends Bootloader
{
    public function init(ContainerScopeInterface $container)
    {
        // Bind into a specific scope
        $container->getBinder('http')->bind(...);

        
        // Bind into the current scope
        $container->getBinder()->bind(...);
    }
}

by @roxblnfk in #941 #935 #936

Singletons

In this release, we've introduced a robust safeguard to prevent accidental overwriting of existing singletons within the container. This new feature ensures a more controlled environment for managing your application's dependencies and provides greater clarity when redefining singletons. In previous versions of the container, users could override existing singleton definitions by redefining them with new configurations. While this flexibility allowed for dynamic changes, it also led to potential confusion and unexpected behavior. To ad...

Read more

v3.7.1

22 Apr 07:56
Compare
Choose a tag to compare

What's Changed

  • Fixing PromotedParameter in the Reactor by @msmakouz in #922
  • Fixed InputScope to allow retrieval of non-bag input sources by @butschster in #926

Full Changelog: 3.7.0...3.7.1

v3.7.0

14 Apr 07:58
Compare
Choose a tag to compare

New Features

1. Added the ability to push objects to a queue

Previously, only arrays could be pushed to the queue, but with this feature, developers can now push any other types, such as objects, strings, etc. Now you can use various serializers like Symfony Serializer, Valinor, and Protobuf to serialize and deserialize objects. For example, using a more efficient and compact serialization format like Protobuf can help reduce the size of the data being pushed to the queue, resulting in faster processing times and lower storage costs.

Note
Read more about jobs payload serialization on official documentation

by @msmakouz in #887

2. Added the ability to guess option mode in by property type console commands

This makes it easier for developers to define command options by allowing them to simply define the property type and the console command will automatically guess the appropriate mode for the option.

namespace App\Endpoint\Console;

use Spiral\Console\Attribute\AsCommand;
use Spiral\Console\Attribute\Option;
use Spiral\Console\Command;

#[AsCommand(name: 'create:user', description: 'Create a new user')]
class CreateUserCommand extends Command
{
    #[Option]
    private bool $active; // InputOption::VALUE_NEGATABLE

    #[Option]
    private int $age; // InputOption::VALUE_REQUIRED

    #[Option]
    private int $friends = 0; // InputOption::VALUE_OPTIONAL (property have a default value)

    #[Option]
    private ?string $address = null; // InputOption::VALUE_OPTIONAL (nullable property)

    #[Option]
    private array $groups; // InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY

    #[Option]
    private array $phones = []; // InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY

    #[Option]
    private ?array $emails; // InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY

    public function __invoke(): int
    {
        // ...
    }
}

Note
Read more about console commands on official documentation

by @msmakouz in #893

3. Added the ability to override Scaffolder config partially

Previously, if a developer wanted to customize the scaffolder configuration, they had to override the entire default configuration in the scaffolder.php config file.

With this feature, developers can configure only specific options for each declaration type.

return [
    'namespace' => 'App',

    'declarations' => [
        Declaration\BootloaderDeclaration::TYPE => [
            'namespace' => 'Application\Bootloader',
        ],
        Declaration\ConfigDeclaration::TYPE => [
            'namespace' => 'Application\Config',
        ],
        Declaration\ControllerDeclaration::TYPE => [
            'namespace' => 'Endpoint\Web',
        ],
        Declaration\MiddlewareDeclaration::TYPE => [
            'class' => Declaration\MiddlewareDeclaration::class,
            'namespace' => 'Application\Http\Middleware',
        ],
        Declaration\CommandDeclaration::TYPE => [
            'namespace' => 'Endpoint\Console',
        ],
        Declaration\JobHandlerDeclaration::TYPE => [
            'namespace' => 'Endpoint\Job',
            'postfix' => 'Job',
        ],
    ],
];

It allows developers to customize only the specific declaration types they need, without having to override the entire default declaration configuration. This can make the configuration process simpler and reduce the risk of errors.

Note
Read more about scaffolding on official documentation

by @msmakouz in #918

4. Refactored scaffolder commands

  • create:bootloader command now has an ability to create a domain bootloader with interceptors using the -d option.
  • create:command - command has been improved with several changes, such as adding the final keyword for class and using PHP attributes for command definition and console command declaration. The command now also has the ability to add arguments and options to the generated console command.
// Generate command with arguments and options 
php app.php create:command UserRegister -a username -a password -o isAdmin -d "Register a new user"

Will generate

<?php

declare(strict_types=1);

namespace App\Api\Cli\Command;

use Spiral\Console\Attribute\Argument;
use Spiral\Console\Attribute\AsCommand;
use Spiral\Console\Attribute\Option;
use Spiral\Console\Attribute\Question;
use Spiral\Console\Command;

#[AsCommand(name: 'user:register', description: 'Register a new user')]
final class UserRegisterCommand extends Command
{
    #[Argument(description: 'Argument description')]
    #[Question(question: 'What would you like to name the username argument?')]
    private string $username;

    #[Argument(description: 'Argument description')]
    #[Question(question: 'What would you like to name the password argument?')]
    private string $password;

    #[Option(description: 'Argument description')]
    private bool $isAdmin;

    public function __invoke(): int
    {
        // Put your command logic here
        $this->info('Command logic is not implemented yet');

        return self::SUCCESS;
    }
}
  • create:filter command has also been added for filters declaration, which allows developers to generate filters with validation rules.
php app.php create:filter CreateUser -p username:post -p tags:post:array -p ip:ip -p token:header -p status:query:int

Will generate

<?php

declare(strict_types=1);

namespace App\Api\Web\Filter;

use Spiral\Filters\Attribute\Input\Header;
use Spiral\Filters\Attribute\Input\Post;
use Spiral\Filters\Attribute\Input\Query;
use Spiral\Filters\Attribute\Input\RemoteAddress;
use Spiral\Filters\Model\Filter;

final class CreateUserFilter extends Filter
{
    #[Post(key: 'username')]
    public string $username;

    #[Post(key: 'tags')]
    public array $tags;

    #[RemoteAddress(key: 'ip')]
    public string $ip;

    #[Header(key: 'token')]
    public string $token;

    #[Query(key: 'status')]
    public int $status;
}

There is also an ability to use validator to validate filter

php app.php create:filter CreateUser -p ... -s

Will generate

<?php

declare(strict_types=1);

namespace App\Api\Web\Filter;

use Spiral\Filters\Attribute\Input\Header;
use Spiral\Filters\Attribute\Input\Post;
use Spiral\Filters\Attribute\Input\Query;
use Spiral\Filters\Attribute\Input\RemoteAddress;
use Spiral\Filters\Model\Filter;
use Spiral\Filters\Model\FilterDefinitionInterface;
use Spiral\Filters\Model\HasFilterDefinition;
use Spiral\Validator\FilterDefinition;

final class CreateUserFilter extends Filter implements HasFilterDefinition
{
    #[Post(key: 'username')]
    public string $username;

    #[Post(key: 'tags')]
    public array $tags;

    #[RemoteAddress(key: 'ip')]
    public string $ip;

    #[Header(key: 'token')]
    public string $token;

    #[Query(key: 'status')]
    public int $status;

    public function filterDefinition(): FilterDefinitionInterface
    {
        return new FilterDefinition(validationRules: [
            // Put your validation rules here
        ]);
    }
}

All these improvements can help to streamline the process of generating classes using the scaffolder component, making it easier and more efficient for developers to generate the necessary classes for their application.

Note
Read more about scaffolding on official documentation

by @butschster in #902

Improvements

Bug Fixes

  • Fixed problem with displaying console commands description when it was defined via DESCRIPTION constant by @msmakouz in #889
  • Fixed applying setters during validation filter objects by @msmakouz in #891
  • Fixed the problem with using named parameters in class located by a tokenizer by @butschster in #895
  • Fixed elapsed time calculation for LogTracer in spiral/telemetry component by @gam6itko in #919

v3.6.1

20 Feb 15:42
Compare
Choose a tag to compare

What's Changed

Full Changelog: 3.6.0...3.6.1

v3.6.0

16 Feb 21:23
Compare
Choose a tag to compare

Spiral Framework 3.6 has been released, introducing a range of new features, performance improvements, and bug fixes.

What's Changed

1. Container

One notable change is the introduction of a new and improved container, which offers isolated memory scopes for more fine-grained control over how dependencies are resolved. This is useful in situations where you want to ensure that certain dependencies are only used within a specific context and do not bleed over to other parts of the application.

PR: #870

Container scopes

The Container::scope() method is used to run a true isolated scope, and it takes a callable as its first argument. The callable is executed within the scope, and any bindings passed as the second argument will only be used within that scope.

Named scopes can also be used. This is useful for cases where you have multiple scopes with similar dependencies, but with some variations. Parallel named scopes are also allowed.

When a scope is finished callback execution, all the dependencies that were created in that scope will be destroyed, including singletons. This means that any instances of objects that were created within the scope will no longer be available.

Additionally, the container that was used to run the scope will also be destroyed, which means that any attempts to use the container after the scope has finished execution will result in an exception being thrown.

If you create a named scope using Container::scope() method, you can access and modify the default bindings for that scope by calling Container::getBinder(string $scope) method and using the returned Binder instance to make new bindings.

$container = new Container();
// Configure `root` scope bindings (the current instance)
$container->bindSingleton(Interface::class, Implementation::class);

// Configure `request` scope default bindings
// Prefer way to make many bindings
$binder = $container->getBinder('request');
$binder->bindSingleton(Interface::class, Implementation::class);
$binder->bind(Interface::class, factory(...));

New attributes

Here's an overview of the new attributes in Spiral's new container:

#[Singleton]: This attribute is used to mark a class as a singleton. This attribute can be used in addition to the traditional bindSingleton() method.

#[Scope(string $name)]: This attribute is used to set a scope in which a dependency can be resolved.

#[Finalize(string $method)]: This an experimental attribute is used to define a finalize method for a class. The finalize method will be called before the scope is being destroyed. This can be used to perform any necessary cleanup operations.

PHP 8.2 Fibers support

And also it's now fully compatible with PHP 8.2 Fibers. The static class \Spiral\Core\ContainerScope::getContainer()
will return correct container instance for the current fiber and scope.

2. Improved performance of Tokenizer component

Additionally, We improved application performance when utilizing tokenizer components. Specifically, the introduction of a TOKENIZER_CACHE_TARGETS environment variable now allows for tokenizer listener caching. Upon the initial application bootstrapping, all found classes for each tokenizer listener will be cached. Subsequent bootstrapping processes will then retrieve the cached data, leading to faster and more efficient performance.

PR: #878

3. Console commands base on attributes

Developers can now create console commands using attributes.

PR: #872

4

Furthermore, if required arguments are not provided when calling a console command, the framework will prompt users to input the missing arguments rather than displaying an error. This is particularly useful for scaffolder commands like php app.php create:controller, where developers previously needed to remember which arguments were required. With this new feature, all required arguments will be automatically prompted for, streamlining the development process.

console-prompt

When creating a console command, developers can use the Spiral\Console\Attribute\Question attribute to specify the question text that should be displayed when the command is run and a required argument is missing.

final class CreateUser extends Command
{
    #[Argument]
    #[Question(question: 'Provide the User Email')]
    private string $email;
}

4. Repeatable setters for filters

Another update is that the Setter attribute for the spiral/filters component is now repeatable. This means that it can be used multiple times on the same property, enabling more flexible and powerful filtering options.

class StoreUserFilter extends Filter
{
    #[Post]
    #[Setter(filter: 'strval')]
    #[Setter('ltrim', '-')]
    #[Setter('rtrim', ' ')]
    #[Setter('htmlspecialchars')]
    public string $name;
}

PR: #874

5. Cache key prefixes

Spiral also includes a new feature for cache storage aliases. Developers can now configure a key prefix for each alias, providing more granular control over caching options.

PR: #869

return [
    'aliases' => [
        'user-data' => [
            'storage' => 'in-memory',
            'prefix' => 'user_'
        ],
        'blog-data' => [
            'storage' => 'in-memory',
            'prefix' => 'blog_'
        ],
    ],
    'storages' => [
        'in-memory' => [
            'type' => 'roadrunner-local'
        ],
    ],
];

This allows developers to use the same cache storage for multiple purposes while still maintaining separate cache keys for each purpose. The prefix option can be used to differentiate cache keys and ensure that they do not overlap, improving the reliability and accuracy of caching in the application.

// Cache Manager $cache
$cache->storage('user-data')->set('data', 'foo');
$cache->storage('blog-data')->set('data', 'bar');

// Result
array:2 [
  "user_data" => array:2 [
    "value" => "foo"
    "timestamp" => 1677694480
  ]
  "blog_data" => array:2 [
    "value" => "bar"
    "timestamp" => 1677694480
  ]
]

6. Custom mail transport

Another new feature is the ability to register a custom mail transport factory via Bootloader. This can be useful in cases where developers want to use a non-standard mail transport with the SendIt component.

PR: #873

To register a new mail transport factory, developers can create a new Bootloader class

use Spiral\Boot\Bootloader\Bootloader;
use Spiral\SendIt\TransportRegistryInterface;
use Symfony\Component\Mailer\Transport\c;

class AppBootloader extends Bootloader 
{
    public function boot(TransportRegistryInterface $registry): void
    {
        $registry->registerTransport(new SendmailTransportFactory(...));
    }
}

Bug fixes

Spiral Framework 3.6 also includes several bug fixes to improve the reliability and stability of the framework.

  1. The first bug fix addresses a problem with the container factory where previously created objects were sometimes used instead of creating new objects with the provided arguments. This issue could cause unexpected behavior and was fixed.
$service = new Service();
$container->bindSingleton(Service::class, $service);

$service === $container->make(Service::class);                   // true
$service === $container->make(Service::class, ['foo' => 'bar']); // false

PR: #862

  1. The second bug fix resolves an error message that could occur in the Prototype component when updating a DOCComment. The error message Updating PrototypeTrait DOCComment... DOCComment is missing no longer appears.

PR: #865

  1. The third bug fix addresses a problem with validation nested filters. This issue could cause validation errors to be missed or incorrectly reported.

PR: #868


New Contributors

Full Changelog: 3.5.0...3.6.0

v3.5.0

23 Dec 14:11
Compare
Choose a tag to compare

What's Changed

  • Improved the exception trace output for both the plain and console renderers to provide more detailed information about previous exceptions. by @butschster in #853
  • Added named route patterns registry Spiral\Router\Registry\RoutePatternRegistryInterface to allow for easier management of route patterns. by @kastahov in #847
  • Made the Verbosity enum injectable to allow for easier customization and management of verbosity levels from env variable VERBOSITY_LEVEL. by @kastahov in #846
  • Deprecated Kernel constants and add new function defineSystemBootloaders to allow for more flexibility in defining system bootloaders. by @kastahov in #845

Full Changelog: 3.4.0...3.5.0

v3.4.0

08 Dec 18:41
Compare
Choose a tag to compare

What's Changed

  • Medium Impact Changes
    • [spiral/boot] Class Spiral\Boot\BootloadManager\BootloadManager is deprecated. Will be removed in version v4.0. #840
    • [spiral/stempler] Adds null locale processor to remove brackets [[ ... ]] when don't use Translator component by @butschster in #843
  • Other Features
    • [spiral/session] Added session handle with cache driver by @kastahov in #835
    • [spiral/router] Added routes with PATCH method into route:list command by @gam6itko in #839
    • [spiral/boot] Added Spiral\Boot\BootloadManager\InitializerInterface. This will allow changing the implementation
      of this interface by the developer by @msmakouz #840
    • [spiral/boot] Added Spiral\Boot\BootloadManager\StrategyBasedBootloadManager. It allows the implementation of a
      custom bootloaders loading strategy by @msmakouz #840
    • [spiral/boot] Added the ability to register application bootloaders via object instance or anonymous object.
    • [spiral/boot] Removed final from the Spiral\Boot\BootloadManager\Initializer class by @msmakouz #840
  • Bug Fixes
    • [spiral/views] Fixes problem with using view context with default value by @butschster in #842
    • [spiral/queue] Added Spiral\Telemetry\Bootloader\TelemetryBootloader dependency to QueueBootloader by @butschster in #837
    • [spiral/core] (PHP 8.2 support) Fixed problem with dynamic properties in Spiral\Core\Container by @msmakouz in #844

Full Changelog: 3.3.0...3.4.0

v3.3.0

17 Nov 16:27
Compare
Choose a tag to compare

What's Changed

  • New [spiral/telemetry] component and spiral/otel-bridge by @butschster in #816
  • Added Spiral\Auth\Middleware\Firewall\RedirectFirewall by @msmakouz in #827
  • Created TokenStorageProvider to able manage token storages by @kastahov in #826
  • Fixed error suppressing in the Spiral\Http\Middleware\ErrorHandlerMiddleware by @msmakouz in #833
  • Router improvements by @msmakouz in #831
  • Changed where Tokenizer listener events are called by @msmakouz in #825

New Contributors

Full Changelog: 3.2.0...3.3.0

v3.2.0

21 Oct 10:10
Compare
Choose a tag to compare

What's Changed

Full Changelog: 3.1.0...3.2.0