├── .gitignore ├── img └── maker_bundle.png ├── phpstan.neon ├── src ├── Utils │ ├── NameGenerators │ │ ├── UniqueNameGenerator.php │ │ └── ResourceKeyExtractor.php │ └── ConsoleHelperTrait.php ├── Maker │ ├── TashHandlerMaker │ │ ├── TashHandlerGeneratorSettings.php │ │ ├── trash_handler_template.tpl.php │ │ └── MakeTrashHandlerCommand.php │ ├── PreviewMaker │ │ ├── preview_template.tpl.php │ │ ├── preview_provider_template.tpl.php │ │ └── MakePreviewCommand.php │ ├── SuluPageMaker │ │ ├── page_template.tpl.php │ │ ├── page_config.tpl.php │ │ └── MakePageTypeCommand.php │ ├── ListConfigurationMaker │ │ ├── ListJoinInfo.php │ │ ├── ListPropertyInfo.php │ │ ├── ConditionType.php │ │ ├── JoinType.php │ │ ├── list_template.tpl.php │ │ ├── MakeListConfigurationCommand.php │ │ └── ListPropertyInfoProvider.php │ ├── DocumentFixtureMaker │ │ ├── fixture.tpl.php │ │ └── MakeDocumentFixtureCommand.php │ ├── ControllerMaker │ │ ├── ControllerGeneratorSettings.php │ │ ├── controllerTemplate.tpl.php │ │ └── MakeControllerCommand.php │ ├── AdminConfigurationMaker │ │ ├── AdminGeneratorSettings.php │ │ ├── configurationTemplate.tpl.php │ │ └── MakeAdminConfigurationCommand.php │ ├── MigrationMaker │ │ ├── MigrationFilters.php │ │ ├── migration_template.tpl.php │ │ └── MakeMigrationCommand.php │ └── WebspaceConfigMaker │ │ ├── webspace_template.tpl.php │ │ └── MakeWebspaceConfigCommand.php ├── Property │ ├── PropertyToSuluTypeGuesserInterface.php │ └── PropertyToSuluTypeGuesser.php ├── SuluMakerBundle.php ├── DependencyInjection │ └── SuluMakerExtension.php ├── Enums │ └── Visibility.php └── Resources │ └── config │ └── services.php ├── LICENSE.md ├── composer.json ├── .github └── workflows │ └── php.yml ├── .php-cs-fixer.dist.php └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /.gitignore 3 | /composer.lock 4 | /.php-cs-fixer.cache 5 | -------------------------------------------------------------------------------- /img/maker_bundle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FriendsOfSulu/maker-bundle/HEAD/img/maker_bundle.png -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - vendor/phpstan/phpstan-webmozart-assert/extension.neon 3 | 4 | parameters: 5 | level: max 6 | paths: 7 | - src 8 | ignoreErrors: 9 | - "#ClassMetadata#" 10 | 11 | -------------------------------------------------------------------------------- /src/Utils/NameGenerators/UniqueNameGenerator.php: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | Preview of <?= $resource_key; ?> 10 | 11 | 12 | {% block content %} 13 | 14 | {{ dump() }} 15 | {% endblock %} 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/Property/PropertyToSuluTypeGuesserInterface.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | public function getPossibleTypes(string $doctrineType): array; 15 | } 16 | -------------------------------------------------------------------------------- /src/Maker/SuluPageMaker/page_template.tpl.php: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | {{ content.title }} 10 | 11 | 12 |

{{ content.title }}

13 |

14 | The configuration for this page is under:

. 15 |
16 | Here are some more properties of this page: 17 | {{ dump(content) }} 18 |

19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Maker/ListConfigurationMaker/ListJoinInfo.php: -------------------------------------------------------------------------------- 1 | hasExtension('maker')) { 13 | throw new \LogicException('The Symfony MakerBundle is not installed or not enabled in the bundles.php file.'); 14 | } 15 | parent::build($container); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Maker/ListConfigurationMaker/ConditionType.php: -------------------------------------------------------------------------------- 1 | */ 14 | public static function descriptions(): array 15 | { 16 | return [ 17 | self::ON->value => 'Use ON condition for the join', 18 | self::WITH->value => 'Use WITH condition for the join', 19 | ]; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/DependencyInjection/SuluMakerExtension.php: -------------------------------------------------------------------------------- 1 | $configs */ 13 | public function load(array $configs, ContainerBuilder $container): void 14 | { 15 | $loader = new PhpFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); 16 | $loader->load('services.php'); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Maker/ListConfigurationMaker/JoinType.php: -------------------------------------------------------------------------------- 1 | */ 15 | public static function descriptions(): array 16 | { 17 | return [ 18 | self::LEFT->value => 'Left join (all entries from left table with optional entries from the right table)', 19 | self::RIGHT->value => 'Left join (all entries from right table with optional entries from the left table)', 20 | self::INNER->value => 'Inner join (only entries that are in both tables', 21 | ]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Maker/DocumentFixtureMaker/fixture.tpl.php: -------------------------------------------------------------------------------- 1 | 12 | 13 | declare(strict_types=1); 14 | 15 | namespace ; 16 | 17 | 18 | 19 | class implements DocumentFixtureInterface 20 | { 21 | final public function load(DocumentManagerInterface $documentManager): void 22 | { 23 | // Create your objects here... 24 | 25 | // Don't forget to flush at the end 26 | $documentManager->flush(); 27 | } 28 | 29 | public function getOrder(): int 30 | { 31 | return 10; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/Enums/Visibility.php: -------------------------------------------------------------------------------- 1 | */ 18 | public static function descriptions(): array 19 | { 20 | return [ 21 | self::YES->value => 'Show the property', 22 | self::NO->value => 'Hide the property', 23 | self::ALWAYS->value => "Same as yes but the user can't choose to hide the property", 24 | self::NEVER_->value => "Same as no but the user can't choose to show the property", 25 | ]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Maker/ControllerMaker/ControllerGeneratorSettings.php: -------------------------------------------------------------------------------- 1 | shouldHaveGetAction 23 | || $this->shouldHavePostAction 24 | || $this->shouldHavePutAction 25 | || $this->shouldHaveDeleteAction; 26 | } 27 | 28 | public function hasUpdateActions(): bool 29 | { 30 | return $this->shouldHavePutAction || $this->shouldHavePostAction; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Property/PropertyToSuluTypeGuesser.php: -------------------------------------------------------------------------------- 1 | 'Renders a checkbox']; 11 | } 12 | 13 | if (\in_array($doctrineType, ['text', 'string'], true)) { 14 | return [null => 'Renders a text field']; 15 | } 16 | 17 | if (\in_array($doctrineType, ['float', 'int'], true)) { 18 | return ['number' => 'Renders a number']; 19 | } 20 | 21 | if (\in_array($doctrineType, ['datetime', 'datetime_immutable'], true)) { 22 | return [ 23 | 'datetime' => 'Renders a datetime selector', 24 | 'date' => 'Renders a date selector', 25 | 'time' => 'Renders a time selector', 26 | ]; 27 | } 28 | 29 | return []; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Maker/AdminConfigurationMaker/AdminGeneratorSettings.php: -------------------------------------------------------------------------------- 1 | slug = '/' . $resourceKey; 26 | $this->formKey = $resourceKey; 27 | $this->listKey = $resourceKey; 28 | } 29 | 30 | /** @var array */ 31 | public array $listToolbarActions = [ 32 | 'add', 'delete', 'export', 33 | ]; 34 | 35 | /** @var array */ 36 | public array $formToolbarActions = [ 37 | 'save', 'delete', 38 | ]; 39 | 40 | /** @var array */ 41 | public array $permissionTypes = []; 42 | } 43 | -------------------------------------------------------------------------------- /src/Maker/PreviewMaker/preview_provider_template.tpl.php: -------------------------------------------------------------------------------- 1 | 10 | 11 | declare(strict_types=1); 12 | 13 | namespace ; 14 | 15 | 16 | 17 | class implements PreviewObjectProviderInterface 18 | { 19 | public function getObject($id, $locale): 20 | { 21 | } 22 | 23 | public function getId($object): string 24 | { 25 | } 26 | 27 | public function setValues($object, $locale, array $data): void 28 | { 29 | } 30 | 31 | public function setContext($object, $locale, array $context) 32 | { 33 | } 34 | 35 | public function serialize($object): string 36 | { 37 | } 38 | 39 | public function deserialize($serializedObject, $objectClass): 40 | { 41 | } 42 | 43 | public function getSecurityContext($id, $locale): ?string 44 | { 45 | } 46 | } 47 | 48 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 mamazu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "friendsofsulu/maker-bundle", 3 | "description": "Package to generate configuration and boilerplate code in Sulu", 4 | "type": "library", 5 | "keywords": [ 6 | "maker-bundle", 7 | "sulu" 8 | ], 9 | "require": { 10 | "php": ">=8.2", 11 | "symfony/maker-bundle": "^v1.60.0", 12 | "webmozart/assert": "^1.11.0" 13 | }, 14 | "require-dev": { 15 | "phpunit/phpunit": "^9.6.20", 16 | "phpstan/phpstan": "^1.11.7", 17 | "phpstan/phpstan-webmozart-assert": "^1.2.7", 18 | "php-cs-fixer/shim": "^3.14" 19 | }, 20 | "license": "MIT", 21 | "autoload": { 22 | "psr-4": { 23 | "FriendsOfSulu\\MakerBundle\\": "src/" 24 | } 25 | }, 26 | "config": { 27 | "sort-packages": true 28 | }, 29 | "scripts": { 30 | "analyse": [ 31 | "vendor/bin/phpstan" 32 | ], 33 | "fix": [ 34 | "vendor/bin/php-cs-fixer fix src" 35 | ] 36 | }, 37 | "authors": [ 38 | { 39 | "name": "mamazu", 40 | "email": "14860264+mamazu@users.noreply.github.com" 41 | } 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /.github/workflows/php.yml: -------------------------------------------------------------------------------- 1 | name: PHP Composer 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | build: 14 | name: "PHP ${{ matrix.php }}" 15 | runs-on: ubuntu-22.04 16 | timeout-minutes: 30 17 | strategy: 18 | matrix: 19 | php: [ '8.2', '8.3' ] 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: "Set up php" 24 | uses: "shivammathur/setup-php@v2" 25 | with: 26 | php-version: "${{ matrix.php }}" 27 | coverage: "none" 28 | 29 | - name: Validate composer.json and composer.lock 30 | run: composer validate --strict 31 | 32 | - name: Cache Composer packages 33 | id: composer-cache 34 | uses: actions/cache@v3 35 | with: 36 | path: vendor 37 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} 38 | restore-keys: | 39 | ${{ runner.os }}-php- 40 | 41 | - name: Install dependencies 42 | run: composer install --prefer-dist --no-progress 43 | 44 | - name: Run test suite 45 | run: vendor/bin/phpstan 46 | 47 | - name: Run codestyle 48 | run: vendor/bin/php-cs-fixer check src 49 | -------------------------------------------------------------------------------- /src/Utils/NameGenerators/ResourceKeyExtractor.php: -------------------------------------------------------------------------------- 1 | hasConstant(self::RESOURCE_KEY_CONSTANT)) { 18 | $resourceKey = $reflection->getConstant(self::RESOURCE_KEY_CONSTANT); 19 | } 20 | 21 | if ($reflection->hasProperty(self::RESOURCE_KEY_CONSTANT)) { 22 | $resourceKey = $reflection->getProperty(self::RESOURCE_KEY_CONSTANT)->getValue(); 23 | } 24 | 25 | Assert::notNull( 26 | $resourceKey, 27 | 'Could not find resource key. It has to be a constant or a property on the class: ' . $className, 28 | ); 29 | Assert::string( 30 | $resourceKey, 31 | 'Resource key must be a "string" but got "' . \get_debug_type($resourceKey) . '" given', 32 | ); 33 | 34 | return $resourceKey; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Maker/SuluPageMaker/page_config.tpl.php: -------------------------------------------------------------------------------- 1 | '; 9 | ?> 10 | 47 | 48 | -------------------------------------------------------------------------------- /src/Maker/MigrationMaker/MigrationFilters.php: -------------------------------------------------------------------------------- 1 | $locales */ 11 | public array $locales, 12 | /** @var string|null $webspace */ 13 | public ?string $webspace, 14 | /** @var array $templateKeys */ 15 | public array $templateKeys, 16 | /** @var array $stages */ 17 | public array $stages, 18 | ) { 19 | } 20 | 21 | public function getWhereCondition(): string 22 | { 23 | $whereCondition = '(version = 0)'; 24 | if (null !== $this->webspace) { 25 | $whereCondition .= ' (AND webspace = :webspace)'; 26 | } 27 | if ([] !== $this->locales) { 28 | $whereCondition .= ' (AND locale IN (:locales))'; 29 | } 30 | if ([] !== $this->templateKeys) { 31 | $whereCondition .= ' (AND templateKey IN (:templateKeys))'; 32 | } 33 | if ([] !== $this->stages) { 34 | $whereCondition .= ' (AND stage IN (:stages))'; 35 | } 36 | 37 | return $whereCondition; 38 | } 39 | 40 | /** 41 | * @return array 42 | */ 43 | public function getParams(): array 44 | { 45 | $params = []; 46 | if (null !== $this->webspace) { 47 | $params['webspace'] = $this->webspace; 48 | } 49 | if ([] !== $this->locales) { 50 | $params['locales'] = $this->locales; 51 | } 52 | if ([] !== $this->templateKeys) { 53 | $params['templateKeys'] = $this->templateKeys; 54 | } 55 | if ([] !== $this->stages) { 56 | $params['stages'] = $this->stages; 57 | } 58 | 59 | return $params; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | exclude(['var/cache', 'tests/Resources/cache', 'node_modules']) 5 | ->in(__DIR__); 6 | 7 | $config = new PhpCsFixer\Config(); 8 | $config->setRiskyAllowed(true) 9 | ->setUnsupportedPhpVersionAllowed(true) 10 | ->setParallelConfig(PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect()) 11 | ->setRules([ 12 | '@Symfony' => true, 13 | 'array_syntax' => ['syntax' => 'short'], 14 | 'class_definition' => false, 15 | 'concat_space' => ['spacing' => 'one'], 16 | 'function_declaration' => ['closure_function_spacing' => 'none'], 17 | 'native_constant_invocation' => true, 18 | 'native_function_casing' => true, 19 | 'native_function_invocation' => ['include' => ['@internal']], 20 | 'global_namespace_import' => ['import_classes' => false, 'import_constants' => false, 'import_functions' => false], 21 | 'no_superfluous_phpdoc_tags' => ['allow_mixed' => true, 'remove_inheritdoc' => true], 22 | 'ordered_imports' => true, 23 | 'phpdoc_align' => ['align' => 'left'], 24 | 'phpdoc_types_order' => false, 25 | 'single_line_throw' => false, 26 | 'single_line_comment_spacing' => false, 27 | 'phpdoc_to_comment' => [ 28 | 'ignored_tags' => ['todo', 'var'], 29 | ], 30 | 'phpdoc_separation' => [ 31 | 'groups' => [ 32 | ['Serializer\\*', 'VirtualProperty', 'Accessor', 'Type', 'Groups', 'Expose', 'Exclude', 'SerializedName', 'Inline', 'ExclusionPolicy'], 33 | ], 34 | ], 35 | 'echo_tag_syntax' => false, 36 | 'get_class_to_class_keyword' => false, // should be enabled as soon as support for php < 8 is dropped 37 | 'nullable_type_declaration_for_default_null_value' => true, 38 | 'no_null_property_initialization' => false, 39 | 'fully_qualified_strict_types' => false, 40 | 'new_with_parentheses' => true, 41 | 'trailing_comma_in_multiline' => ['after_heredoc' => true, 'elements' => ['array_destructuring', 'arrays', 'match']], 42 | ]) 43 | ->setFinder($finder); 44 | 45 | return $config; 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/Maker/ListConfigurationMaker/list_template.tpl.php: -------------------------------------------------------------------------------- 1 | $properties 7 | * @var array $joins 8 | */ 9 | 10 | use FriendsOfSulu\MakerBundle\Maker\ListConfigurationMaker\ListJoinInfo; 11 | use FriendsOfSulu\MakerBundle\Maker\ListConfigurationMaker\ListPropertyInfo; 12 | 13 | /** @param array $attributes */ 14 | function renderAttributes(array $attributes): string 15 | { 16 | return \implode(' ', \array_map( 17 | fn (string $key, $value) => $key . '="' . $value . '"', 18 | \array_keys($attributes), 19 | \array_values($attributes), 20 | )); 21 | } 22 | 23 | echo '' . \PHP_EOL; 24 | ?> 25 | 26 | 27 | 28 | $property->name, 31 | 'visibility' => $property->visibility->value, 32 | 'translation' => $property->translations, 33 | ]; 34 | if ($property->visibility->isVisible()) { 35 | $attributes['searchability'] = $property->searchability ? 'yes' : 'no'; 36 | } 37 | if ($property->type) { 38 | $attributes['type'] = $property->type; 39 | } 40 | ?> > 41 | name; ?> 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | name; ?> 50 | targetEntity; ?> 51 | joinType->value; ?> 52 | condition) { 54 | echo ' ' . $join->condition . '' . \PHP_EOL; 55 | } 56 | if (null !== $join->conditionType) { 57 | echo ' ' . $join->conditionType->value . '' . \PHP_EOL; 58 | } 59 | ?> 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/Maker/MigrationMaker/migration_template.tpl.php: -------------------------------------------------------------------------------- 1 | 13 | 14 | declare(strict_types=1); 15 | 16 | namespace ; 17 | 18 | 19 | 20 | final class extends AbstractMigration 21 | { 22 | /** 23 | * Process the template data for a given template key and locale. 24 | * If the locale is null, this is the base data for all locales and represents the non-translated properties. 25 | * 26 | * @param array $templateData 27 | * @return array 28 | */ 29 | private function process(?string $templateKey, string $stage, ?string $locale, array $templateData): array 30 | { 31 | // TODO: Implement your own logic to process the template data. 32 | return $templateData; 33 | } 34 | 35 | public function up(Schema $schema): void 36 | { 37 | // By default only migrate the current version of the page (version 0). 38 | $whereCondition = getWhereCondition()); ?>; 39 | $params = getParams()); ?>; 40 | 41 | $sql = 'SELECT id, templateKey, locale, stage, templateData FROM pa_page_dimension_contents WHERE '.$whereCondition; 42 | 43 | // Foreach result run the process method to get the new template data. 44 | foreach ($this->connection->executeQuery($sql, $params) as $page) { 45 | $newTemplateData = $this->process( 46 | $page['templateKey'], 47 | $page['stage'], 48 | $page['locale'], 49 | json_decode($page['templateData'], associative: true, flags: JSON_THROW_ON_ERROR) 50 | ); 51 | 52 | // Update the template data in the database. 53 | $this->connection->update('pa_page_dimension_contents', [ 54 | 'templateData' => json_encode($newTemplateData, flags: JSON_THROW_ON_ERROR), 55 | ], [ 56 | 'id' => $page['id'], 57 | ]); 58 | } 59 | } 60 | 61 | public function down(Schema $schema): void 62 | { 63 | throw new \RuntimeException('Down migrations are not supported.'); 64 | } 65 | } 66 | 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sulu Maker Bundle 2 | 3 | This package adds code generators for Sulu configurations and other features of Sulu to get you started quicker. This bundle is based on the [Symfony maker bundle](https://symfony.com/bundles/SymfonyMakerBundle/current/index.html) 4 | ## How to install 5 | Installing it with composer is very easy: 6 | ```bash 7 | composer require --dev friendsofsulu/maker-bundle 8 | ``` 9 | 10 | ### Example Usage 11 | Create an entity (either manually or with `make:entity`). 12 | ```php 13 | You can generate an XML file in the Sulu pages directory and an example template in the Twig directory. 39 | 40 | :white_check_mark: **Generating the List XML Configruation `make:sulu:list`** 41 | > The basics for this are working. This can't generate a configuration for entities with join columns. 42 | 43 | :exclamation: **Generating form XML configuration `make:sulu:form`** 44 | > TODO 45 | 46 | :white_check_mark: **Generating an admin class for an entity `sulu:make:admin`** 47 | > Basic generation is working. You can also disable parts of the view generation (generating a view without the edit form). 48 | 49 | :white_check_mark: **Generating a controller `sulu:make:controller`** 50 | > You can generate a controller with get, delete and update actions or any combination of those. And it even has some helpful tips on avoiding pitfalls with `_` in the resource key. 51 | 52 | :exclamation: **Generate all of the above `sulu:make:resource`** 53 | > TODO 54 | 55 | :white_check_mark: **Generate a Trash handler `sulu:make:trash_handler`** 56 | > Generates a Trash handler with the option to also implement restoring functionality for the resource. 57 | 58 | :white_check_mark: **Generate a Sulu fixture `sulu:make:fixture`** 59 | > Generates an example fixture to create a Sulu document 60 | 61 | -------------------------------------------------------------------------------- /src/Maker/DocumentFixtureMaker/MakeDocumentFixtureCommand.php: -------------------------------------------------------------------------------- 1 | addArgument( 37 | self::ARG_FIXTURE_CLASS, 38 | InputArgument::OPTIONAL, 39 | 'The class name of the fixture to create', 40 | ); 41 | } 42 | 43 | public function configureDependencies(DependencyBuilder $dependencies): void 44 | { 45 | } 46 | 47 | public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void 48 | { 49 | $io->info('This command is for generating sulu document fixtures. If you just want to generate doctrine fixtures use bin/console make:fixtures instead.'); 50 | 51 | $fixtureClass = $generator->createClassNameDetails( 52 | $this->getStringArgument($input, self::ARG_FIXTURE_CLASS), 53 | 'DataFixtures\\SuluDocument\\' 54 | ); 55 | 56 | $useStatements = new UseStatementGenerator([ 57 | 'Sulu\Bundle\DocumentManagerBundle\DataFixtures\DocumentFixtureInterface', 58 | 'Sulu\Component\DocumentManager\DocumentManagerInterface', 59 | ]); 60 | 61 | $generator->generateClass( 62 | $fixtureClass->getFullName(), 63 | __DIR__ . '/fixture.tpl.php', 64 | [ 65 | 'use_statements' => $useStatements, 66 | ] 67 | ); 68 | 69 | $generator->writeChanges(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Maker/WebspaceConfigMaker/webspace_template.tpl.php: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | default 23 | homepage 24 | 25 | 26 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | Main Navigation 43 | Hauptnavigation 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | Website 52 | website 53 | 54 | 55 | 56 | 57 | {host} 58 | 59 | 60 | 61 | 62 | {host} 63 | 64 | 65 | 66 | 67 | {host} 68 | 69 | 70 | 71 | 72 | {host} 73 | 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /src/Maker/TashHandlerMaker/trash_handler_template.tpl.php: -------------------------------------------------------------------------------- 1 | 14 | 15 | declare(strict_types=1); 16 | 17 | namespace ; 18 | 19 | 20 | 21 | class implements StoreTrashItemHandlerInterface 22 | shouldHaveRestore) { ?>, RestoreTrashItemHandlerInterface 23 | { 24 | public function __construct( 25 | private readonly TrashItemRepositoryInterface $trashItemRepository, 26 | shouldHaveRestore) { ?> 27 | private readonly EntityManagerInterface $entityManager, 28 | 29 | ) { 30 | } 31 | 32 | public function store(object $resourceToTrash, array $options = []): TrashItemInterface 33 | { 34 | $restoreData = []; 35 | $id = (string) $resourceToTrash->getId(); 36 | $title = 'Deleted resourceClassToTrash; ?> with id '. $id; 37 | 38 | dd('Implement trashing logic here.'); 39 | 40 | return $this->trashItemRepository->create( 41 | resourceKey: $this->getResourceKey(), 42 | resourceId: $id, 43 | resourceTitle: $title, 44 | restoreData: $restoreData, 45 | restoreType: null, 46 | restoreOptions: $options, 47 | resourceSecurityContext: null, // This should be something like resourceClassToTrash; ?>Admin::SECURITY_CONTEXT, 48 | resourceSecurityObjectType: null, 49 | resourceSecurityObjectId: null, 50 | ); 51 | } 52 | 53 | shouldHaveRestore) { ?> 54 | public function restore(TrashItemInterface $trashItem, array $restoreFormData = []): object 55 | { 56 | // Disable id generation for this entity, because we want to set the Id manually. 57 | $metadata = $this->entityManager->getClassMetaData(resourceClassToTrash; ?>::class); 58 | $metadata->setIdGenerator(new AssignedGenerator()); 59 | $metadata->setIdGeneratorType(ClassMetadata::GENERATOR_TYPE_NONE); 60 | 61 | /** @var array $data */ 62 | $data = $trashItem->getRestoreData(); 63 | 64 | dd('Implement restore logic here.'); 65 | /** 66 | Example: 67 | 68 | $resourceToRestore = new resourceClassToTrash; ?>(); 69 | $resourceToRestore->id = $trashItem->getResourceId(); 70 | $this->entityManager->persist($resourceToRestore); 71 | 72 | return $resourceToRestore; 73 | */ 74 | } 75 | 76 | 77 | public static function getResourceKey(): string 78 | { 79 | return resourceClassToTrash; ?>::RESOURCE_KEY; 80 | } 81 | } 82 | 83 | -------------------------------------------------------------------------------- /src/Utils/ConsoleHelperTrait.php: -------------------------------------------------------------------------------- 1 | ask($prompt, $default, null); 21 | 22 | return $result; 23 | } 24 | 25 | /** 26 | * @template T of BackedEnum $enum 27 | * 28 | * @param class-string $enum 29 | * @param ?T $default 30 | * 31 | * @return ($default is null ? T|null : T) 32 | */ 33 | private function askEnum(ConsoleStyle $io, string $prompt, string $enum, ?\BackedEnum $default): ?\BackedEnum 34 | { 35 | Assert::implementsInterface($enum, \BackedEnum::class); 36 | $options = []; 37 | if (\method_exists($enum, 'descriptions')) { 38 | $options = [$enum, 'descriptions'](); 39 | } else { 40 | foreach ([$enum, 'cases']() as $option) { 41 | $options[$option->value] = $option->name; 42 | } 43 | } 44 | 45 | $question = new ChoiceQuestion($prompt, $options, $default?->value); 46 | /** @var null|string $valueString */ 47 | $valueString = $io->askQuestion($question); 48 | if (null === $valueString) { 49 | return $default; 50 | } 51 | 52 | return [$enum, 'from']($valueString); 53 | } 54 | 55 | private static function getStringArgument(InputInterface $input, string $key): string 56 | { 57 | $result = $input->getArgument($key); 58 | Assert::string($result, 'Input option: "' . $key . '" should be a string'); 59 | 60 | return $result; 61 | } 62 | 63 | private static function interactiveEntityArgument(InputInterface $input, string $argumentName, DoctrineHelper $doctrineHelper): void 64 | { 65 | if ($input->getArgument($argumentName)) { 66 | return; 67 | } 68 | 69 | $entityQuestion = new Question('What entity do you want to generate the admin view for'); 70 | $entityQuestion->setValidator(static function(mixed $value) { 71 | if (!$value) { 72 | throw new \InvalidArgumentException('This value cannot be blank.'); 73 | } 74 | 75 | return $value; 76 | }); 77 | $entityQuestion->setAutocompleterValues($doctrineHelper->getEntitiesForAutocomplete()); 78 | $io = new SymfonyStyle($input, new ConsoleOutput()); 79 | 80 | $className = $doctrineHelper->getEntityNamespace() . '\\' . $io->askQuestion($entityQuestion); 81 | $input->setArgument($argumentName, $className); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Maker/SuluPageMaker/MakePageTypeCommand.php: -------------------------------------------------------------------------------- 1 | addArgument(self::ARG_PAGE_KEY, InputArgument::OPTIONAL, 'Key of the page (needs to be unique)'); 43 | $command->addOption( 44 | self::OPT_CONTROLLER, 45 | null, 46 | InputOption::VALUE_OPTIONAL, 47 | 'Service name of the controller that should be called (eg. App\\SomeController::__invoke)', 48 | 'Sulu\Bundle\WebsiteBundle\Controller\DefaultController::indexAction', 49 | ); 50 | $command->addOption(self::OPT_VIEW, null, InputOption::VALUE_OPTIONAL, 'Path where the template should be located'); 51 | } 52 | 53 | public function configureDependencies(DependencyBuilder $dependencies): void 54 | { 55 | } 56 | 57 | public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void 58 | { 59 | /** @var string $pageKey */ 60 | $pageKey = $input->getArgument(self::ARG_PAGE_KEY); 61 | $viewPath = $input->getOption(self::OPT_VIEW) ?? 'page/' . $pageKey; 62 | $configPath = $this->projectDirectory . '/config/templates/pages/' . $pageKey . '.xml'; 63 | 64 | if (\file_exists($configPath) && !$io->confirm("Config path '$configPath' already exists. Overwrite it?")) { 65 | return; 66 | } 67 | 68 | // Generate the config 69 | $generator->generateFile( 70 | $configPath, 71 | __DIR__ . '/page_config.tpl.php', 72 | [ 73 | 'pageKey' => $input->getArgument(self::ARG_PAGE_KEY), 74 | 'viewPath' => $viewPath, 75 | 'controller' => $input->getOption(self::OPT_CONTROLLER), 76 | 'pageName' => Str::asHumanWords($pageKey), 77 | ] 78 | ); 79 | 80 | // Generate an example template 81 | $generator->generateTemplate( 82 | $viewPath . '.html.twig', 83 | __DIR__ . '/page_template.tpl.php', 84 | ['configPath' => $configPath], 85 | ); 86 | 87 | $generator->writeChanges(); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Resources/config/services.php: -------------------------------------------------------------------------------- 1 | services(); 21 | 22 | // Maker commands 23 | $services 24 | ->set(MakeListConfigurationCommand::class) 25 | ->args([ 26 | '%kernel.project_dir%', 27 | service('maker.doctrine_helper'), 28 | service(ListPropertyInfoProvider::class), 29 | service(ResourceKeyExtractor::class), 30 | ]) 31 | ->tag('maker.command') 32 | ; 33 | 34 | $services 35 | ->set(MakePageTypeCommand::class) 36 | ->args([ 37 | '%kernel.project_dir%', 38 | ]) 39 | ->tag('maker.command') 40 | ; 41 | 42 | $services 43 | ->set(MakeAdminConfigurationCommand::class) 44 | ->args([ 45 | service(ResourceKeyExtractor::class), 46 | service('maker.doctrine_helper'), 47 | ]) 48 | ->tag('maker.command') 49 | ; 50 | 51 | $services 52 | ->set(MakeControllerCommand::class) 53 | ->args([ 54 | '%kernel.project_dir%', 55 | service(ResourceKeyExtractor::class), 56 | service('maker.doctrine_helper'), 57 | ]) 58 | ->tag('maker.command') 59 | ; 60 | 61 | $services 62 | ->set(MakeDocumentFixtureCommand::class) 63 | ->tag('maker.command') 64 | ; 65 | 66 | $services->set(MakeTrashHandlerCommand::class) 67 | ->args([service('maker.doctrine_helper')]) 68 | ->tag('maker.command') 69 | ; 70 | 71 | $services->set(MakePreviewCommand::class) 72 | ->args([ 73 | service(ResourceKeyExtractor::class), 74 | service('maker.doctrine_helper'), 75 | ]) 76 | ->tag('maker.command') 77 | ; 78 | 79 | $services 80 | ->set(MakeWebspaceConfigCommand::class) 81 | ->args([ 82 | '%kernel.project_dir%', 83 | ]) 84 | ->tag('maker.command') 85 | ; 86 | 87 | $services 88 | ->set(MakeMigrationCommand::class) 89 | ->args([ 90 | '%kernel.project_dir%', 91 | ]) 92 | ->tag('maker.command') 93 | ; 94 | 95 | // Other services 96 | $services->set(ListPropertyInfoProvider::class) 97 | ->args([ 98 | service(PropertyToSuluTypeGuesser::class), 99 | ]); 100 | 101 | $services->set(PropertyToSuluTypeGuesser::class); 102 | $services->set(ResourceKeyExtractor::class); 103 | }; 104 | -------------------------------------------------------------------------------- /src/Maker/TashHandlerMaker/MakeTrashHandlerCommand.php: -------------------------------------------------------------------------------- 1 | addArgument(self::ARG_RESOURCE_CLASS, InputArgument::REQUIRED, 'The class name of the resource to trash'); 46 | $command->addOption(self::OPT_NO_RESTORE, null, InputOption::VALUE_NONE, 'Do not add restore functionality'); 47 | } 48 | 49 | public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void 50 | { 51 | $this->interactiveEntityArgument($input, self::ARG_RESOURCE_CLASS, $this->doctrineHelper); 52 | } 53 | 54 | public function configureDependencies(DependencyBuilder $dependencies): void 55 | { 56 | } 57 | 58 | public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void 59 | { 60 | /** @var string $resourceClass */ 61 | $resourceClass = $input->getArgument(self::ARG_RESOURCE_CLASS); 62 | 63 | $className = $generator->createClassNameDetails( 64 | Str::getShortClassName($resourceClass), 65 | namespacePrefix: 'Trash\\', 66 | suffix: 'TrashItemHandler' 67 | ); 68 | 69 | $settings = new TashHandlerGeneratorSettings( 70 | Str::getShortClassName($resourceClass), 71 | !$input->getOption(self::OPT_NO_RESTORE), 72 | ); 73 | 74 | $useStatements = new UseStatementGenerator([ 75 | 'Sulu\Bundle\TrashBundle\Application\TrashItemHandler\StoreTrashItemHandlerInterface', 76 | 'Sulu\Bundle\TrashBundle\Domain\Model\TrashItemInterface', 77 | 'Sulu\Bundle\TrashBundle\Domain\Repository\TrashItemRepositoryInterface', 78 | $resourceClass, 79 | ]); 80 | 81 | if ($settings->shouldHaveRestore) { 82 | $useStatements->addUseStatement([ 83 | 'Doctrine\ORM\EntityManagerInterface', 84 | 'Doctrine\ORM\Id\AssignedGenerator', 85 | 'Doctrine\ORM\Mapping\ClassMetadata', 86 | 'Sulu\Bundle\TrashBundle\Application\TrashItemHandler\RestoreTrashItemHandlerInterface', 87 | ]); 88 | } 89 | 90 | $generator->generateClass( 91 | $className->getFullName(), 92 | __DIR__ . '/trash_handler_template.tpl.php', 93 | [ 94 | 'useStatements' => $useStatements, 95 | 'settings' => $settings, 96 | ], 97 | ); 98 | $generator->writeChanges(); 99 | 100 | $io->success(\sprintf('The "%s" trash handler class was created successfully.', $className->getShortName())); 101 | $io->text(<<addArgument(self::ARG_WEBSPACE_KEY, InputArgument::REQUIRED, 'Key of the webspace configuration'); 46 | $command->addArgument( 47 | self::ARG_WEBSPACE_DIRECTORY, 48 | InputArgument::OPTIONAL, 49 | 'Directory for list configurations', 50 | $this->projectDirectory . '/config/webspaces', 51 | ); 52 | $command->addOption( 53 | self::OPT_WEBSPACE_NAME, 54 | null, 55 | InputOption::VALUE_REQUIRED, 56 | 'Name of the webspace configuration', 57 | ); 58 | $command->addOption( 59 | self::OPT_ASSUME_DEFAULTS, 60 | '-d', 61 | InputOption::VALUE_NONE, 62 | 'Assume default values (names will be generated from keys)', 63 | ); 64 | } 65 | 66 | public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void 67 | { 68 | /** @var string $webspaceKey */ 69 | $webspaceKey = $input->getArgument(self::ARG_WEBSPACE_KEY); 70 | 71 | if ($input->getOption(self::OPT_WEBSPACE_NAME)) { 72 | return; 73 | } 74 | 75 | if ($input->getOption(self::OPT_ASSUME_DEFAULTS)) { 76 | $webspaceName = Str::asHumanWords($webspaceKey); 77 | } else { 78 | $webspaceName = $io->askQuestion(new Question('What should the webspace name be')); 79 | } 80 | $input->setOption(self::OPT_WEBSPACE_NAME, $webspaceName); 81 | } 82 | 83 | public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void 84 | { 85 | $webspaceKey = $input->getArgument(self::ARG_WEBSPACE_KEY); 86 | $webspaceName = $input->getOption(self::OPT_WEBSPACE_NAME); 87 | 88 | /** @var string $configDirectory */ 89 | $configDirectory = $input->getArgument(self::ARG_WEBSPACE_DIRECTORY); 90 | if (!\file_exists($configDirectory)) { 91 | throw new FileNotFoundException('Could not find config directory: ' . $configDirectory); 92 | } 93 | 94 | $io->info('Using config directory: ' . $configDirectory); 95 | 96 | $filePath = $configDirectory . '/' . $webspaceKey . '.xml'; 97 | if (\file_exists($filePath)) { 98 | if (!$io->confirm("The list configuration under '$filePath' already exists. Do you want to overwrite it?")) { 99 | return; 100 | } 101 | \unlink($filePath); 102 | } 103 | 104 | $generator->generateFile($filePath, __DIR__ . '/webspace_template.tpl.php', [ 105 | 'webspaceKey' => $webspaceKey, 106 | 'webspaceName' => $webspaceName, 107 | ]); 108 | $generator->writeChanges(); 109 | } 110 | 111 | public function configureDependencies(DependencyBuilder $dependencies): void 112 | { 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/Maker/PreviewMaker/MakePreviewCommand.php: -------------------------------------------------------------------------------- 1 | addArgument(self::ARG_RESOURCE_CLASS, InputOption::VALUE_REQUIRED, 'The resource class to be previewed'); 48 | } 49 | 50 | public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void 51 | { 52 | $this->interactiveEntityArgument($input, self::ARG_RESOURCE_CLASS, $this->doctrineHelper); 53 | } 54 | 55 | public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void 56 | { 57 | /** @var string $resourceClass */ 58 | $resourceClass = $input->getArgument(self::ARG_RESOURCE_CLASS); 59 | Assert::classExists($resourceClass); 60 | $resourceClassName = Str::getShortClassName($resourceClass); 61 | 62 | $classNameDetails = $generator->createClassNameDetails( 63 | name: $resourceClassName, 64 | namespacePrefix: 'PreviewProvider\\', 65 | suffix: 'PreviewProvider' 66 | ); 67 | $resourceKey = $this->resourceKeyExtractor->getUniqueName($resourceClass); 68 | 69 | if (\is_a($resourceClass, '\Sulu\Bundle\ContentBundle\Content\Domain\Model\ContentRichEntityInterface', true)) { 70 | $io->info([<<generateClass( 91 | $classNameDetails->getFullName(), 92 | __DIR__ . '/preview_provider_template.tpl.php', 93 | [ 94 | 'use_statements' => $useStatements, 95 | 'resource_class' => $resourceClassName, 96 | ] 97 | ); 98 | 99 | $templateName = 'admin_preview/' . Str::asTwigVariable($resourceClassName); 100 | $generator->generateTemplate( 101 | $templateName . '.html.twig', 102 | __DIR__ . '/preview_template.tpl.php', 103 | [ 104 | 'resource_key' => $resourceKey, 105 | ] 106 | ); 107 | $generator->writeChanges(); 108 | 109 | $io->info(<< 114 | ... 115 | $templateName 116 | ... 117 | 118 | 119 | * Fill the template with your preview content 120 | * In the {$resourceKey}Admin class use the `createPreviewFormBuilder` method instead of `createFormViewBuilder` 121 | TEXT); 122 | } 123 | 124 | public function configureDependencies(DependencyBuilder $dependencies): void 125 | { 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/Maker/MigrationMaker/MakeMigrationCommand.php: -------------------------------------------------------------------------------- 1 | addOption( 45 | self::ARG_LOCALES, 46 | null, 47 | InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 48 | 'Array of locales that should be migrated (e.g. en fr de). All by default.', 49 | ) 50 | ->addOption( 51 | self::OPT_WEBSPACE, 52 | null, 53 | InputOption::VALUE_OPTIONAL, 54 | 'Filter for webspaces (All by default)' 55 | ) 56 | ->addOption( 57 | self::OPT_TEMPLATE_KEYS, 58 | null, 59 | InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 60 | 'Filter for template keys (All by default)', 61 | ) 62 | ->addOption( 63 | self::OPT_STAGES, 64 | null, 65 | InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 66 | 'Filter for stages (Allowed values: live, draft. All by default)', 67 | suggestedValues: ['live', 'draft'], 68 | ) 69 | ; 70 | } 71 | 72 | public function configureDependencies(DependencyBuilder $dependencies): void 73 | { 74 | $dependencies->addClassDependency( 75 | 'Doctrine\Migrations\AbstractMigration', 76 | 'doctrine/doctrine-migrations-bundle' 77 | ); 78 | } 79 | 80 | public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void 81 | { 82 | /** @var array $locales */ 83 | $locales = $input->getOption(self::ARG_LOCALES); 84 | 85 | /** @var string|null $webspace */ 86 | $webspace = $input->getOption(self::OPT_WEBSPACE); 87 | /** @var array $templateKeys */ 88 | $templateKeys = $input->getOption(self::OPT_TEMPLATE_KEYS) ?? []; 89 | /** @var array $stages */ 90 | $stages = $input->getOption(self::OPT_STAGES) ?? []; 91 | 92 | if (null !== $stages && [] !== $stages) { 93 | $allowedStages = ['live', 'draft']; 94 | foreach ($stages as $stage) { 95 | Assert::inArray( 96 | $stage, 97 | $allowedStages, 98 | \sprintf('Stage "%s" is not allowed. Allowed values are: %s', $stage, \implode(', ', $allowedStages)) 99 | ); 100 | } 101 | } 102 | 103 | $migrationDirectory = $this->projectDirectory . '/migrations'; 104 | if (!\is_dir($migrationDirectory)) { 105 | \mkdir($migrationDirectory, 0755, true); 106 | } 107 | 108 | $timestamp = \date('YmdHis'); 109 | $className = 'Version' . $timestamp; 110 | 111 | $migrationPath = $migrationDirectory . '/' . $className . '.php'; 112 | 113 | if (\file_exists($migrationPath) && !$io->confirm("Migration file '$migrationPath' already exists. Overwrite it?")) { 114 | return; 115 | } 116 | 117 | $useStatements = new UseStatementGenerator([ 118 | 'Doctrine\DBAL\Schema\Schema', 119 | 'Doctrine\Migrations\AbstractMigration', 120 | ]); 121 | 122 | $generator->generateFile( 123 | $migrationPath, 124 | __DIR__ . '/migration_template.tpl.php', 125 | [ 126 | 'class_name' => $className, 127 | 'namespace' => 'DoctrineMigrations', 128 | 'use_statements' => $useStatements, 129 | 'filters' => new MigrationFilters( 130 | $locales, 131 | $webspace, 132 | $templateKeys, 133 | $stages 134 | ), 135 | ] 136 | ); 137 | 138 | $generator->writeChanges(); 139 | 140 | $io->success('Migration created successfully at: ' . $migrationPath); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/Maker/ListConfigurationMaker/MakeListConfigurationCommand.php: -------------------------------------------------------------------------------- 1 | addArgument( 53 | self::ARG_RESOURCE_CLASS, 54 | InputArgument::OPTIONAL, 55 | \sprintf('Class that you want to generate the list view for (eg. %s)', Str::asClassName(Str::getRandomTerm())), 56 | ) 57 | ->addArgument( 58 | self::ARG_LIST_DIRECTORY, 59 | InputArgument::OPTIONAL, 60 | 'Directory for list configurations', 61 | $this->projectDirectory . '/config/lists', 62 | ) 63 | ->addOption( 64 | self::OPT_ASSUME_DEFAULTS, 65 | '-d', 66 | InputOption::VALUE_NONE, 67 | 'Assuming all visible fields are searchable and use default translations.', 68 | ); 69 | 70 | $inputConfig->setArgumentAsNonInteractive(self::ARG_RESOURCE_CLASS); 71 | } 72 | 73 | public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void 74 | { 75 | $this->interactiveEntityArgument($input, self::ARG_RESOURCE_CLASS, $this->doctrineHelper); 76 | } 77 | 78 | public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void 79 | { 80 | /** @var string $configDirectory */ 81 | $configDirectory = $input->getArgument(self::ARG_LIST_DIRECTORY); 82 | if (!\file_exists($configDirectory)) { 83 | throw new FileNotFoundException('Could not find config directory: ' . $configDirectory); 84 | } 85 | 86 | $io->info('Using config directory: ' . $configDirectory); 87 | 88 | /** @var string $className */ 89 | $className = $input->getArgument(self::ARG_RESOURCE_CLASS); 90 | Assert::classExists($className, 'Class does not exist. Please provide an existing entity'); 91 | 92 | $resourceKey = $this->nameGenerator->getUniqueName($className); 93 | $filePath = $configDirectory . '/' . $resourceKey . '.xml'; 94 | if (\file_exists($filePath)) { 95 | if (!$io->confirm("The list configuration under '$filePath' already exists. Do you want to overwrite it?")) { 96 | return; 97 | } 98 | \unlink($filePath); 99 | } 100 | 101 | $io->writeln('Generating list configuration for ' . $className); 102 | 103 | /** @var bool $assumeDefaults */ 104 | $assumeDefaults = $input->getOption(self::OPT_ASSUME_DEFAULTS); 105 | 106 | $this->propertyInfoProvider->setIo($io); 107 | 108 | $metadata = $this->doctrineHelper->getMetadata($className); 109 | Assert::implementsInterface($metadata, 'Doctrine\Persistence\Mapping\ClassMetadata'); 110 | $infos = $this->propertyInfoProvider->provide($metadata, $assumeDefaults); 111 | 112 | $generator->generateFile($filePath, __DIR__ . '/list_template.tpl.php', [ 113 | 'entityClass' => $className, 114 | 'listKey' => $resourceKey, 115 | 'properties' => $infos['properties'], 116 | 'joins' => $infos['joins'], 117 | ]); 118 | $generator->writeChanges(); 119 | 120 | $io->success('Successfully generated list configuration.'); 121 | } 122 | 123 | public function configureDependencies(DependencyBuilder $dependencies): void 124 | { 125 | $dependencies->addClassDependency( 126 | 'Doctrine\Persistence\Mapping\ClassMetadata', 127 | 'doctrine/persistence' 128 | ); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/Maker/ControllerMaker/controllerTemplate.tpl.php: -------------------------------------------------------------------------------- 1 | 14 | 15 | namespace ; 16 | 17 | use ; 18 | 19 | 20 | class 21 | { 22 | 23 | public function __construct( 24 | shouldHaveGetListAction) { ?> 25 | private ViewHandlerInterface $viewHandler, 26 | private FieldDescriptorFactoryInterface $fieldDescriptorFactory, 27 | private DoctrineListBuilderFactoryInterface $listBuilderFactory, 28 | private RestHelperInterface $restHelper, 29 | 30 | needsEntityManager()) {?> 31 | private EntityManagerInterface $entityManager, 32 | 33 | shouldHaveTrashing) {?> 34 | private StoreTrashItemHandlerInterface $trashItemHandler, 35 | 36 | ) { 37 | } 38 | shouldHaveGetListAction) { ?> 39 | 40 | #[Route( 41 | '/', 42 | name: 'app_admin..list', 43 | methods: ['GET'], 44 | )] 45 | public function cgetAction(): Response 46 | { 47 | $fieldDescriptors = $this->fieldDescriptorFactory->getFieldDescriptors(::RESOURCE_KEY); 48 | $listBuilder = $this->listBuilderFactory->create(::class); 49 | $this->restHelper->initializeListBuilder($listBuilder, $fieldDescriptors); 50 | 51 | $listRepresentation = new PaginatedRepresentation( 52 | $listBuilder->execute(), 53 | ::RESOURCE_KEY, 54 | $listBuilder->getCurrentPage(), 55 | $listBuilder->getLimit(), 56 | $listBuilder->count() 57 | ); 58 | 59 | return $this->viewHandler->handle(View::create($listRepresentation)); 60 | } 61 | 62 | shouldHaveGetAction) { ?> 63 | 64 | #[Route( 65 | '//{id}', 66 | name: 'app_admin..get', 67 | methods: ['GET'], 68 | )] 69 | public function getAction(string $id): Response 70 | { 71 | $entity = $this->entityManager->find(::class, $id); 72 | if ($entity === null) { 73 | return new Response('', Response::HTTP_NOT_FOUND); 74 | } 75 | 76 | return $this->viewHandler->handle(View::create($entity)); 77 | } 78 | 79 | shouldHavePostAction) { ?> 80 | 81 | #[Route( 82 | '/', 83 | name: 'app_admin..post', 84 | methods: ['POST'], 85 | )] 86 | public function postAction(Request $request): Response 87 | { 88 | $entity = new (); 89 | $this->mapDataFromRequest($request, $entity); 90 | 91 | $this->entityManager->persist($entity); 92 | $this->entityManager->flush(); 93 | 94 | return $this->viewHandler->handle(View::create($entity)); 95 | } 96 | 97 | shouldHavePutAction) { ?> 98 | 99 | #[Route( 100 | '//{id}', 101 | name: 'app_admin..put', 102 | methods: ['PUT'], 103 | )] 104 | public function putAction(string $id, Request $request): Response 105 | { 106 | $entity = $this->entityManager->find(::class, $id); 107 | if ($entity === null) { 108 | return new Response('', Response::HTTP_NOT_FOUND); 109 | } 110 | 111 | $this->mapDataFromRequest($request, $entity); 112 | 113 | $this->entityManager->flush(); 114 | 115 | return $this->viewHandler->handle(View::create($entity)); 116 | } 117 | 118 | shouldHaveDeleteAction) { ?> 119 | 120 | #[Route( 121 | '//{id}', 122 | name: 'app_admin..delete', 123 | methods: ['DELETE'], 124 | )] 125 | public function deleteAction(string $id): Response 126 | { 127 | $entity = $this->entityManager->find(::class, $id); 128 | if ($entity === null) { 129 | return new Response('', Response::HTTP_NOT_FOUND); 130 | } 131 | 132 | shouldHaveTrashing) { ?> 133 | $this->trashManager->store('', $entity); 134 | 135 | 136 | $this->entityManager->remove($entity); 137 | $this->entityManager->flush(); 138 | 139 | return $this->viewHandler->handle(View::create(null, Response::HTTP_NO_CONTENT)); 140 | } 141 | 142 | shouldHavePostAction || $settings->shouldHavePutAction) { ?> 143 | 144 | public function mapDataFromRequest(Request $request, $entity): void 145 | { 146 | throw new \BadMethodCallException('There was no mapping function defined that can map a request to a object. Implement '. self::class. '::mapDataFromRequest to remove the error'); 147 | } 148 | 149 | 150 | public function getSecurityContext(): string 151 | { 152 | return 'sulu.app.'; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/Maker/ListConfigurationMaker/ListPropertyInfoProvider.php: -------------------------------------------------------------------------------- 1 | io = $io; 28 | } 29 | 30 | /** 31 | * @return array{properties: array, joins: array} 32 | */ 33 | public function provide(ClassMetadata $reflectionClass, bool $assumeDefaults): array 34 | { 35 | $properties = []; 36 | 37 | foreach ($reflectionClass->fieldMappings as $name => $mapping) { 38 | if (null !== ($property = $this->provideProperty($name, $mapping, $assumeDefaults))) { 39 | $properties[] = $property; 40 | } 41 | } 42 | 43 | $joins = []; 44 | foreach ($reflectionClass->associationMappings as $mapping) { 45 | if (null !== ($join = $this->provideJoin($mapping, $assumeDefaults))) { 46 | $joins[] = $join; 47 | } 48 | } 49 | 50 | return ['properties' => $properties, 'joins' => $joins]; 51 | } 52 | 53 | /** 54 | * @param array{id?: true, type?: string} $mapping 55 | */ 56 | protected function provideProperty(string $name, array $mapping, bool $assumeDefaults): ?ListPropertyInfo 57 | { 58 | Assert::notNull($this->io, 'No io set. Please call ' . self::class . '::setIo() before'); 59 | 60 | // If it's a primary identifier (like id) we don't want to show that. 61 | if ($mapping['id'] ?? false) { 62 | return new ListPropertyInfo($name, Visibility::NO, false, 'sulu_admin.' . $name); 63 | } 64 | 65 | $this->io->info(\sprintf('Configuring property: "%s"', $name)); 66 | if (!$assumeDefaults && !$this->io->confirm(\sprintf('Should this property "%s" be configured', $name))) { 67 | $this->io->info(\sprintf('Property "%s" skipped', $name)); 68 | 69 | return null; 70 | } 71 | 72 | /** @var Visibility $visibility */ 73 | $visibility = $this->askEnum($this->io, 'Visible?', Visibility::class, Visibility::YES); 74 | 75 | $searchable = false; 76 | if ($visibility->isVisible()) { 77 | $searchable = $assumeDefaults || $this->io->confirm('Searchable?'); 78 | } 79 | 80 | $type = $this->getType($mapping['type'] ?? 'string'); 81 | 82 | if ($assumeDefaults) { 83 | $translation = 'sulu_admin.' . $name; 84 | } else { 85 | $translation = $this->askString($this->io, 'Translation', 'sulu_admin.' . $name); 86 | } 87 | 88 | return new ListPropertyInfo($name, $visibility, $searchable, $translation, $type); 89 | } 90 | 91 | /** 92 | * @param array{fieldName: string, sourceEntity: string} $mapping 93 | */ 94 | protected function provideJoin(array $mapping, bool $assumeDefaults): ?ListJoinInfo 95 | { 96 | Assert::notNull($this->io, 'No io set. Please call ' . self::class . '::setIo() before'); 97 | 98 | $name = $mapping['fieldName']; 99 | if (!$this->io->confirm(\sprintf('Should this association "%s" be configured', $name))) { 100 | $this->io->info(\sprintf('Association "%s" skipped', $name)); 101 | 102 | return null; 103 | } 104 | 105 | $joinType = JoinType::INNER; 106 | if (!$assumeDefaults) { 107 | /** @var JoinType $joinType */ 108 | $joinType = $this->askEnum($this->io, 'What type of join should be used', JoinType::class, JoinType::INNER); 109 | } 110 | 111 | $condition = $this->askString($this->io, 'Additional condition (leave empty for none)', ''); 112 | if ('' === $condition) { 113 | $condition = null; 114 | $conditionType = null; 115 | } else { 116 | $conditionType = $this->askEnum($this->io, 'What type of condition should be used', ConditionType::class, ConditionType::ON); 117 | } 118 | 119 | return new ListJoinInfo( 120 | $name, 121 | $mapping['sourceEntity'] . '.' . $name, 122 | $joinType, 123 | $condition, 124 | $conditionType, 125 | ); 126 | } 127 | 128 | private function getType(string $doctrineType): ?string 129 | { 130 | Assert::notNull($this->io, 'No io set. Please call ' . self::class . '::setIo() before'); 131 | 132 | $possibleTypes = $this->typeGuesser->getPossibleTypes($doctrineType); 133 | if ([] === $possibleTypes) { 134 | $this->io->note('Could not find any suggestions for the PHP Type of the property. You can extend the class ' . PropertyToSuluTypeGuesser::class . ' for smarter type guessing.'); 135 | 136 | return null; 137 | } 138 | 139 | if (1 === \count($possibleTypes)) { 140 | $keys = \array_keys($possibleTypes); 141 | $type = \reset($keys); 142 | $description = \reset($possibleTypes); 143 | $this->io->info(\sprintf('Choosing the only possible type: %s (%s)', $type ?: 'string', $description)); 144 | 145 | return $type; 146 | } 147 | 148 | /** @var string|null $type */ 149 | $type = $this->io->choice('Sulu display type', $possibleTypes); 150 | 151 | if (null === $type) { 152 | $keys = \array_keys($possibleTypes); 153 | $type = \reset($keys); 154 | $description = \reset($possibleTypes); 155 | $this->io->info(\sprintf('Choosing the best guess: %s (%s)', $type ?: 'string', $description)); 156 | } 157 | 158 | return $type; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/Maker/AdminConfigurationMaker/configurationTemplate.tpl.php: -------------------------------------------------------------------------------- 1 | slug; 9 | 10 | echo " 12 | 13 | namespace ; 14 | 15 | 16 | 17 | class extends Admin 18 | { 19 | public const SECURITY_CONTEXT = ''; 20 | 21 | public const LIST_VIEW = '.list_view'; 22 | shouldHaveEditForm) { ?> 23 | 24 | public const EDIT_FORM_VIEW = '.edit_form'; 25 | 26 | shouldHaveEditForm) { ?> 27 | 28 | public const ADD_FORM_VIEW = '.add_form'; 29 | 30 | 31 | public function __construct( 32 | private SecurityCheckerInterface $securityChecker, 33 | private ViewBuilderFactoryInterface $viewBuilderFactory 34 | shouldHaveReferences) { ?> 35 | private ReferenceViewBuilderFactoryInterface $referenceViewBuilderFactory, 36 | 37 | ) {} 38 | 39 | shouldAddMenuItem) { ?> 40 | public function configureNavigationItems(NavigationItemCollection $navigationItemCollection): void 41 | { 42 | if (!$this->securityChecker->hasPermission(static::SECURITY_CONTEXT, PermissionTypes::VIEW)) { 43 | return; 44 | } 45 | 46 | $menuItem = new NavigationItem('app.menu.'); 47 | $menuItem->setView(static::LIST_VIEW); 48 | 49 | $navigationItemCollection->get(Admin::SETTINGS_NAVIGATION_ITEM)->addChild($menuItem); 50 | } 51 | 52 | 53 | public function configureViews(ViewCollection $viewCollection): void 54 | { 55 | if (!$this->securityChecker->hasPermission(static::SECURITY_CONTEXT, PermissionTypes::VIEW)) { 56 | return; 57 | } 58 | 59 | $formToolbarActions = [ 60 | formToolbarActions as $actionName) { ?> 61 | new ToolbarAction('sulu_admin.'), 62 | 63 | ]; 64 | $listToolbarActions = [ 65 | listToolbarActions as $actionName) { ?> 66 | new ToolbarAction('sulu_admin.'), 67 | 68 | ]; 69 | 70 | // View that displays the table of all entities 71 | $viewCollection->add( 72 | $this->viewBuilderFactory->createListViewBuilder(static::LIST_VIEW, '') 73 | ->setResourceKey() 74 | ->setListKey('listKey; ?>') 75 | ->setTitle('') 76 | ->addListAdapters(['table']) 77 | ->setAddView(static::ADD_FORM_VIEW) 78 | shouldHaveEditForm) { ?> 79 | ->setEditView(static::EDIT_FORM_VIEW) 80 | 81 | ->addToolbarActions($listToolbarActions) 82 | ); 83 | 84 | shouldHaveAddForm) { ?> 85 | // Add form for the resource 86 | $viewCollection->add( 87 | $this->viewBuilderFactory->createResourceTabViewBuilder(static::ADD_FORM_VIEW, '/add') 88 | ->setResourceKey() 89 | ->setBackView(static::LIST_VIEW) 90 | ); 91 | $viewCollection->add( 92 | $this->viewBuilderFactory->createFormViewBuilder(self::ADD_FORM_VIEW.'.details', '/details') 93 | ->setResourceKey('') 94 | ->setFormKey('formKey; ?>') 95 | ->setTabTitle('sulu_admin.details') 96 | shouldHaveEditForm) { ?> 97 | ->setEditView(static::EDIT_FORM_VIEW) 98 | 99 | ->addToolbarActions($formToolbarActions) 100 | ->setParent(static::ADD_FORM_VIEW) 101 | ); 102 | 103 | 104 | shouldHaveEditForm) { ?> 105 | // Edit form view 106 | $viewCollection->add( 107 | $this->viewBuilderFactory->createResourceTabViewBuilder(static::EDIT_FORM_VIEW, '/:id') 108 | ->setResourceKey('') 109 | ->setBackView(static::LIST_VIEW) 110 | ->setTitleProperty('name') 111 | ); 112 | $viewCollection->add( 113 | $this->viewBuilderFactory->createFormViewBuilder(self::EDIT_FORM_VIEW.'.details', '/details') 114 | ->setResourceKey('') 115 | ->setFormKey('formKey; ?>') 116 | ->setTabTitle('sulu_admin.details') 117 | ->addToolbarActions($formToolbarActions) 118 | ->setParent(static::EDIT_FORM_VIEW) 119 | ); 120 | 121 | 122 | shouldHaveReferences) { ?> 123 | if ($this->referenceViewBuilderFactory->hasReferenceListPermission()) { 124 | $insightsResourceTabViewName = static::EDIT_TABS_VIEW.'.insights'; 125 | 126 | $viewCollection->add( 127 | $this->viewBuilderFactory 128 | ->createResourceTabViewBuilder($insightsResourceTabViewName, '/insights') 129 | ->setResourceKey(ListingTile::RESOURCE_KEY) 130 | ->setTabOrder(6144) 131 | ->setTabTitle('sulu_admin.insights') 132 | ->setTitleProperty('') 133 | ->setParent(static::EDIT_TABS_VIEW), 134 | ); 135 | 136 | $viewCollection->add( 137 | $this->referenceViewBuilderFactory 138 | ->createReferenceListViewBuilder( 139 | $insightsResourceTabViewName.'.reference', 140 | '/references', 141 | ListingTile::RESOURCE_KEY, 142 | ) 143 | ->setParent($insightsResourceTabViewName), 144 | ); 145 | } 146 | 147 | } 148 | 149 | public function getSecurityContexts(): array 150 | { 151 | return [ 152 | self::SULU_ADMIN_SECURITY_SYSTEM => [ 153 | 'Settings' => [ 154 | static::SECURITY_CONTEXT => [ 155 | permissionTypes as $permissionType) { ?> 156 | PermissionTypes::, 157 | 158 | ], 159 | ], 160 | ], 161 | ]; 162 | } 163 | 164 | } 165 | -------------------------------------------------------------------------------- /src/Maker/AdminConfigurationMaker/MakeAdminConfigurationCommand.php: -------------------------------------------------------------------------------- 1 | addArgument(self::ARG_RESOURCE_CLASS, InputArgument::OPTIONAL, \sprintf('Class that you want to generate the list view for (eg. %s)', Str::asClassName(Str::getRandomTerm()))) 67 | ->addOption(self::OPT_PERMISSIONS, null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'List of permissions that should be configurable') 68 | ->addOption(self::OPT_FORCE, '-f', InputOption::VALUE_NONE, 'Force the creation of a new file even if the old one is already there') 69 | ->addOption(self::OPT_ASSUME_DEFAULTS, '-d', InputOption::VALUE_NONE, 'Assume default values and ask less questions') 70 | ; 71 | } 72 | 73 | public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void 74 | { 75 | $this->interactiveEntityArgument($input, self::ARG_RESOURCE_CLASS, $this->doctrineHelper); 76 | 77 | /** @var class-string $resourceClassName */ 78 | $resourceClassName = $input->getArgument(self::ARG_RESOURCE_CLASS); 79 | $resourceKey = $this->resourceKeyExtractor->getUniqueName($resourceClassName); 80 | 81 | $this->settings = $this->askMethodsToBeGenerated( 82 | $io, 83 | assumeDefaults: true === $input->getOption(self::OPT_ASSUME_DEFAULTS), 84 | resourceKey: $resourceKey 85 | ); 86 | } 87 | 88 | public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void 89 | { 90 | /** @var class-string $resourceClassName */ 91 | $resourceClassName = $input->getArgument(self::ARG_RESOURCE_CLASS); 92 | 93 | $className = $generator->createClassNameDetails( 94 | Str::getShortClassName($resourceClassName), 95 | namespacePrefix: 'Admin\\', 96 | suffix: 'Admin' 97 | ); 98 | 99 | $useStatements = new UseStatementGenerator( 100 | \array_merge( 101 | self::ADMIN_DEPENDENCIES, 102 | [ 103 | 'Sulu\Component\Security\Authorization\PermissionTypes', 104 | 'Sulu\Component\Security\Authorization\SecurityCheckerInterface', 105 | ] 106 | ) 107 | ); 108 | 109 | if ($this->settings->shouldHaveReferences) { 110 | $useStatements->addUseStatement('Sulu\Bundle\ReferenceBundle\Infrastructure\Sulu\Admin\View\ReferenceViewBuilderFactoryInterface'); 111 | } 112 | 113 | if (\str_contains($this->settings->slug, '_')) { 114 | $io->warning('Your slug contains an _ this could cause problems when generating a controller for this class. It is recommended to not use underscores in the slug.'); 115 | } 116 | 117 | /** @var class-string $permissionTypeClass */ 118 | $permissionTypeClass = 'Sulu\Component\Security\Authorization\PermissionTypes'; 119 | 120 | /** @var array $availablePermissions */ 121 | $availablePermissions = \array_keys((new \ReflectionClass($permissionTypeClass))->getConstants()); 122 | 123 | /** @var array $currentOptionvalue */ 124 | $currentOptionvalue = $input->getOption(self::OPT_PERMISSIONS); 125 | if ($input->isInteractive() && !$currentOptionvalue) { 126 | // Get available PermissionTypes from Sulu class 127 | $choiceQuestion = new ChoiceQuestion( 128 | 'Which permissions should be configurable in the admin panel? (Multiple selections are allowed: comma separated)', 129 | $availablePermissions 130 | ); 131 | $choiceQuestion->setMultiselect(true); 132 | 133 | /** @var array $answer */ 134 | $answer = $io->askQuestion($choiceQuestion); 135 | 136 | $this->settings->permissionTypes = $answer; 137 | } else { 138 | $this->settings->permissionTypes = $currentOptionvalue ?: $availablePermissions; 139 | } 140 | 141 | $generator->generateClass( 142 | $className->getFullName(), 143 | __DIR__ . '/configurationTemplate.tpl.php', 144 | [ 145 | 'use_statements' => $useStatements, 146 | 'resourceKey' => $this->settings->resourceKey, 147 | 'settings' => $this->settings, 148 | ] 149 | ); 150 | 151 | $generator->writeChanges(); 152 | } 153 | 154 | public function configureDependencies(DependencyBuilder $dependencies): void 155 | { 156 | foreach (self::ADMIN_DEPENDENCIES as $class) { 157 | $dependencies->addClassDependency($class, 'sulu/sulu-admin'); 158 | } 159 | } 160 | 161 | private function askMethodsToBeGenerated(ConsoleStyle $io, bool $assumeDefaults, string $resourceKey): AdminGeneratorSettings 162 | { 163 | $settings = new AdminGeneratorSettings($resourceKey); 164 | if ($assumeDefaults) { 165 | return $settings; 166 | } 167 | 168 | $settings->shouldAddMenuItem = $io->confirm('Do you want to have a menu entry?'); 169 | $settings->shouldHaveEditForm = $io->confirm('Do you want to have an edit form?'); 170 | $settings->shouldHaveReferences = $io->confirm('Do you want to have an a references tab?'); 171 | 172 | $slug = $this->askString($io, 'Enter the API slug', '/' . $settings->resourceKey); 173 | $settings->slug = '/' . \ltrim($slug, '/'); 174 | 175 | $settings->formKey = $this->askString($io, 'Form Key', $resourceKey); 176 | $settings->listKey = $this->askString($io, 'List Key', $resourceKey); 177 | 178 | return $settings; 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/Maker/ControllerMaker/MakeControllerCommand.php: -------------------------------------------------------------------------------- 1 | addArgument( 65 | self::ARG_RESOURCE_CLASS, 66 | InputArgument::OPTIONAL, 67 | \sprintf('Class that you want to generate the list view for (eg. %s)', Str::asClassName(Str::getRandomTerm())), 68 | ) 69 | ->addOption( 70 | self::OPT_ESCAPE_ROUTEKEY, 71 | null, 72 | InputOption::VALUE_NONE, 73 | 'If your resource key contains underscores they will be removed', 74 | ) 75 | ->addOption( 76 | self::OPT_ADD_TRASHING, 77 | null, 78 | InputOption::VALUE_NONE, 79 | 'Adding trashing functionality to the controller (see sulu:make:trash)', 80 | ) 81 | ->addOption( 82 | self::OPT_ASSUME_DEFAULTS, 83 | '-d', 84 | InputOption::VALUE_NONE, 85 | 'Assume default values', 86 | ) 87 | ; 88 | } 89 | 90 | public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void 91 | { 92 | $this->interactiveEntityArgument($input, self::ARG_RESOURCE_CLASS, $this->doctrineHelper); 93 | 94 | $this->settings = $this->askMethodsToBeGenerated($io, true === $input->getOption(self::OPT_ASSUME_DEFAULTS)); 95 | $this->settings->shouldHaveTrashing = true === $input->getOption(self::OPT_ADD_TRASHING); 96 | } 97 | 98 | public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void 99 | { 100 | $resourceClass = self::getStringArgument($input, self::ARG_RESOURCE_CLASS); 101 | Assert::classExists($resourceClass); 102 | 103 | $resourceKey = $this->resourceKeyExtractor->getUniqueName($resourceClass); 104 | 105 | $generatedClassName = $generator->createClassNameDetails( 106 | Str::getShortClassName($resourceClass), 107 | namespacePrefix: 'Controller\\Admin\\', 108 | suffix: 'Controller' 109 | ); 110 | 111 | $routeResource = $resourceKey; 112 | if (\str_contains($resourceKey, '_')) { 113 | $io->warning('Your resource key "' . $resourceKey . '" contains an underscore. If this is used as a route key this will generate routes like this: "' . \str_replace('_', '/', $resourceClass) . '". This is normally unwanted behaviour. '); 114 | if ($io->confirm('Should the underscores (_) be removed?', false)) { 115 | $routeResource = \str_replace('_', '', $resourceKey); 116 | $io->info('Removed underscore in route key'); 117 | } 118 | } 119 | 120 | $useStatements = self::CONTROLLER_DEPENDENCIES; 121 | if ($this->settings->shouldHaveGetListAction) { 122 | $useStatements = 123 | \array_merge( 124 | $useStatements, 125 | [ 126 | 'Sulu\Component\Rest\ListBuilder\Doctrine\DoctrineListBuilderFactoryInterface', 127 | 'Sulu\Component\Rest\ListBuilder\Metadata\FieldDescriptorFactoryInterface', 128 | 'Sulu\Component\Rest\ListBuilder\PaginatedRepresentation', 129 | 'Sulu\Component\Rest\RestHelperInterface', 130 | 'Symfony\Component\HttpFoundation\Response', 131 | ] 132 | ); 133 | } 134 | 135 | if ($this->settings->shouldHaveTrashing) { 136 | $useStatements[] = 'Sulu\Bundle\TrashBundle\Application\TrashItemHandler\StoreTrashItemHandlerInterface'; 137 | } 138 | if ($this->settings->needsEntityManager()) { 139 | $useStatements[] = 'Doctrine\ORM\EntityManagerInterface'; 140 | } 141 | 142 | if ($this->settings->hasUpdateActions()) { 143 | $useStatements[] = 'Symfony\Component\HttpFoundation\Request'; 144 | 145 | $io->note('You need to implement the "mapDataFromRequest" on the generated class.'); 146 | } 147 | 148 | $generator->generateClass( 149 | $generatedClassName->getFullName(), 150 | __DIR__ . '/controllerTemplate.tpl.php', 151 | [ 152 | 'use_statements' => new UseStatementGenerator($useStatements), 153 | 'resourceKey' => $resourceKey, 154 | 'route_resource_key' => $resourceKey, 155 | 'resourceClass' => $resourceClass, 156 | 'settings' => $this->settings, 157 | ] 158 | ); 159 | 160 | $generator->writeChanges(); 161 | 162 | $controllerClassName = $generatedClassName->getFullName(); 163 | 164 | $this->suggestAddingRouting($io); 165 | 166 | $io->info([ 167 | 'Registering the controller in the admin panel under `config/sulu_admin.yaml`:', 168 | <<addClassDependency($class, 'friendsofsymfony/rest-bundle'); 183 | } 184 | } 185 | 186 | private function askMethodsToBeGenerated(ConsoleStyle $io, bool $assumeDefaults): ControllerGeneratorSettings 187 | { 188 | $settings = new ControllerGeneratorSettings(); 189 | if ($assumeDefaults) { 190 | return $settings; 191 | } 192 | 193 | $settings->shouldHaveGetListAction = $io->confirm('Should the cgetAction be generated (list view)'); 194 | $settings->shouldHaveGetAction = $io->confirm('Should the getAction be generated (single item)'); 195 | $settings->shouldHaveDeleteAction = $io->confirm('Should a deleteAction be generated'); 196 | 197 | // Settings for the update actions 198 | $settings->shouldHavePostAction = $io->confirm('Should it have a postAction (create)'); 199 | $settings->shouldHavePutAction = $io->confirm('Should it have a putAction (update action)'); 200 | 201 | return $settings; 202 | } 203 | 204 | private function suggestAddingRouting(ConsoleStyle $io): void 205 | { 206 | // Try to see if the thing is already set up and don't suggest it. 207 | $routesContent = \file_get_contents(\implode( 208 | \DIRECTORY_SEPARATOR, 209 | [$this->projectDirectory, 'config', 'routes', 'sulu_admin.yaml'], 210 | )); 211 | if (\is_string($routesContent) && \str_contains($routesContent, 'admin_controllers:')) { 212 | return; 213 | } 214 | 215 | $io->note( 216 | <<