biurad/flange

View on GitHub
src/Extensions/Symfony/RateLimiterExtension.php

Summary

Maintainability
A
3 hrs
Test Coverage
<?php declare(strict_types=1);

/*
 * This file is part of Biurad opensource projects.
 *
 * @copyright 2019 Biurad Group (https://biurad.com/)
 * @license   https://opensource.org/licenses/BSD-3-Clause License
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Flange\Extensions\Symfony;

use Rade\DI\Container;
use Rade\DI\Definition;
use Rade\DI\Definitions\Reference;
use Rade\DI\Definitions\Statement;
use Rade\DI\Extensions\AliasedInterface;
use Rade\DI\Extensions\ExtensionInterface;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\RateLimiter\LimiterInterface;
use Symfony\Component\RateLimiter\RateLimiterFactory;
use Symfony\Component\RateLimiter\Storage\CacheStorage;

/**
 * Symfony component rate limiter extension.
 *
 * @author Divine Niiquaye Ibok <divineibok@gmail.com>
 */
class RateLimiterExtension implements AliasedInterface, ConfigurationInterface, ExtensionInterface
{
    /**
     * {@inheritdoc}
     */
    public function getAlias(): string
    {
        return 'rate_limiter';
    }

    /**
     * {@inheritdoc}
     */
    public function getConfigTreeBuilder(): TreeBuilder
    {
        $treeBuilder = new TreeBuilder(__CLASS__);

        $treeBuilder->getRootNode()
            ->info('Rate limiter configuration')
            ->canBeEnabled()
            ->fixXmlConfig('limiter')
            ->beforeNormalization()
                ->ifTrue(fn ($v) => \is_array($v) && !isset($v['limiters']) && !isset($v['limiter']))
                ->then(function (array $v) {
                    $newV = [
                        'enabled' => $v['enabled'] ?? true,
                    ];
                    unset($v['enabled']);

                    $newV['limiters'] = $v;

                    return $newV;
                })
            ->end()
            ->children()
                ->arrayNode('limiters')
                    ->useAttributeAsKey('name')
                    ->arrayPrototype()
                        ->children()
                            ->scalarNode('lock_factory')
                                ->info('The service ID of the lock factory used by this limiter (or null to disable locking)')
                                ->defaultValue('lock.factory')
                            ->end()
                            ->scalarNode('cache_pool')
                                ->info('The cache pool to use for storing the current limiter state')
                                ->defaultValue('cache.app')
                            ->end()
                            ->scalarNode('storage_service')
                                ->info('The service ID of a custom storage implementation, this precedes any configured "cache_pool"')
                                ->defaultNull()
                            ->end()
                            ->enumNode('policy')
                                ->info('The algorithm to be used by this limiter')
                                ->isRequired()
                                ->values(['fixed_window', 'token_bucket', 'sliding_window', 'no_limit'])
                            ->end()
                            ->integerNode('limit')
                                ->info('The maximum allowed hits in a fixed interval or burst')
                                ->isRequired()
                            ->end()
                            ->scalarNode('interval')
                                ->info('Configures the fixed interval if "policy" is set to "fixed_window" or "sliding_window". The value must be a number followed by "second", "minute", "hour", "day", "week" or "month" (or their plural equivalent).')
                            ->end()
                            ->arrayNode('rate')
                                ->info('Configures the fill rate if "policy" is set to "token_bucket"')
                                ->children()
                                    ->scalarNode('interval')
                                        ->info('Configures the rate interval. The value must be a number followed by "second", "minute", "hour", "day", "week" or "month" (or their plural equivalent).')
                                    ->end()
                                    ->integerNode('amount')->info('Amount of tokens to add each interval')->defaultValue(1)->end()
                                ->end()
                            ->end()
                        ->end()
                    ->end()
                ->end()
            ->end()
        ;

        return $treeBuilder;
    }

    /**
     * {@inheritdoc}
     */
    public function register(Container $container, array $configs = []): void
    {
        if (!$configs['enabled']) {
            return;
        }

        if (!\interface_exists(LimiterInterface::class)) {
            throw new \LogicException('Rate limiter support cannot be enabled as the RateLimiter component is not installed. Try running "composer require symfony/rate-limiter".');
        }
        $nLimiters = \count($configs['limiters']);

        foreach ($configs['limiters'] as $name => $limiterConfig) {
            // default configuration (when used by other DI extensions)
            $limiterConfig += ['lock_factory' => 'lock.factory', 'cache_pool' => 'cache.app'];
            $limiter = $container->set('limiter.'.$name, new Definition(RateLimiterFactory::class))->public(false);

            if (null !== $limiterConfig['lock_factory']) {
                if (!$container->hasExtension(LockExtension::class)) {
                    throw new \LogicException(\sprintf('Rate limiter "%s" requires the Lock component to be installed and configured.', $name));
                }

                $limiter->arg(2, new Reference($limiterConfig['lock_factory']));
            }
            unset($limiterConfig['lock_factory']);

            $limiter->arg(1, isset($limiterConfig['storage_service']) ? new Reference($limiterConfig['storage_service']) : new Statement(CacheStorage::class, [new Reference($limiterConfig['cache_pool'])]));
            unset($limiterConfig['storage_service'], $limiterConfig['cache_pool']);

            $limiterConfig['id'] = $name;
            $limiter->arg(0, $limiterConfig);

            if (1 === $nLimiters) {
                $limiter->typed(RateLimiterFactory::class);
            }
        }
    }
}