From 2f80a90052cceb8957718d0724f5a9a4d68061e9 Mon Sep 17 00:00:00 2001 From: Alexander Schranz Date: Thu, 11 Mar 2021 15:32:57 +0100 Subject: [PATCH] Add basic configuration for content bundle storage --- .github/workflows/test-application.yaml | 2 - Article/Domain/Model/Article.php | 39 ++++ Article/Domain/Model/ArticleInterface.php | 25 +++ DependencyInjection/Configuration.php | 26 ++- DependencyInjection/SuluArticleExtension.php | 167 +++++++++++++++--- .../config/doctrine/Article/Article.orm.xml | 14 ++ Resources/config/services.xml | 6 + Resources/doc/installation.md | 1 + SuluArticleBundle.php | 38 +++- Tests/Application/Kernel.php | 17 ++ Tests/Application/config/config.yml | 42 ----- .../config/config_experimental.yml | 4 + Tests/Application/config/config_phpcr.yml | 27 +++ .../Unit/Article/Domain/Model/ArticleTest.php | 40 +++++ UPGRADE.md | 11 ++ composer.json | 8 +- 16 files changed, 394 insertions(+), 73 deletions(-) create mode 100644 Article/Domain/Model/Article.php create mode 100644 Article/Domain/Model/ArticleInterface.php create mode 100644 Resources/config/doctrine/Article/Article.orm.xml create mode 100644 Tests/Application/config/config_experimental.yml create mode 100644 Tests/Application/config/config_phpcr.yml create mode 100644 Tests/Unit/Article/Domain/Model/ArticleTest.php diff --git a/.github/workflows/test-application.yaml b/.github/workflows/test-application.yaml index 6bed923d6..02871301e 100644 --- a/.github/workflows/test-application.yaml +++ b/.github/workflows/test-application.yaml @@ -41,7 +41,6 @@ jobs: dependency-versions: 'highest' php-extensions: 'ctype, iconv, mysql, imagick' tools: 'composer:v2' - phpstan: true lint: true env: SYMFONY_DEPRECATIONS_HELPER: weak @@ -54,7 +53,6 @@ jobs: dependency-versions: 'highest' php-extensions: 'ctype, iconv, mysql, imagick' tools: 'composer:v2' - phpstan: false lint: false env: SYMFONY_DEPRECATIONS_HELPER: weak diff --git a/Article/Domain/Model/Article.php b/Article/Domain/Model/Article.php new file mode 100644 index 000000000..afd7a2b4f --- /dev/null +++ b/Article/Domain/Model/Article.php @@ -0,0 +1,39 @@ +id = $id ?: Uuid::uuid4()->toString(); + } + + public function getId(): string + { + return $this->id; + } +} diff --git a/Article/Domain/Model/ArticleInterface.php b/Article/Domain/Model/ArticleInterface.php new file mode 100644 index 000000000..d626e41ef --- /dev/null +++ b/Article/Domain/Model/ArticleInterface.php @@ -0,0 +1,25 @@ +children() - ->scalarNode('index_name')->isRequired()->end() + ->arrayNode('article') + ->addDefaultsIfNotSet() + ->children() + ->enumNode('storage') + ->values([self::ARTICLE_STORAGE_PHPCR, self::ARTICLE_STORAGE_EXPERIMENTAL]) + ->defaultValue(self::ARTICLE_STORAGE_PHPCR) + ->end() + ->arrayNode('objects') + ->addDefaultsIfNotSet() + ->children() + ->arrayNode('article') + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('model')->defaultValue(Article::class)->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->scalarNode('index_name')->end() ->arrayNode('hosts') ->prototype('scalar')->end() ->end() diff --git a/DependencyInjection/SuluArticleExtension.php b/DependencyInjection/SuluArticleExtension.php index 0d0418fd9..5b0a7774e 100644 --- a/DependencyInjection/SuluArticleExtension.php +++ b/DependencyInjection/SuluArticleExtension.php @@ -11,6 +11,7 @@ namespace Sulu\Bundle\ArticleBundle\DependencyInjection; +use Sulu\Bundle\ArticleBundle\Article\Domain\Model\ArticleInterface; use Sulu\Bundle\ArticleBundle\Document\ArticleDocument; use Sulu\Bundle\ArticleBundle\Document\ArticlePageDocument; use Sulu\Bundle\ArticleBundle\Document\Form\ArticleDocumentType; @@ -19,6 +20,7 @@ use Sulu\Bundle\ArticleBundle\Document\Structure\ArticlePageBridge; use Sulu\Bundle\ArticleBundle\Exception\ArticlePageNotFoundException; use Sulu\Bundle\ArticleBundle\Exception\ParameterNotAllowedException; +use Sulu\Bundle\PersistenceBundle\DependencyInjection\PersistenceExtensionTrait; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; @@ -30,12 +32,65 @@ */ class SuluArticleExtension extends Extension implements PrependExtensionInterface { + use PersistenceExtensionTrait; + /** * {@inheritdoc} */ public function prepend(ContainerBuilder $container) { - if ($container->hasExtension('sulu_core')) { + $configs = $container->getExtensionConfig($this->getAlias()); + $resolvingBag = $container->getParameterBag(); + $configs = $resolvingBag->resolveValue($configs); + $config = $this->processConfiguration(new Configuration(), $configs); + + $storage = $config['article']['storage']; + $isPHPCRStorage = Configuration::ARTICLE_STORAGE_PHPCR === $storage; + $isExperimentalStorage = Configuration::ARTICLE_STORAGE_EXPERIMENTAL === $storage; + + if ($isExperimentalStorage && $container->hasExtension('doctrine')) { + $container->prependExtensionConfig( + 'doctrine', + [ + 'orm' => [ + 'mappings' => [ + 'SuluBundleArticle' => [ + 'type' => 'xml', + 'prefix' => 'Sulu\Bundle\ArticleBundle\Article\Domain\Model', + 'dir' => \dirname(__DIR__) . '/Resources/config/doctrine/Article', + 'alias' => 'SuluArticleBundle', + 'is_bundle' => false, + 'mapping' => true, + ], + ], + ], + ] + ); + } + + if ($isExperimentalStorage && $container->hasExtension('sulu_core')) { + $container->prependExtensionConfig( + 'sulu_core', + [ + 'content' => [ + 'structure' => [ + 'paths' => [ + ArticleInterface::TEMPLATE_TYPE => [ + 'path' => '%kernel.project_dir%/config/templates/articles', + 'type' => 'article', + ], + ], + 'default_type' => [ + ArticleInterface::TEMPLATE_TYPE => 'default', + ], + ], + ], + ] + ); + } + + if ($isPHPCRStorage && $container->hasExtension('sulu_core')) { + // can be removed when phpcr storage is removed $container->prependExtensionConfig( 'sulu_core', [ @@ -61,7 +116,8 @@ public function prepend(ContainerBuilder $container) ); } - if ($container->hasExtension('sulu_route')) { + if ($isPHPCRStorage && $container->hasExtension('sulu_route')) { + // can be removed when phpcr storage is removed $container->prependExtensionConfig( 'sulu_route', [ @@ -77,6 +133,23 @@ public function prepend(ContainerBuilder $container) ); } + if ($isExperimentalStorage && $container->hasExtension('sulu_route')) { + $container->prependExtensionConfig( + 'sulu_route', + [ + 'mappings' => [ + ArticleInterface::class => [ + 'generator' => 'schema', + 'options' => [ + 'route_schema' => '/{object["title"]}', + ], + 'resource_key' => ArticleInterface::RESOURCE_KEY, + ], + ], + ] + ); + } + if ($container->hasExtension('jms_serializer')) { $container->prependExtensionConfig( 'jms_serializer', @@ -93,7 +166,8 @@ public function prepend(ContainerBuilder $container) ); } - if ($container->hasExtension('sulu_search')) { + if ($isPHPCRStorage && $container->hasExtension('sulu_search')) { + // can be removed when phpcr storage is removed $container->prependExtensionConfig( 'sulu_page', [ @@ -107,7 +181,27 @@ public function prepend(ContainerBuilder $container) ); } - if ($container->hasExtension('sulu_document_manager')) { + if ($isExperimentalStorage && $container->hasExtension('sulu_search')) { + $suluSearchConfigs = $container->getExtensionConfig('sulu_search'); + + foreach ($suluSearchConfigs as $suluSearchConfig) { + if (isset($suluSearchConfig['website']['indexes'])) { + $container->prependExtensionConfig( + 'sulu_search', + [ + 'website' => [ + 'indexes' => [ + ArticleInterface::RESOURCE_KEY => ArticleInterface::RESOURCE_KEY . '_published', + ], + ], + ] + ); + } + } + } + + if ($isPHPCRStorage && $container->hasExtension('sulu_document_manager')) { + // can be removed when phpcr storage is removed $container->prependExtensionConfig( 'sulu_document_manager', [ @@ -130,7 +224,8 @@ public function prepend(ContainerBuilder $container) ); } - if ($container->hasExtension('fos_js_routing')) { + if ($isPHPCRStorage && $container->hasExtension('fos_js_routing')) { + // can be removed when phpcr storage is removed $container->prependExtensionConfig( 'fos_js_routing', [ @@ -141,7 +236,8 @@ public function prepend(ContainerBuilder $container) ); } - if ($container->hasExtension('fos_rest')) { + if ($isPHPCRStorage && $container->hasExtension('fos_rest')) { + // can be removed when phpcr storage is removed $container->prependExtensionConfig( 'fos_rest', [ @@ -155,7 +251,8 @@ public function prepend(ContainerBuilder $container) ); } - if ($container->hasExtension('massive_build')) { + if ($isPHPCRStorage && $container->hasExtension('massive_build')) { + // can be removed when phpcr storage is removed $container->prependExtensionConfig( 'massive_build', [ @@ -246,7 +343,8 @@ public function prepend(ContainerBuilder $container) ); } - if ($container->hasExtension('ongr_elasticsearch')) { + if ($isPHPCRStorage && $container->hasExtension('ongr_elasticsearch')) { + // can be removed when phpcr storage is removed $configs = $container->getExtensionConfig($this->getAlias()); $config = $this->processConfiguration(new Configuration(), $configs); @@ -289,41 +387,62 @@ public function load(array $configs, ContainerBuilder $container) { $configuration = new Configuration(); $config = $this->processConfiguration($configuration, $configs); + + $storage = $config['article']['storage']; + $container->setParameter('sulu_article.article_storage', $storage); + $isPHPCRStorage = Configuration::ARTICLE_STORAGE_PHPCR === $storage; + $isExperimentalStorage = Configuration::ARTICLE_STORAGE_EXPERIMENTAL === $storage; + $container->setParameter('sulu_article.default_main_webspace', $config['default_main_webspace']); $container->setParameter('sulu_article.default_additional_webspaces', $config['default_additional_webspaces']); $container->setParameter('sulu_article.types', $config['types']); - $container->setParameter('sulu_article.documents', $config['documents']); - $container->setParameter('sulu_article.view_document.article.class', $config['documents']['article']['view']); $container->setParameter('sulu_article.display_tab_all', $config['display_tab_all']); $container->setParameter('sulu_article.smart_content.default_limit', $config['smart_content']['default_limit']); $container->setParameter('sulu_article.search_fields', $config['search_fields']); + if ($isPHPCRStorage) { + // can be removed when phpcr storage is removed + $container->setParameter('sulu_article.documents', $config['documents']); + $container->setParameter('sulu_article.view_document.article.class', $config['documents']['article']['view']); + } + $loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); - $loader->load('services.xml'); + + if ($isPHPCRStorage) { + // can be removed when phpcr storage is removed + $loader->load('services.xml'); + } + + if ($isExperimentalStorage) { + $this->configurePersistence($config['article']['objects'], $container); + } $bundles = $container->getParameter('kernel.bundles'); if (array_key_exists('SuluAutomationBundle', $bundles)) { $loader->load('automation.xml'); } - $this->appendDefaultAuthor($config, $container); - $this->appendArticlePageConfig($container); + if ($isPHPCRStorage) { + // can be removed when phpcr storage is removed + $this->appendDefaultAuthor($config, $container); + $this->appendArticlePageConfig($container); - $articleDocument = ArticleDocument::class; - $articlePageDocument = ArticlePageDocument::class; + $articleDocument = ArticleDocument::class; + $articlePageDocument = ArticlePageDocument::class; - foreach ($container->getParameter('sulu_document_manager.mapping') as $mapping) { - if ('article' == $mapping['alias']) { - $articleDocument = $mapping['class']; - } + foreach ($container->getParameter('sulu_document_manager.mapping') as $mapping) { + if ('article' == $mapping['alias']) { + $articleDocument = $mapping['class']; + } - if ('article_page' == $mapping['alias']) { - $articlePageDocument = $mapping['class']; + if ('article_page' == $mapping['alias']) { + $articlePageDocument = $mapping['class']; + } } - } - $container->setParameter('sulu_article.article_document.class', $articleDocument); - $container->setParameter('sulu_article.article_page_document.class', $articlePageDocument); + $container->setParameter('sulu_article.article_document.class', $articleDocument); + $container->setParameter('sulu_article.article_page_document.class', $articlePageDocument); + } } /** diff --git a/Resources/config/doctrine/Article/Article.orm.xml b/Resources/config/doctrine/Article/Article.orm.xml new file mode 100644 index 000000000..2ad383d86 --- /dev/null +++ b/Resources/config/doctrine/Article/Article.orm.xml @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/Resources/config/services.xml b/Resources/config/services.xml index 8a578e64c..d55c26c6e 100644 --- a/Resources/config/services.xml +++ b/Resources/config/services.xml @@ -2,6 +2,12 @@ + + @SuluArticle/Export/Article/1.2.xliff.twig diff --git a/Resources/doc/installation.md b/Resources/doc/installation.md index 5d8532b68..18180acb6 100644 --- a/Resources/doc/installation.md +++ b/Resources/doc/installation.md @@ -8,6 +8,7 @@ The SuluArticleBundle requires a running elasticsearch `^5.0`, `^6.0` or `^7.0`. ```bash composer require "elasticsearch/elasticsearch:7.9.*" # should match version of your elasticsearch installation +composer require "handcraftedinthealps/elasticsearch-bundle:^5.2" composer require sulu/article-bundle ``` diff --git a/SuluArticleBundle.php b/SuluArticleBundle.php index 60e0e5a61..240d7a42b 100644 --- a/SuluArticleBundle.php +++ b/SuluArticleBundle.php @@ -11,9 +11,14 @@ namespace Sulu\Bundle\ArticleBundle; +use Sulu\Bundle\ArticleBundle\Article\Domain\Model\ArticleInterface; +use Sulu\Bundle\ArticleBundle\DependencyInjection\Configuration; use Sulu\Bundle\ArticleBundle\DependencyInjection\ConverterCompilerPass; use Sulu\Bundle\ArticleBundle\DependencyInjection\RouteEnhancerCompilerPass; use Sulu\Bundle\ArticleBundle\DependencyInjection\StructureValidatorCompilerPass; +use Sulu\Bundle\PersistenceBundle\DependencyInjection\Compiler\ResolveTargetEntitiesPass; +use Sulu\Bundle\PersistenceBundle\PersistenceBundleTrait; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\Compiler\PassConfig; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; @@ -21,15 +26,42 @@ /** * Entry-point for article-bundle. */ -class SuluArticleBundle extends Bundle +class SuluArticleBundle extends Bundle implements CompilerPassInterface { + use PersistenceBundleTrait; + /** * {@inheritdoc} */ public function build(ContainerBuilder $container) { - $container->addCompilerPass(new ConverterCompilerPass()); + $container->addCompilerPass($this); $container->addCompilerPass(new StructureValidatorCompilerPass(), PassConfig::TYPE_AFTER_REMOVING); - $container->addCompilerPass(new RouteEnhancerCompilerPass()); + } + + public function process(ContainerBuilder $container): void + { + $interfaces = []; + + if (Configuration::ARTICLE_STORAGE_EXPERIMENTAL === $container->getParameter('sulu_article.article_storage')) { + $interfaces = \array_merge($interfaces, [ + ArticleInterface::class => 'sulu.model.article.class', + ]); + } + + $compilerPasses = []; + if (0 < \count($interfaces)) { + $compilerPasses[] = new ResolveTargetEntitiesPass($interfaces); + } + + if (Configuration::ARTICLE_STORAGE_PHPCR === $container->getParameter('sulu_article.article_storage')) { + // can be removed when phpcr storage is removed + $compilerPasses[] = new RouteEnhancerCompilerPass(); + $compilerPasses[] = new ConverterCompilerPass(); + } + + foreach ($compilerPasses as $compilerPass) { + $compilerPass->process($container); + } } } diff --git a/Tests/Application/Kernel.php b/Tests/Application/Kernel.php index 2cec8f994..9d70ea002 100644 --- a/Tests/Application/Kernel.php +++ b/Tests/Application/Kernel.php @@ -16,6 +16,7 @@ use Sulu\Bundle\ArticleBundle\Tests\Application\Testing\ArticleBundleKernelBrowser; use Sulu\Bundle\ArticleBundle\Tests\TestExtendBundle\TestExtendBundle; use Sulu\Bundle\TestBundle\Kernel\SuluTestKernel; +use Sulu\Component\HttpKernel\SuluKernel; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -25,6 +26,20 @@ */ class Kernel extends SuluTestKernel implements CompilerPassInterface { + /** + * @var string|null + */ + private $config = 'phpcr'; + + public function __construct(string $environment, bool $debug, string $suluContext = SuluKernel::CONTEXT_ADMIN) + { + $environmentParts = explode('_', $environment, 2); + $environment = $environmentParts[0]; + $this->config = $environmentParts[1] ?? $this->config; + + parent::__construct($environment, $debug, $suluContext); + } + /** * {@inheritdoc} */ @@ -53,6 +68,8 @@ public function registerContainerConfiguration(LoaderInterface $loader) } $loader->load(__DIR__ . '/config/config.yml'); + $loader->load(__DIR__ . '/config/config_' . $this->config . '.yml'); + $type = 'default'; if (getenv('ARTICLE_TEST_CASE')) { $type = getenv('ARTICLE_TEST_CASE'); diff --git a/Tests/Application/config/config.yml b/Tests/Application/config/config.yml index 684f6ef6b..cd3e3c3bc 100644 --- a/Tests/Application/config/config.yml +++ b/Tests/Application/config/config.yml @@ -1,20 +1,5 @@ # Doctrine Configuration doctrine: - dbal: - driver: pdo_mysql - host: 127.0.0.1 - port: 3306 - dbname: su_articles_test - user: root - password: - server_version: '5.7' - url: '%env(DATABASE_URL)%' - - charset: '%env(DATABASE_CHARSET)%' - default_table_options: - charset: '%env(DATABASE_CHARSET)%' - collate: '%env(DATABASE_COLLATE)%' - orm: mappings: gedmo_tree: @@ -23,30 +8,3 @@ doctrine: dir: "%gedmo_directory%/Tree/Entity" alias: GedmoTree is_bundle: false - -# Sulu Routing -sulu_route: - mappings: - Sulu\Bundle\ArticleBundle\Document\ArticleDocument: - generator: "schema" - options: - route_schema: "/articles/{object.getTitle()}" - Sulu\Bundle\ArticleBundle\Document\ArticlePageDocument: - generator: "article_page" - options: - route_schema: "/{translator.trans(\"page\")}-{object.getPageNumber()}" - parent: "{object.getParent().getRoutePath()}" - -sulu_article: - index_name: "su_articles_tests" - hosts: ["%env(ELASTICSEARCH_HOST)%"] - default_main_webspace: 'sulu_io' - -ongr_elasticsearch: - analysis: - tokenizer: - pathTokenizer: - type: path_hierarchy - analyzer: - pathAnalyzer: - tokenizer: pathTokenizer diff --git a/Tests/Application/config/config_experimental.yml b/Tests/Application/config/config_experimental.yml new file mode 100644 index 000000000..fbc84c7d4 --- /dev/null +++ b/Tests/Application/config/config_experimental.yml @@ -0,0 +1,4 @@ +sulu_article: + article: + storage: 'experimental' + default_main_webspace: 'sulu_io' diff --git a/Tests/Application/config/config_phpcr.yml b/Tests/Application/config/config_phpcr.yml new file mode 100644 index 000000000..72b470ecf --- /dev/null +++ b/Tests/Application/config/config_phpcr.yml @@ -0,0 +1,27 @@ +sulu_route: + mappings: + Sulu\Bundle\ArticleBundle\Document\ArticleDocument: + generator: "schema" + options: + route_schema: "/articles/{object.getTitle()}" + Sulu\Bundle\ArticleBundle\Document\ArticlePageDocument: + generator: "article_page" + options: + route_schema: "/{translator.trans(\"page\")}-{object.getPageNumber()}" + parent: "{object.getParent().getRoutePath()}" + +sulu_article: + article: + storage: 'phpcr' + index_name: "su_articles_tests" + hosts: ["%env(ELASTICSEARCH_HOST)%"] + default_main_webspace: 'sulu_io' + +ongr_elasticsearch: + analysis: + tokenizer: + pathTokenizer: + type: path_hierarchy + analyzer: + pathAnalyzer: + tokenizer: pathTokenizer diff --git a/Tests/Unit/Article/Domain/Model/ArticleTest.php b/Tests/Unit/Article/Domain/Model/ArticleTest.php new file mode 100644 index 000000000..9aaa6292b --- /dev/null +++ b/Tests/Unit/Article/Domain/Model/ArticleTest.php @@ -0,0 +1,40 @@ +createArticle(); + + $this->assertNotNull($article->getId()); + } + + public function testGetIdCustom() + { + $article = $this->createArticle(['id' => '9dd3f8c6-f000-4a37-a780-fe8c3128526d']); + + $this->assertSame('9dd3f8c6-f000-4a37-a780-fe8c3128526d', $article->getId()); + } + + private function createArticle(array $data = []): ArticleInterface + { + return new Article( + $data['id'] ?? null + ); + } +} diff --git a/UPGRADE.md b/UPGRADE.md index cbf07a792..92c4150af 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -1,5 +1,16 @@ # Upgrade +## 2.x + +### Elasticsearch Bundle need to be required + +The SuluArticleBundle defines not longer its dependency to `handcraftedinthealps/elasticsearch-bundle` because +of the new `experimental` storage. If you update you need to require the bundle in your `composer.json`. + +```bash +composer require "handcraftedinthealps/elasticsearch-bundle:^5.2" +``` + ## 2.2.0 ### Index mapping changed diff --git a/composer.json b/composer.json index e89318b1a..5786d19f0 100644 --- a/composer.json +++ b/composer.json @@ -20,11 +20,16 @@ "require": { "php": "^7.2 || ^8.0", "ext-json": "*", + "doctrine/collections": "^1.0", + "doctrine/orm": "^2.5.3", + "doctrine/doctrine-bundle": "^1.10 || ^2.0", + "doctrine/persistence": "^1.3 || ^2.0", "elasticsearch/elasticsearch": "^5.0 || ^6.0 || ^7.0", "handcraftedinthealps/elasticsearch-bundle": "^5.2.6.4", "handcraftedinthealps/elasticsearch-dsl": "^5.0.7.1 || ^6.2.0.1 || ^7.2.0.1", "jms/serializer": "^3.3", "jms/serializer-bundle": "^3.3", + "ramsey/uuid": "^3.1 || ^4.0", "sulu/sulu": "^2.0.10 || ^2.1@dev", "symfony/config": "^4.3 || ^5.0", "symfony/dependency-injection": "^4.3 || ^5.0", @@ -34,7 +39,7 @@ "twig/twig": "^1.41 || ^2.0 || ^3.0" }, "require-dev": { - "doctrine/data-fixtures": "^1.1", + "doctrine/data-fixtures": "^1.3.3", "friendsofphp/php-cs-fixer": "^2.17", "handcraftedinthealps/zendsearch": "^2.0", "jackalope/jackalope-doctrine-dbal": "^1.3.4", @@ -46,6 +51,7 @@ "phpstan/phpstan-symfony": "^0.12.3", "phpunit/phpunit": "^8.2", "sulu/automation-bundle": "^2.0@dev", + "sulu/content-bundle": "^0.6.1", "symfony/browser-kit": "^4.3 || ^5.0", "symfony/dotenv": "^4.3 || ^5.0", "symfony/monolog-bundle": "^3.1",