src/Extensions/Symfony/FormExtension.php
<?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\Parameter;
use Rade\DI\Definitions\Reference;
use Rade\DI\Definitions\Statement;
use Rade\DI\Definitions\TaggedLocator;
use Rade\DI\Extensions\AliasedInterface;
use Rade\DI\Extensions\BootExtensionInterface;
use Rade\DI\Extensions\ExtensionInterface;
use Rade\DI\Extensions\RequiredPackagesInterface;
use Rade\DI\Services\ServiceLocator;
use Flange\Extensions\Symfony\Form\HttpFoundationRequestHandler;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator;
use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory;
use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator;
use Symfony\Component\Form\Command\DebugCommand;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\ColorType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TransformationFailureExtension;
use Symfony\Component\Form\Extension\Csrf\Type\FormTypeCsrfExtension;
use Symfony\Component\Form\Extension\DependencyInjection\DependencyInjectionExtension;
use Symfony\Component\Form\Extension\HttpFoundation\Type\FormTypeHttpFoundationExtension;
use Symfony\Component\Form\Extension\Validator\Type\FormTypeValidatorExtension;
use Symfony\Component\Form\Extension\Validator\Type\RepeatedTypeValidatorExtension;
use Symfony\Component\Form\Extension\Validator\Type\SubmitTypeValidatorExtension;
use Symfony\Component\Form\Extension\Validator\Type\UploadValidatorExtension;
use Symfony\Component\Form\Extension\Validator\ValidatorTypeGuesser;
use Symfony\Component\Form\Form;
use Symfony\Component\Form\FormFactory;
use Symfony\Component\Form\FormRegistry;
use Symfony\Component\Form\RequestHandlerInterface;
use Symfony\Component\Form\ResolvedFormTypeFactory;
use Symfony\Component\Form\ResolvedFormTypeFactoryInterface;
use Symfony\Component\Form\Util\ServerParams;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use function Rade\DI\Loader\service;
/**
* Symfony component form extension.
*
* @author Divine Niiquaye Ibok <divineibok@gmail.com>
*/
class FormExtension implements AliasedInterface, BootExtensionInterface, ConfigurationInterface, ExtensionInterface, RequiredPackagesInterface
{
/**
* {@inheritdoc}
*/
public function getAlias(): string
{
return 'form';
}
/**
* {@inheritdoc}
*/
public function getConfigTreeBuilder(): TreeBuilder
{
$treeBuilder = new TreeBuilder(__CLASS__);
$treeBuilder->getRootNode()
->info('form configuration')
->canBeEnabled()
->children()
->arrayNode('csrf_protection')
->addDefaultsIfNotSet()
->beforeNormalization()
->ifTrue(fn ($v) => \is_bool($v))->then(fn ($v) => ['enabled' => $v])
->ifString()->then(fn ($v) => ['field_name' => $v, 'enabled' => true])
->end()
->children()
->booleanNode('enabled')->defaultTrue()->end()
->scalarNode('field_name')->defaultValue('_token')->end()
->end()
->end()
// to be deprecated in Symfony 6.1
->booleanNode('legacy_error_messages')->end()
->end()
;
return $treeBuilder;
}
/**
* {@inheritdoc}
*/
public function getRequiredPackages(): array
{
return [
Form::class => 'symfony/form',
PropertyAccess::class => 'symfony/property-access',
];
}
/**
* {@inheritdoc}
*/
public function register(Container $container, array $configs = []): void
{
if (!$configs['enabled']) {
return;
}
$definitions = [
'form.resolved_type_factory' => service(ResolvedFormTypeFactory::class)->typed(ResolvedFormTypeFactoryInterface::class),
'form.registry' => service(FormRegistry::class, [new TaggedLocator('form.extension')])->typed(),
'form.factory' => service(FormFactory::class)->typed(),
'form.extension' => service(DependencyInjectionExtension::class)->typed()->tag('form.extension'),
'form.choice_list_factory.default' => service(DefaultChoiceListFactory::class)->public(false),
'form.choice_list_factory.property_access' => service(PropertyAccessDecorator::class, [new Reference('form.choice_list_factory.default')])->public(false),
'form.choice_list_factory.cached' => service(CachingFactoryDecorator::class, [new Reference('form.choice_list_factory.property_access')])->typed(),
'form.server_params' => service(ServerParams::class, [new Reference('request_stack')])->typed(),
'form.type.form' => service(FormType::class)->public(false)->tag('form.type'),
'form.type.choice' => service(ChoiceType::class)->public(false)->tag('form.type'),
'form.type.color' => service(ColorType::class)->public(false)->tag('form.type'),
'form.type.file' => service(FileType::class)->public(false)->tag('form.type'),
'form.type_extension.repeated.validator' => service(RepeatedTypeValidatorExtension::class)->public(false)->tag('form.type_extension'),
'form.type_extension.submit.validator' => service(SubmitTypeValidatorExtension::class)->public(false)->tag('form.type_extension', ['extended-type' => SubmitType::class]),
'form.type_extension.http_foundation' => service(FormTypeHttpFoundationExtension::class)->public(false)->tag('form.type_extension'),
'form.type_extension.transformation_failure_handling' => service(TransformationFailureExtension::class)->public(false)->tag('form.type_extension', ['extended-type' => FormType::class]),
];
if ($container->hasExtension(ValidatorExtension::class)) {
$container->getExtensionBuilder()->modifyConfig(ValidatorExtension::class, ['enabled' => true], FrameworkExtension::CONFIG_CALL);
$definitions['form.type_guesser.validator'] = service(ValidatorTypeGuesser::class)->public(false)->tag('form.type_guesser');
$definitions['form.type_extension.validator'] = service(FormTypeValidatorExtension::class)->public(false)->tag('form.type_extension', ['extended-type' => FormType::class]);
} else {
$container->parameters['validator.translation_domain'] = 'validators';
}
if ($container->hasExtension(TranslatorExtension::class)) {
$definitions['form.type_extension.upload.validator'] = service(UploadValidatorExtension::class, [1 => new Parameter('validator.translation_domain')])->public(false)->tag('form.type_extension');
}
if ($container->typed(CsrfTokenManagerInterface::class)) {
$definitions['form.type_extension.csrf'] = service(FormTypeCsrfExtension::class)
->args([1 => $configs['csrf_protection']['enabled'], 2 => $configs['csrf_protection']['field_name']])
->tag('form.type_extension')
->public(false);
}
if ($container->has('console')) {
$definitions['console.command.form_debug'] = service(DebugCommand::class)->tag('console.command', 'debug:form');
}
$container->multiple($definitions);
$container->alias('form.choice_list_factory', 'form.choice_list_factory.cached');
}
/**
* {@inheritdoc}
*/
public function boot(Container $container): void
{
$container->definition('form.extension')->args([
$this->processFormTypes($container),
$this->processFormTypeExtensions($container),
$this->processFormTypeGuessers($container),
]);
if (!$container->typed(RequestHandlerInterface::class)) {
$container->autowire('form.type_extension.form.request_handler', new Definition(HttpFoundationRequestHandler::class));
}
}
private function processFormTypes(Container $container): Statement
{
// Get service locator argument
$servicesMap = [];
$namespaces = ['Symfony\Component\Form\Extension\Core\Type' => true];
// Builds an array with fully-qualified type class names as keys and service IDs as values
foreach ($container->tagged('form.type') as $serviceId => $tag) {
// Add form type service to the service locator
$serviceDefinition = $container->definition($serviceId);
$servicesMap[$formType = $serviceDefinition->getEntity()] = new Reference($serviceId);
$namespaces[\substr($formType, 0, \strrpos($formType, '\\'))] = true;
}
if ($container->has('console.command.form_debug')) {
$container->definition('console.command.form_debug')->args([1 => \array_keys($namespaces), 2 => \array_keys($servicesMap)]);
}
return new Statement(ServiceLocator::class, $servicesMap);
}
private function processFormTypeExtensions(Container $container): array
{
$typeExtensions = $typeExtensionsClasses = [];
foreach ($container->tagged('form.type_extension') as $serviceId => $tag) {
$serviceDefinition = $container->definition($serviceId);
$typeExtensionClass = $container->parameter($serviceDefinition->getEntity());
if (isset($tag['extended_type'])) {
$typeExtensions[$tag['extended_type']][] = new Reference($serviceId);
$typeExtensionsClasses[] = $typeExtensionClass;
} else {
$extendsTypes = false;
$typeExtensionsClasses[] = $typeExtensionClass;
foreach ($typeExtensionClass::getExtendedTypes() as $extendedType) {
$typeExtensions[$extendedType][] = new Reference($serviceId);
$extendsTypes = true;
}
if (!$extendsTypes) {
throw new \InvalidArgumentException(\sprintf('The getExtendedTypes() method for service "%s" does not return any extended types.', $serviceId));
}
}
}
if ($container->has('console.command.form_debug')) {
$container->definition('console.command.form_debug')->arg(3, $typeExtensionsClasses);
}
return $typeExtensions;
}
private function processFormTypeGuessers(Container $container): array
{
$guessers = $guessersClasses = [];
foreach ($container->tagged('form.type_guesser') as $serviceId => $tags) {
$guessers[] = new Reference($serviceId);
$serviceDefinition = $container->definition($serviceId);
$guessersClasses[] = $serviceDefinition->getEntity();
}
if ($container->has('console.command.form_debug')) {
$container->definition('console.command.form_debug')->arg(4, $guessersClasses);
}
return $guessers;
}
}