diff --git a/.github/workflows/recipe.yaml b/.github/workflows/recipe.yaml index 4c84937e..a6f10a8f 100644 --- a/.github/workflows/recipe.yaml +++ b/.github/workflows/recipe.yaml @@ -18,21 +18,8 @@ jobs: strategy: fail-fast: false matrix: - php: ['7.4', '8.0', '8.1'] - sylius: ['~1.8.0', '~1.9.0', '~1.10.0', '~1.11.0', '~1.12.0'] - exclude: - - php: 8.0 - sylius: '~1.8.0' - - php: 8.0 - sylius: '~1.9.0' - - php: 8.1 - sylius: '~1.8.0' - - php: 8.1 - sylius: '~1.9.0' - - php: 7.4 - sylius: '~1.11.0' - - php: 7.4 - sylius: '~1.12.0' + php: ['8.0', '8.1'] + sylius: ['~1.10.0', '~1.11.0', '~1.12.0'] steps: - name: Setup PHP diff --git a/.github/workflows/security.yaml b/.github/workflows/security.yaml index 46e47827..9acdf597 100644 --- a/.github/workflows/security.yaml +++ b/.github/workflows/security.yaml @@ -17,7 +17,7 @@ jobs: strategy: fail-fast: false matrix: - php: ['7.4', '8.0', '8.1'] + php: ['8.0', '8.1'] steps: - uses: actions/checkout@v3 diff --git a/README.md b/README.md index 4e10ceda..7abfe74e 100644 --- a/README.md +++ b/README.md @@ -217,7 +217,23 @@ You can distinguish the `Row` element and the `Column` element by their dotted b In this example, we will add a Google Maps element. -### Define your UiElement +With the Maker Bundle, you can create a new UiElement very easily: + +```bash +bin/console make:ui-element +``` + +Then you will have to answer some questions, or you can add arguments to the command to avoid the questions. + +```bash +bin/console make:ui-element app.google_maps "map pin" +``` + +Just add the translations! + +### Define your UiElement (for PHP < 8.1) + +**Tips:** If you are using PHP 8.1 or newer, you can use the `#[AsUiElement]` attribute to define your UiElement. You can skip this step. Define your UiElement in your configuration folder, let's say in `config/packages/monsieurbiz_sylius_richeditor_plugin.yaml` as example. @@ -273,6 +289,48 @@ class GoogleMapsType extends AbstractType } ``` +For PHP 8.1 and newer, you can use the `#[AsUiElement]` attribute to define your UiElement. For example: + +```php +=1.8 <1.13" + "sylius/sylius": ">=1.10 <1.13" }, "require-dev": { "behat/behat": "^3.6.1", @@ -43,7 +43,8 @@ "symfony/dotenv": "^4.4", "symfony/flex": "^1.7", "symfony/web-profiler-bundle": "^4.4", - "phpmd/phpmd": "@stable" + "phpmd/phpmd": "@stable", + "symfony/maker-bundle": "^1.39" }, "prefer-stable": true, "autoload": { diff --git a/dist/config/packages/monsieurbiz_sylius_rich_editor_plugin_custom.yaml b/dist/config/packages/monsieurbiz_sylius_rich_editor_plugin_custom.yaml index 3b7f2f4e..0009d7c7 100644 --- a/dist/config/packages/monsieurbiz_sylius_rich_editor_plugin_custom.yaml +++ b/dist/config/packages/monsieurbiz_sylius_rich_editor_plugin_custom.yaml @@ -4,17 +4,18 @@ monsieurbiz_sylius_richeditor: tags: [ html ] monsieurbiz.youtube: tags: [ youtube ] - app.google_maps: - title: 'app.ui_element.google_maps.title' - description: 'app.ui_element.google_maps.description' - icon: map pin - tags: [ map ] - classes: - form: App\Form\Type\UiElement\GoogleMapsType - ui_element: App\UiElement\GoogleMapsUiElement - templates: - admin_render: '/Admin/UiElement/google_maps.html.twig' - front_render: '/Shop/UiElement/google_maps.html.twig' +# Example without PHP Attributes or for PHP version < 8.1 +# app.google_maps: +# title: 'app.ui_element.google_maps.title' +# description: 'app.ui_element.google_maps.description' +# icon: map pin +# tags: [ map ] +# classes: +# form: App\Form\Type\UiElement\GoogleMapsType +# ui_element: App\UiElement\GoogleMapsUiElement +# templates: +# admin_render: '/Admin/UiElement/google_maps.html.twig' +# front_render: '/Shop/UiElement/google_maps.html.twig' app.noseeme: title: 'You should not see me' description: 'The invisible Ui Element' diff --git a/dist/src/Form/Type/UiElement/GoogleMapsType.php b/dist/src/Form/Type/UiElement/GoogleMapsType.php index c4f4dacd..09a4553b 100644 --- a/dist/src/Form/Type/UiElement/GoogleMapsType.php +++ b/dist/src/Form/Type/UiElement/GoogleMapsType.php @@ -13,11 +13,19 @@ namespace App\Form\Type\UiElement; +use App\UiElement\GoogleMapsUiElement; +use MonsieurBiz\SyliusRichEditorPlugin\Attribute\AsUiElement; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Validator\Constraints as Assert; +#[AsUiElement( + code: 'app.google_maps', + icon: 'map pin', + uiElement: GoogleMapsUiElement::class, + tags: ['map'], +)] class GoogleMapsType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void diff --git a/dist/templates/Admin/UiElement/google_maps.html.twig b/dist/templates/Admin/UiElement/google_maps.html.twig new file mode 100644 index 00000000..fc27f965 --- /dev/null +++ b/dist/templates/Admin/UiElement/google_maps.html.twig @@ -0,0 +1,13 @@ +{# +UI Element template +type: google_map +element fields: + link: string +element methods: + getLocale(): string +#} + +
+ {{ ui_element.getLocale() }} + {{ element.link|replace({'hl=en': 'hl=' ~ ui_element.getLocale()}) }} +
diff --git a/dist/templates/Shop/UiElement/google_maps.html.twig b/dist/templates/Shop/UiElement/google_maps.html.twig new file mode 100644 index 00000000..5b03a243 --- /dev/null +++ b/dist/templates/Shop/UiElement/google_maps.html.twig @@ -0,0 +1,12 @@ +{# +UI Element template +type: google_map +element fields: + link: string +element methods: + getLocale(): string +#} + +
+ +
diff --git a/phpmd.xml b/phpmd.xml index 0000ac58..d4952b22 100644 --- a/phpmd.xml +++ b/phpmd.xml @@ -44,7 +44,7 @@ - + diff --git a/phpstan.neon b/phpstan.neon index 226c59b5..dc6ef67f 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -19,5 +19,8 @@ parameters: - 'tests/Application/app/**.php' - 'tests/Application/src/**.php' + # Skeleton files + - 'src/Resources/skeleton/*.php' + ignoreErrors: - '/Parameter #1 \$configuration of method Symfony\\Component\\DependencyInjection\\Extension\\Extension::processConfiguration\(\) expects Symfony\\Component\\Config\\Definition\\ConfigurationInterface, Symfony\\Component\\Config\\Definition\\ConfigurationInterface\|null given\./' diff --git a/src/Attribute/AsUiElement.php b/src/Attribute/AsUiElement.php new file mode 100644 index 00000000..c0439fad --- /dev/null +++ b/src/Attribute/AsUiElement.php @@ -0,0 +1,96 @@ + + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace MonsieurBiz\SyliusRichEditorPlugin\Attribute; + +use Attribute; +use MonsieurBiz\SyliusRichEditorPlugin\UiElement\UiElement; + +#[Attribute(Attribute::TARGET_CLASS)] +class AsUiElement +{ + /** + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + * @SuppressWarnings(PHPMD.BooleanArgumentFlag) + */ + public function __construct( + public string $code, + public string $icon, + public ?string $title = null, + public ?string $description = null, + public string $uiElement = UiElement::class, + public ?TemplatesUiElement $templates = null, + public string $alias = '', + public string $wireframe = '', + public bool $enabled = true, + public array $tags = [], + ) { + } + + public function getCode(): string + { + return $this->code; + } + + public function getConfiguration(): array + { + $configuration = [ + 'title' => $this->getTitle(), + 'description' => $this->getDescription(), + 'icon' => $this->icon, + 'wireframe' => $this->wireframe, + 'enabled' => $this->enabled, + 'tags' => $this->tags, + 'classes' => [ + 'ui_element' => $this->uiElement, + ], + 'templates' => [ + 'admin_render' => $this->getTemplates()->adminRender, + 'front_render' => $this->getTemplates()->frontRender, + 'admin_form' => $this->getTemplates()->adminForm, + ], + 'form_options' => [], + ]; + + if ($this->alias) { + $configuration['alias'] = $this->alias; + } + + return $configuration; + } + + private function getTitle(): string + { + return $this->title ?? 'app.ui_element.' . $this->getLastPartOfCode() . '.title'; + } + + private function getDescription(): string + { + return $this->description ?? 'app.ui_element.' . $this->getLastPartOfCode() . '.description'; + } + + private function getTemplates(): TemplatesUiElement + { + return $this->templates ?? new TemplatesUiElement( + adminRender: 'Admin/UiElement/' . $this->getLastPartOfCode() . '.html.twig', + frontRender: 'Shop/UiElement/' . $this->getLastPartOfCode() . '.html.twig', + ); + } + + private function getLastPartOfCode(): string + { + $parts = explode('.', $this->code); + + return end($parts); + } +} diff --git a/src/Attribute/TemplatesUiElement.php b/src/Attribute/TemplatesUiElement.php new file mode 100644 index 00000000..dda45ac9 --- /dev/null +++ b/src/Attribute/TemplatesUiElement.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace MonsieurBiz\SyliusRichEditorPlugin\Attribute; + +use Attribute; + +#[Attribute(Attribute::TARGET_PROPERTY)] +final class TemplatesUiElement +{ + public function __construct( + public string $adminRender, + public string $frontRender, + public string $adminForm = '@MonsieurBizSyliusRichEditorPlugin/Admin/form.html.twig', + ) { + } +} diff --git a/src/DependencyInjection/UiElementRegistryPass.php b/src/DependencyInjection/UiElementRegistryPass.php index 3c68a698..86614107 100644 --- a/src/DependencyInjection/UiElementRegistryPass.php +++ b/src/DependencyInjection/UiElementRegistryPass.php @@ -13,9 +13,11 @@ namespace MonsieurBiz\SyliusRichEditorPlugin\DependencyInjection; +use MonsieurBiz\SyliusRichEditorPlugin\Attribute\AsUiElement; use MonsieurBiz\SyliusRichEditorPlugin\UiElement\Metadata; use MonsieurBiz\SyliusRichEditorPlugin\UiElement\UiElement; use MonsieurBiz\SyliusRichEditorPlugin\UiElement\UiElementInterface; +use ReflectionClass; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; @@ -38,35 +40,79 @@ public function process(ContainerBuilder $container): void return; } + $this->processConfiguration($uiElements, $container, $registry, $metadataRegistry); + $this->processAttribute($container, $registry, $metadataRegistry); + } + + private function processConfiguration(mixed $uiElements, ContainerBuilder $container, Definition $registry, Definition $metadataRegistry): void + { if (!\is_array($uiElements)) { return; } foreach ($uiElements as $code => $configuration) { - $metadataRegistry->addMethodCall('addFromCodeAndConfiguration', [$code, $configuration]); - $metadata = Metadata::fromCodeAndConfiguration($code, $configuration); + if (!\is_array($configuration)) { + continue; + } + $this->registerUiElement($code, $configuration, $container, $registry, $metadataRegistry); + } + } - $id = $metadata->getServiceId('richeditor.ui_element'); + private function processAttribute(ContainerBuilder $container, Definition $registry, Definition $metadataRegistry): void + { + foreach ($container->getDefinitions() as $definition) { + if ($this->accept($definition) && $reflectionClass = $container->getReflectionClass($definition->getClass(), false)) { + $this->processClass($definition, $reflectionClass, $container, $registry, $metadataRegistry); + } + } + } - $class = $metadata->getClass('ui_element'); - $this->validateUiElementResource($class); + /** + * @param ReflectionClass $reflectionClass + */ + private function processClass(Definition $definition, ReflectionClass $reflectionClass, ContainerBuilder $container, Definition $registry, Definition $metadataRegistry): void + { + foreach ($reflectionClass->getAttributes(AsUiElement::class, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) { + $attribute = $attribute->newInstance(); + /** @var AsUiElement $attribute */ + $code = $attribute->getCode(); + $configuration = $attribute->getConfiguration(); + $configuration['classes']['form'] = $definition->getClass(); // Add the form class to the configuration + + $this->registerUiElement($code, $configuration, $container, $registry, $metadataRegistry); + } + } - $uiElementDefinition = $container->setDefinition($id, (new Definition($class))->setAutowired(true)); - $uiElementDefinition->addMethodCall('setMetadata', [$this->getMetadataDefinition($metadata)]); - $uiElementDefinition->addMethodCall('setTranslator', [new Reference('translator')]); - $uiElementDefinition->addMethodCall('setTwigEnvironment', [new Reference('Twig\Environment')]); + private function accept(Definition $definition): bool + { + return !$definition->isAbstract(); + } - $aliases = [ - UiElementInterface::class . ' $' . $metadata->getCamelCasedCode() . 'UiElement' => $id, - UiElement::class . ' $' . $metadata->getCamelCasedCode() . 'UiElement' => $id, - ]; - if (UiElement::class !== $class) { - $aliases[$class . ' $' . $metadata->getCamelCasedCode() . 'UiElement'] = $id; - } - $container->addAliases($aliases); + private function registerUiElement(string $code, array $configuration, ContainerBuilder $container, Definition $registry, Definition $metadataRegistry): void + { + $metadataRegistry->addMethodCall('addFromCodeAndConfiguration', [$code, $configuration]); + $metadata = Metadata::fromCodeAndConfiguration($code, $configuration); + + $id = $metadata->getServiceId('richeditor.ui_element'); + + $class = $metadata->getClass('ui_element'); + $this->validateUiElementResource($class); - $registry->addMethodCall('addUiElement', [new Reference($id)]); + $uiElementDefinition = $container->setDefinition($id, (new Definition($class))->setAutowired(true)); + $uiElementDefinition->addMethodCall('setMetadata', [$this->getMetadataDefinition($metadata)]); + $uiElementDefinition->addMethodCall('setTranslator', [new Reference('translator')]); + $uiElementDefinition->addMethodCall('setTwigEnvironment', [new Reference('Twig\Environment')]); + + $aliases = [ + UiElementInterface::class . ' $' . $metadata->getCamelCasedCode() . 'UiElement' => $id, + UiElement::class . ' $' . $metadata->getCamelCasedCode() . 'UiElement' => $id, + ]; + if (UiElement::class !== $class) { + $aliases[$class . ' $' . $metadata->getCamelCasedCode() . 'UiElement'] = $id; } + $container->addAliases($aliases); + + $registry->addMethodCall('addUiElement', [new Reference($id)]); } /** diff --git a/src/Maker/UiElementMaker.php b/src/Maker/UiElementMaker.php new file mode 100644 index 00000000..aa234665 --- /dev/null +++ b/src/Maker/UiElementMaker.php @@ -0,0 +1,100 @@ + + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace MonsieurBiz\SyliusRichEditorPlugin\Maker; + +use Symfony\Bundle\MakerBundle\ConsoleStyle; +use Symfony\Bundle\MakerBundle\DependencyBuilder; +use Symfony\Bundle\MakerBundle\Generator; +use Symfony\Bundle\MakerBundle\InputConfiguration; +use Symfony\Bundle\MakerBundle\Maker\AbstractMaker; +use Symfony\Bundle\MakerBundle\Str; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Form\AbstractType; +use Webmozart\Assert\Assert; + +final class UiElementMaker extends AbstractMaker +{ + public static function getCommandName(): string + { + return 'make:ui-element'; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function configureCommand(Command $command, InputConfiguration $inputConfig): void + { + $command + ->addArgument('code', InputArgument::OPTIONAL, 'The code of the UI Element (e.g. my_ui_element)') + ->addArgument('icon', InputArgument::OPTIONAL, 'The semantic icon code for the UI Element (e.g. map pin)') + ->addArgument('code_prefix', InputArgument::OPTIONAL, 'The code prefix for the UI Element (e.g. app)', 'app') + ->setDescription('Creates a new UI Element FormType and templates') + ; + } + + public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void + { + $code = $input->getArgument('code'); + $codePrefix = $input->getArgument('code_prefix'); + Assert::string($code); + $name = Str::asCamelCase($code); + $uiElementFormClassNameDetails = $generator->createClassNameDetails( + $name, + 'Form\\Type\\UiElement\\', + 'Type' + ); + $generator->generateClass( + $uiElementFormClassNameDetails->getFullName(), + __DIR__ . '/../Resources/skeleton/UiElementFormType.tpl.php', + [ + 'code' => sprintf('%s.%s', $codePrefix, $code), + 'icon' => 'map pin', + 'tags' => json_encode([]), + ] + ); + + // Generate templates + $generator->generateTemplate( + sprintf('Admin/UiElement/%s.html.twig', $code), + __DIR__ . '/../Resources/skeleton/UiElementTemplate.tpl.php', + [ + 'code' => $code, + ] + ); + $generator->generateTemplate( + sprintf('Shop/UiElement/%s.html.twig', $code), + __DIR__ . '/../Resources/skeleton/UiElementTemplate.tpl.php', + [ + 'code' => $code, + ] + ); + + $generator->writeChanges(); + + $this->writeSuccessMessage($io); + $io->text([ + 'Next: Open your new UI Element FormType and templates, and start customizing it.', + ]); + } + + public function configureDependencies(DependencyBuilder $dependencies): void + { + $dependencies->addClassDependency( + AbstractType::class, + 'monsieurbiz/sylius-rich-editor-plugin' + ); + } +} diff --git a/src/Resources/config/services.yaml b/src/Resources/config/services.yaml index 367ad456..ff1ad6f3 100644 --- a/src/Resources/config/services.yaml +++ b/src/Resources/config/services.yaml @@ -3,7 +3,7 @@ services: autowire: true autoconfigure: true public: false - + bind: string $monsieurbizRicheditorDefaultElement: '%monsieurbiz.richeditor.config.default_element%' string $monsieurbizRicheditorDefaultElementDataField: '%monsieurbiz.richeditor.config.default_element_data_field%' @@ -12,7 +12,7 @@ services: MonsieurBiz\SyliusRichEditorPlugin\Controller\: resource: '../../Controller' tags: ['controller.service_arguments'] - + MonsieurBiz\SyliusRichEditorPlugin\Fixture\: resource: '../../Fixture' @@ -21,7 +21,7 @@ services: MonsieurBiz\SyliusRichEditorPlugin\Uploader\: resource: '../../Uploader' - + MonsieurBiz\SyliusRichEditorPlugin\Switcher\: resource: '../../Switcher' @@ -45,3 +45,7 @@ services: MonsieurBiz\SyliusRichEditorPlugin\Uploader\FileUploaderInterface: '@MonsieurBiz\SyliusRichEditorPlugin\Uploader\FileUploader' MonsieurBiz\SyliusRichEditorPlugin\Switcher\SwitchAdminLocaleInterface: '@MonsieurBiz\SyliusRichEditorPlugin\Switcher\SwitchAdminLocale' + + # Maker + MonsieurBiz\SyliusRichEditorPlugin\Maker\UiElementMaker: + tags: ['maker.command'] diff --git a/src/Resources/skeleton/UiElementFormType.tpl.php b/src/Resources/skeleton/UiElementFormType.tpl.php new file mode 100644 index 00000000..3c1999a3 --- /dev/null +++ b/src/Resources/skeleton/UiElementFormType.tpl.php @@ -0,0 +1,22 @@ + +declare(strict_types=1); + +namespace ; + +use MonsieurBiz\SyliusRichEditorPlugin\Attribute\AsUiElement; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\FormBuilderInterface; + +#[AsUiElement( + code: '', + icon: '', + tags: , +)] +class extends AbstractType +{ + public function buildForm(FormBuilderInterface $builder, array $options): void + { + // Build your form here + } +} diff --git a/src/Resources/skeleton/UiElementTemplate.tpl.php b/src/Resources/skeleton/UiElementTemplate.tpl.php new file mode 100644 index 00000000..45b5a019 --- /dev/null +++ b/src/Resources/skeleton/UiElementTemplate.tpl.php @@ -0,0 +1,6 @@ +{# +UI Element template +type: +element fields: + - … +#} diff --git a/src/UiElement/UiElementFormOptionsTrait.php b/src/UiElement/UiElementFormOptionsTrait.php index 21ab2444..8c92be43 100644 --- a/src/UiElement/UiElementFormOptionsTrait.php +++ b/src/UiElement/UiElementFormOptionsTrait.php @@ -13,6 +13,8 @@ namespace MonsieurBiz\SyliusRichEditorPlugin\UiElement; +use InvalidArgumentException; + trait UiElementFormOptionsTrait { use UiElementTrait; @@ -22,6 +24,10 @@ trait UiElementFormOptionsTrait */ public function getFormOptions(): array { - return $this->metadata->getParameter('form_options'); + try { + return $this->metadata->getParameter('form_options'); + } catch (InvalidArgumentException) { + return []; + } } } diff --git a/symfony.lock b/symfony.lock index 49d79451..4a53d665 100644 --- a/symfony.lock +++ b/symfony.lock @@ -701,6 +701,9 @@ "sylius-labs/polyfill-symfony-security": { "version": "v1.0.0" }, + "sylius/calendar": { + "version": "v0.3.0" + }, "sylius/fixtures-bundle": { "version": "v1.6.1" }, @@ -851,6 +854,15 @@ "symfony/intl": { "version": "v4.4.13" }, + "symfony/maker-bundle": { + "version": "1.39", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.0", + "ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f" + } + }, "symfony/messenger": { "version": "4.3", "recipe": {