src/DependencyInjection/SilverbackApiComponentsExtension.php
<?php
/*
* This file is part of the Silverback API Components Bundle Project
*
* (c) Daniel West <daniel@silverback.is>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Silverback\ApiComponentsBundle\DependencyInjection;
use Ramsey\Uuid\Doctrine\UuidType;
use Silverback\ApiComponentsBundle\AttributeReader\UploadableAttributeReader;
use Silverback\ApiComponentsBundle\Doctrine\Extension\ORM\RoutableExtension;
use Silverback\ApiComponentsBundle\Doctrine\Extension\ORM\RouteExtension;
use Silverback\ApiComponentsBundle\Doctrine\Extension\ORM\TablePrefixExtension;
use Silverback\ApiComponentsBundle\Event\FormSuccessEvent;
use Silverback\ApiComponentsBundle\EventListener\Form\FormSuccessEventListenerInterface;
use Silverback\ApiComponentsBundle\Exception\ApiPlatformAuthenticationException;
use Silverback\ApiComponentsBundle\Exception\UnparseableRequestHeaderException;
use Silverback\ApiComponentsBundle\Exception\UserDisabledException;
use Silverback\ApiComponentsBundle\Factory\Uploadable\MediaObjectFactory;
use Silverback\ApiComponentsBundle\Factory\User\Mailer\ChangeEmailConfirmationEmailFactory;
use Silverback\ApiComponentsBundle\Factory\User\Mailer\PasswordChangedEmailFactory;
use Silverback\ApiComponentsBundle\Factory\User\Mailer\PasswordResetEmailFactory;
use Silverback\ApiComponentsBundle\Factory\User\Mailer\UserEnabledEmailFactory;
use Silverback\ApiComponentsBundle\Factory\User\Mailer\UsernameChangedEmailFactory;
use Silverback\ApiComponentsBundle\Factory\User\Mailer\VerifyEmailFactory;
use Silverback\ApiComponentsBundle\Factory\User\Mailer\WelcomeEmailFactory;
use Silverback\ApiComponentsBundle\Factory\User\UserFactory;
use Silverback\ApiComponentsBundle\Form\FormTypeInterface;
use Silverback\ApiComponentsBundle\Form\Type\User\ChangePasswordType;
use Silverback\ApiComponentsBundle\Form\Type\User\NewEmailAddressType;
use Silverback\ApiComponentsBundle\Form\Type\User\PasswordUpdateType;
use Silverback\ApiComponentsBundle\Form\Type\User\UserRegisterType;
use Silverback\ApiComponentsBundle\Helper\Publishable\PublishableStatusChecker;
use Silverback\ApiComponentsBundle\Helper\Uploadable\UploadableFileManager;
use Silverback\ApiComponentsBundle\Helper\User\UserDataProcessor;
use Silverback\ApiComponentsBundle\Helper\User\UserMailer;
use Silverback\ApiComponentsBundle\HttpCache\ResourceChangedPropagatorInterface;
use Silverback\ApiComponentsBundle\Mercure\MercureAuthorization;
use Silverback\ApiComponentsBundle\Repository\Core\RefreshTokenRepository;
use Silverback\ApiComponentsBundle\Repository\User\UserRepositoryInterface;
use Silverback\ApiComponentsBundle\Security\UserChecker;
use Silverback\ApiComponentsBundle\Security\Voter\RoutableVoter;
use Silverback\ApiComponentsBundle\Security\Voter\RouteVoter;
use Silverback\ApiComponentsBundle\Serializer\Normalizer\MetadataNormalizer;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\Security\Http\Event\LogoutEvent;
/**
* @author Daniel West <daniel@silverback.is>
*/
class SilverbackApiComponentsExtension extends Extension implements PrependExtensionInterface
{
/**
* @throws \Exception
*/
public function load(array $configs, ContainerBuilder $container): void
{
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
$this->loadServiceConfig($container);
$definition = $container->getDefinition(TablePrefixExtension::class);
$definition->setArgument('$prefix', $config['table_prefix']);
$definition = $container->getDefinition(UserRepositoryInterface::class);
$definition->setArgument('$entityClass', $config['user']['class_name']);
$definition->setArgument('$passwordRequestTimeout', $config['user']['password_reset']['request_timeout_seconds']);
$definition->setArgument('$newEmailConfirmTimeout', $config['user']['new_email_confirmation']['request_timeout_seconds']);
$cookieProvider = new Reference('lexik_jwt_authentication.cookie_provider.' . $config['refresh_token']['cookie_name']);
$definition = $container->getDefinition('silverback.security.jwt_event_listener');
$definition->setArgument('$cookieProvider', $cookieProvider);
$container->setParameter('silverback.api_components.refresh_token.ttl', (int) $config['refresh_token']['ttl']);
if (!empty($config['refresh_token']['options'])) {
$definition = $container->getDefinition($config['refresh_token']['handler_id']);
$definition->setArgument('$options', $config['refresh_token']['options']);
}
if ('silverback.api_components.refresh_token.storage.doctrine' === $config['refresh_token']['handler_id']) {
$container
->register(RefreshTokenRepository::class)
->setArguments([new Reference('doctrine'), $config['refresh_token']['options']['class']])
->addTag('doctrine.repository_service');
}
$definition = $container->getDefinition('silverback.command.refresh_tokens_expire');
$definition->setArgument('$storage', new Reference($config['refresh_token']['handler_id']));
if (class_exists(LogoutEvent::class)) {
$definition = $container->getDefinition('silverback.security.logout_listener');
$definition->setArgument('$storage', new Reference($config['refresh_token']['handler_id']));
$definition->setArgument('$cookieProvider', $cookieProvider);
} else {
$definition = $container->getDefinition('silverback.security.logout_handler');
$definition->setArgument('$storage', new Reference($config['refresh_token']['handler_id']));
$definition->setArgument('$cookieProvider', $cookieProvider);
}
$definition = $container->getDefinition('silverback.security.jwt_clear_token_listener');
$definition->setArgument('$cookieProvider', $cookieProvider);
$definition = $container->getDefinition('silverback.security.jwt_manager');
$definition->setArgument('$userProvider', new Reference(sprintf('security.user.provider.concrete.%s', $config['refresh_token']['database_user_provider'])));
$definition->setArgument('$storage', new Reference($config['refresh_token']['handler_id']));
$definition = $container->getDefinition(PublishableStatusChecker::class);
$definition->setArgument('$permission', $config['publishable']['permission']);
$definition = $container->getDefinition(MetadataNormalizer::class);
$definition->setArgument('$metadataKey', $config['metadata_key']);
$this->setEmailVerificationArguments($container, $config['user']['email_verification'], $config['user']['password_reset']['repeat_ttl_seconds']);
$this->setUserClassArguments($container, $config['user']['class_name']);
$this->setMailerServiceArguments($container, $config);
$imagineEnabled = $container->getParameter('api_components.imagine_enabled');
$definition = $container->getDefinition(UploadableAttributeReader::class);
$definition->setArgument('$imagineBundleEnabled', $imagineEnabled);
if ($imagineEnabled) {
$definition = $container->getDefinition(UploadableFileManager::class);
$definition->setArgument('$filterService', new Reference('liip_imagine.service.filter'));
$definition->setArgument('$imagineCacheManager', new Reference('liip_imagine.cache.manager'));
$definition = $container->getDefinition(MediaObjectFactory::class);
$definition->setArgument('$filterService', new Reference('liip_imagine.service.filter'));
}
$definition = $container->getDefinition(RouteExtension::class);
$definition->setArgument('$config', $config['route_security']);
$definition = $container->getDefinition(RouteVoter::class);
$definition->setArgument('$config', $config['route_security']);
$definition = $container->getDefinition(RoutableExtension::class);
$definition->setArgument('$securityStr', $config['routable_security']);
$definition = $container->getDefinition(RoutableVoter::class);
$definition->setArgument('$securityStr', $config['routable_security']);
$definition = $container->getDefinition(MercureAuthorization::class);
$definition->setArgument('$cookieSameSite', $config['mercure']['cookie']['samesite']);
$definition->setArgument('$hubName', $config['mercure']['hub_name']);
}
private function setEmailVerificationArguments(ContainerBuilder $container, array $emailVerificationConfig, int $passwordRepeatTtl): void
{
$definition = $container->getDefinition(UserChecker::class);
$definition->setArgument('$denyUnverifiedLogin', $emailVerificationConfig['deny_unverified_login']);
$definition = $container->getDefinition(UserDataProcessor::class);
$definition->setArgument('$initialEmailVerifiedState', $emailVerificationConfig['default_value']);
$definition->setArgument('$verifyEmailOnRegister', $emailVerificationConfig['verify_on_register']);
$definition->setArgument('$verifyEmailOnChange', $emailVerificationConfig['verify_on_change']);
$definition->setArgument('$tokenTtl', $passwordRepeatTtl);
}
private function setUserClassArguments(ContainerBuilder $container, string $userClass): void
{
$definition = $container->getDefinition(UserFactory::class);
$definition->setArgument('$userClass', $userClass);
$definition = $container->getDefinition(ChangePasswordType::class);
$definition->setArgument('$userClass', $userClass);
$definition = $container->getDefinition(NewEmailAddressType::class);
$definition->setArgument('$userClass', $userClass);
$definition = $container->getDefinition(UserRegisterType::class);
$definition->setArgument('$userClass', $userClass);
$definition = $container->getDefinition(PasswordUpdateType::class);
$definition->setArgument('$userClass', $userClass);
}
private function setMailerServiceArguments(ContainerBuilder $container, array $config): void
{
$definition = $container->getDefinition(UserMailer::class);
$definition->setArgument(
'$context',
[
'website_name' => $config['website_name'],
]
);
$mapping = [
PasswordChangedEmailFactory::class => 'password_changed',
UserEnabledEmailFactory::class => 'user_enabled',
UsernameChangedEmailFactory::class => 'username_changed',
WelcomeEmailFactory::class => 'welcome',
];
foreach ($mapping as $class => $key) {
$definition = $container->getDefinition($class);
$definition->setArgument('$subject', $config['user']['emails'][$key]['subject']);
$definition->setArgument('$enabled', $config['user']['emails'][$key]['enabled']);
if (WelcomeEmailFactory::class === $class) {
$definition->setArgument('$defaultRedirectPath', $config['user']['email_verification']['email']['default_redirect_path']);
$definition->setArgument('$redirectPathQueryKey', $config['user']['email_verification']['email']['redirect_path_query']);
}
}
$mapping = [
VerifyEmailFactory::class => 'email_verification',
ChangeEmailConfirmationEmailFactory::class => 'new_email_confirmation',
PasswordResetEmailFactory::class => 'password_reset',
];
foreach ($mapping as $class => $key) {
$definition = $container->getDefinition($class);
$definition->setArgument('$subject', $config['user'][$key]['email']['subject']);
$definition->setArgument('$enabled', true);
$definition->setArgument('$defaultRedirectPath', $config['user'][$key]['email']['default_redirect_path']);
$definition->setArgument('$redirectPathQueryKey', $config['user'][$key]['email']['redirect_path_query']);
}
}
/**
* @throws \Exception
*/
private function loadServiceConfig(ContainerBuilder $container): void
{
$container->registerForAutoconfiguration(FormTypeInterface::class)
->addTag('silverback_api_components.form_type');
$container->registerForAutoconfiguration(ResourceChangedPropagatorInterface::class)
->addTag('silverback_api_components.resource_changed_propagator');
$container->registerForAutoconfiguration(FormSuccessEventListenerInterface::class)
->addTag('kernel.event_listener', ['event' => FormSuccessEvent::class]);
$loader = new PhpFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
$loader->load('services.php');
$loader->load('services_normalizers.php');
$loader->load('services_doctrine_orm_http_cache_purger.php');
$loader->load('services_doctrine_orm_mercure_publisher.php');
}
public function prepend(ContainerBuilder $container): void
{
$configs = $container->getExtensionConfig($this->getAlias());
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
$this->prependApiPlatformConfig($container, $config);
$this->prependDoctrineConfiguration($container);
}
private function prependDoctrineConfiguration(ContainerBuilder $container): void
{
$container->prependExtensionConfig(
'doctrine',
[
'dbal' => [
'types' => [
'uuid' => UuidType::class,
],
],
]
);
}
private function appendMappingPaths(&$mappingPaths, $srcBase, $name): void
{
$configBasePath = $srcBase . '/Resources/config/api_platform';
$mappingPaths[] = sprintf('%s/%s/resource.xml', $configBasePath, $name);
$propertiesPath = sprintf('%s/%s/properties.xml', $configBasePath, $name);
if (file_exists($propertiesPath)) {
$mappingPaths[] = $propertiesPath;
}
}
private function prependApiPlatformConfig(ContainerBuilder $container, array $config): void
{
$srcBase = __DIR__ . '/..';
$mappingPaths = [$srcBase . '/Entity/Core'];
$this->appendMappingPaths($mappingPaths, $srcBase, 'uploadable');
$this->appendMappingPaths($mappingPaths, $srcBase, 'page_data_metadata');
$this->appendMappingPaths($mappingPaths, $srcBase, 'resource_metadata');
foreach ($config['enabled_components'] as $component => $is_enabled) {
if (true === $is_enabled) {
$this->appendMappingPaths($mappingPaths, $srcBase, $component);
}
}
$websiteName = $config['website_name'];
$container->prependExtensionConfig(
'api_platform',
[
'title' => $websiteName,
'description' => sprintf('API for %s', $websiteName),
'defaults' => [
'pagination_client_items_per_page' => true,
'pagination_maximum_items_per_page' => 100,
],
'collection' => [
'pagination' => [
'items_per_page_parameter_name' => 'perPage',
// 'client_items_per_page' => true,
// 'maximum_items_per_page' => 100,
],
],
'mapping' => [
'paths' => $mappingPaths,
],
'swagger' => [
'api_keys' => [
'X-AUTH-TOKEN' => [
'name' => 'X-AUTH-TOKEN',
'type' => 'header',
],
'Authorization' => [
'name' => 'Authorization',
'type' => 'header',
],
],
],
'exception_to_status' => [
UnparseableRequestHeaderException::class => 400,
ApiPlatformAuthenticationException::class => 401,
UserDisabledException::class => 401,
],
]
);
}
}