diff --git a/Content/Application/ContentMetadataInspector/ContentMetadataInspector.php b/Content/Application/ContentMetadataInspector/ContentMetadataInspector.php index 94bd0e99..b1160f13 100644 --- a/Content/Application/ContentMetadataInspector/ContentMetadataInspector.php +++ b/Content/Application/ContentMetadataInspector/ContentMetadataInspector.php @@ -13,6 +13,7 @@ namespace Sulu\Bundle\ContentBundle\Content\Application\ContentMetadataInspector; +use Doctrine\Common\Util\ClassUtils; use Doctrine\ORM\EntityManagerInterface; class ContentMetadataInspector implements ContentMetadataInspectorInterface @@ -29,6 +30,8 @@ public function __construct(EntityManagerInterface $entityManager) public function getDimensionContentClass(string $contentRichEntityClass): string { + $contentRichEntityClass = ClassUtils::getRealClass($contentRichEntityClass); + $classMetadata = $this->entityManager->getClassMetadata($contentRichEntityClass); $associationMapping = $classMetadata->getAssociationMapping('dimensionContents'); @@ -37,6 +40,8 @@ public function getDimensionContentClass(string $contentRichEntityClass): string public function getDimensionContentPropertyName(string $contentRichEntityClass): string { + $contentRichEntityClass = ClassUtils::getRealClass($contentRichEntityClass); + $classMetadata = $this->entityManager->getClassMetadata($contentRichEntityClass); $associationMapping = $classMetadata->getAssociationMapping('dimensionContents'); diff --git a/Content/Infrastructure/Doctrine/RouteRemover.php b/Content/Infrastructure/Doctrine/RouteRemover.php new file mode 100644 index 00000000..00b14c0f --- /dev/null +++ b/Content/Infrastructure/Doctrine/RouteRemover.php @@ -0,0 +1,88 @@ +> + */ + private $routeMappings; + + /** + * @param array> $routeMappings + */ + public function __construct( + ContentMetadataInspectorInterface $contentMetadataInspector, + RouteRepositoryInterface $routeRepository, + array $routeMappings + ) { + $this->routeRepository = $routeRepository; + $this->contentMetadataInspector = $contentMetadataInspector; + $this->routeMappings = $routeMappings; + } + + public function getSubscribedEvents() + { + return [Events::preRemove]; + } + + public function preRemove(LifecycleEventArgs $event): void + { + $object = $event->getObject(); + if (!$object instanceof ContentRichEntityInterface) { + return; // @codeCoverageIgnore + } + + $dimensionContentClass = $this->contentMetadataInspector->getDimensionContentClass(\get_class($object)); + $resourceKey = $dimensionContentClass::getResourceKey(); + + $entityClass = null; + foreach ($this->routeMappings as $key => $mapping) { + if ($resourceKey === $mapping['resource_key']) { + $entityClass = $mapping['entityClass'] ?? $key; + break; + } + } + + if (!$entityClass) { + return; + } + + foreach ($this->routeRepository->findAllByEntity($entityClass, $object->getId()) as $route) { + $event->getEntityManager()->remove($route); + } + } +} diff --git a/Resources/config/services.xml b/Resources/config/services.xml index f85a6170..e97343c0 100644 --- a/Resources/config/services.xml +++ b/Resources/config/services.xml @@ -8,6 +8,13 @@ + + + + %sulu_route.mappings% + + + diff --git a/Tests/Application/ExampleTestBundle/Entity/Example.php b/Tests/Application/ExampleTestBundle/Entity/Example.php index e2d6df30..73a842f1 100644 --- a/Tests/Application/ExampleTestBundle/Entity/Example.php +++ b/Tests/Application/ExampleTestBundle/Entity/Example.php @@ -39,8 +39,6 @@ public function getId() public function createDimensionContent(): DimensionContentInterface { - $exampleDimensionContent = new ExampleDimensionContent($this); - - return $exampleDimensionContent; + return new ExampleDimensionContent($this); } } diff --git a/Tests/Application/assets/admin/.babelrc b/Tests/Application/assets/admin/babel.config.json similarity index 84% rename from Tests/Application/assets/admin/.babelrc rename to Tests/Application/assets/admin/babel.config.json index 145db105..3e7bd2ff 100644 --- a/Tests/Application/assets/admin/.babelrc +++ b/Tests/Application/assets/admin/babel.config.json @@ -3,6 +3,7 @@ "plugins": [ ["@babel/plugin-proposal-decorators", {"legacy": true}], "@babel/plugin-proposal-object-rest-spread", + "@babel/plugin-transform-flow-strip-types", ["@babel/plugin-proposal-class-properties", {"loose": true}] ] } diff --git a/Tests/Application/assets/admin/package.json b/Tests/Application/assets/admin/package.json index 2c164a70..c034c4cc 100644 --- a/Tests/Application/assets/admin/package.json +++ b/Tests/Application/assets/admin/package.json @@ -2,59 +2,66 @@ "license": "MIT", "repository": "https://github.com/sulu/skeleton.git", "scripts": { - "preinstall": "node ../../../../vendor/sulu/sulu/preinstall.js", + "preinstall": "node ../../../../vendor/sulu/sulu/preinstall.js ../../../../vendor node_modules/@sulu/vendor", + "postinstall": "node ../../../../vendor/sulu/sulu/postinstall.js ../../../../vendor node_modules/@sulu/vendor", "build": "webpack --mode production", - "watch": "webpack --mode development -w" + "watch": "webpack --mode development --watch" }, "dependencies": { "mobx": "^4.0.0", "mobx-react": "^5.0.0", - "react": "^16.2.0", - "react-dom": "^16.2.0", - "sulu-admin-bundle": "file:../../../../vendor/sulu/sulu/src/Sulu/Bundle/AdminBundle/Resources/js", - "sulu-audience-targeting-bundle": "file:../../../../vendor/sulu/sulu/src/Sulu/Bundle/AudienceTargetingBundle/Resources/js", - "sulu-category-bundle": "file:../../../../vendor/sulu/sulu/src/Sulu/Bundle/CategoryBundle/Resources/js", - "sulu-contact-bundle": "file:../../../../vendor/sulu/sulu/src/Sulu/Bundle/ContactBundle/Resources/js", - "sulu-custom-url-bundle": "file:../../../../vendor/sulu/sulu/src/Sulu/Bundle/CustomUrlBundle/Resources/js", - "sulu-location-bundle": "file:../../../../vendor/sulu/sulu/src/Sulu/Bundle/LocationBundle/Resources/js", - "sulu-media-bundle": "file:../../../../vendor/sulu/sulu/src/Sulu/Bundle/MediaBundle/Resources/js", - "sulu-page-bundle": "file:../../../../vendor/sulu/sulu/src/Sulu/Bundle/PageBundle/Resources/js", - "sulu-preview-bundle": "file:../../../../vendor/sulu/sulu/src/Sulu/Bundle/PreviewBundle/Resources/js", - "sulu-route-bundle": "file:../../../../vendor/sulu/sulu/src/Sulu/Bundle/RouteBundle/Resources/js", - "sulu-search-bundle": "file:../../../../vendor/sulu/sulu/src/Sulu/Bundle/SearchBundle/Resources/js", - "sulu-security-bundle": "file:../../../../vendor/sulu/sulu/src/Sulu/Bundle/SecurityBundle/Resources/js", - "sulu-snippet-bundle": "file:../../../../vendor/sulu/sulu/src/Sulu/Bundle/SnippetBundle/Resources/js", - "sulu-website-bundle": "file:../../../../vendor/sulu/sulu/src/Sulu/Bundle/WebsiteBundle/Resources/js" + "react": "^17.0.0", + "react-dom": "^17.0.0", + "sulu-admin-bundle": "file:node_modules/@sulu/vendor/sulu/sulu/src/Sulu/Bundle/AdminBundle/Resources/js", + "sulu-audience-targeting-bundle": "file:node_modules/@sulu/vendor/sulu/sulu/src/Sulu/Bundle/AudienceTargetingBundle/Resources/js", + "sulu-category-bundle": "file:node_modules/@sulu/vendor/sulu/sulu/src/Sulu/Bundle/CategoryBundle/Resources/js", + "sulu-contact-bundle": "file:node_modules/@sulu/vendor/sulu/sulu/src/Sulu/Bundle/ContactBundle/Resources/js", + "sulu-custom-url-bundle": "file:node_modules/@sulu/vendor/sulu/sulu/src/Sulu/Bundle/CustomUrlBundle/Resources/js", + "sulu-location-bundle": "file:node_modules/@sulu/vendor/sulu/sulu/src/Sulu/Bundle/LocationBundle/Resources/js", + "sulu-media-bundle": "file:node_modules/@sulu/vendor/sulu/sulu/src/Sulu/Bundle/MediaBundle/Resources/js", + "sulu-page-bundle": "file:node_modules/@sulu/vendor/sulu/sulu/src/Sulu/Bundle/PageBundle/Resources/js", + "sulu-preview-bundle": "file:node_modules/@sulu/vendor/sulu/sulu/src/Sulu/Bundle/PreviewBundle/Resources/js", + "sulu-route-bundle": "file:node_modules/@sulu/vendor/sulu/sulu/src/Sulu/Bundle/RouteBundle/Resources/js", + "sulu-search-bundle": "file:node_modules/@sulu/vendor/sulu/sulu/src/Sulu/Bundle/SearchBundle/Resources/js", + "sulu-security-bundle": "file:node_modules/@sulu/vendor/sulu/sulu/src/Sulu/Bundle/SecurityBundle/Resources/js", + "sulu-snippet-bundle": "file:node_modules/@sulu/vendor/sulu/sulu/src/Sulu/Bundle/SnippetBundle/Resources/js", + "sulu-trash-bundle": "file:node_modules/@sulu/vendor/sulu/sulu/src/Sulu/Bundle/TrashBundle/Resources/js", + "sulu-website-bundle": "file:node_modules/@sulu/vendor/sulu/sulu/src/Sulu/Bundle/WebsiteBundle/Resources/js" }, "devDependencies": { "@babel/core": "^7.5.5", - "@babel/plugin-proposal-class-properties": "^7.5.5", "@babel/plugin-proposal-decorators": "^7.4.4", - "@babel/plugin-proposal-object-rest-spread": "^7.5.5", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.14.2", "@babel/plugin-transform-flow-strip-types": "^7.4.4", "@babel/preset-env": "^7.5.5", "@babel/preset-react": "^7.0.0", - "@ckeditor/ckeditor5-dev-utils": "^12.0.1", - "@ckeditor/ckeditor5-theme-lark": "^14.0.0", - "autoprefixer": "^9.6.1", + "@ckeditor/ckeditor5-dev-utils": "^24.4.2", + "@ckeditor/ckeditor5-theme-lark": "^27.1.0", + "autoprefixer": "^9.8.6", "babel-loader": "^8.0.6", "clean-webpack-plugin": "^3.0.0", - "css-loader": "^3.2.0", - "file-loader": "^4.2.0", + "core-js": "^3.18.0", + "css-loader": "^5.2.4", + "file-loader": "^6.0.0", "glob": "^7.1.2", - "mini-css-extract-plugin": "^0.8.0", + "mini-css-extract-plugin": "^1.5.0", "optimize-css-assets-webpack-plugin": "^5.0.3", - "postcss-calc": "^7.0.1", - "postcss-hexrgba": "^1.0.0", + "postcss": "7.0.35", + "postcss-calc": "^7.0.5", + "postcss-hexrgba": "^2.0.0", "postcss-import": "^12.0.1", "postcss-loader": "^3.0.0", - "postcss-nested": "^4.1.2", + "postcss-nested": "^4.2.3", "postcss-simple-vars": "^5.0.2", - "raw-loader": "^3.1.0", + "raw-loader": "^4.0.0", "regenerator-runtime": "^0.13.3", - "webpack": "^4.20.2", + "webpack": "^4.27.0", "webpack-clean-obsolete-chunks": "^0.4.0", - "webpack-cli": "^3.1.1", - "webpack-manifest-plugin": "^2.0.2" + "webpack-cli": "^4.7.0", + "webpack-manifest-plugin": "^3.1.1" + }, + "engines": { + "node": ">=12", + "npm": ">=6 <7" } } diff --git a/Tests/Application/assets/admin/postcss.config.js b/Tests/Application/assets/admin/postcss.config.js new file mode 100644 index 00000000..5757f7e8 --- /dev/null +++ b/Tests/Application/assets/admin/postcss.config.js @@ -0,0 +1,17 @@ +/* eslint-disable flowtype/require-valid-file-annotation */ +/* eslint-disable import/no-nodejs-modules */ +const path = require('path'); + +// eslint-disable-next-line no-undef +module.exports = { + plugins: { + 'postcss-import': { + path: path.resolve(process.cwd(), 'node_modules'), + }, + 'postcss-nested': {}, + 'postcss-simple-vars': {}, + 'postcss-calc': {}, + 'postcss-hexrgba': {}, + 'autoprefixer': {}, + }, +}; diff --git a/Tests/Functional/Integration/ExampleControllerTest.php b/Tests/Functional/Integration/ExampleControllerTest.php index 07758d4e..a8bda539 100644 --- a/Tests/Functional/Integration/ExampleControllerTest.php +++ b/Tests/Functional/Integration/ExampleControllerTest.php @@ -139,6 +139,9 @@ public function testPost(): int $this->assertResponseSnapshot('example_post.json', $response, 201); + $routeRepository = $this->getContainer()->get('sulu.repository.route'); + $this->assertCount(0, $routeRepository->findAll()); + $id = \json_decode((string) $response->getContent(), true)['id'] ?? null; return $id; @@ -215,6 +218,9 @@ public function testDelete(int $id): void $this->client->request('DELETE', '/admin/api/examples/' . $id . '?locale=en'); $response = $this->client->getResponse(); $this->assertHttpStatusCode(204, $response); + + $routeRepository = $this->getContainer()->get('sulu.repository.route'); + $this->assertCount(0, $routeRepository->findAll()); } protected function getSnapshotFolder(): string diff --git a/Tests/Unit/Content/Infrastructure/Doctrine/RouteRemoverTest.php b/Tests/Unit/Content/Infrastructure/Doctrine/RouteRemoverTest.php new file mode 100644 index 00000000..d1ba9358 --- /dev/null +++ b/Tests/Unit/Content/Infrastructure/Doctrine/RouteRemoverTest.php @@ -0,0 +1,135 @@ + 'examples', + 'entityClass' => ExampleDimensionContent::class, + ], + ]; + + /** + * @var RouteRemover + */ + private $routeRemover; + + protected function setUp(): void + { + parent::setUp(); + + $this->contentMetadataInspector = $this->prophesize(ContentMetadataInspectorInterface::class); + $this->routeRepository = $this->prophesize(RouteRepositoryInterface::class); + + $this->routeRemover = new RouteRemover( + $this->contentMetadataInspector->reveal(), + $this->routeRepository->reveal(), + $this->routeMappings + ); + } + + public function testGetSubscribedEvents(): void + { + $this->assertSame([ + Events::preRemove, + ], $this->routeRemover->getSubscribedEvents()); + } + + public function testPreRemove(): void + { + $object = new Example(); + $property = new \ReflectionProperty(Example::class, 'id'); + $property->setAccessible(true); + $property->setValue($object, '123-123-123'); + + $this->contentMetadataInspector->getDimensionContentClass(Example::class) + ->willReturn(ExampleDimensionContent::class); + + $entityManager = $this->prophesize(EntityManagerInterface::class); + $event = new LifecycleEventArgs($object, $entityManager->reveal()); + + $route1 = $this->prophesize(RouteInterface::class); + $route2 = $this->prophesize(RouteInterface::class); + $this->routeRepository->findAllByEntity(Argument::any(), '123-123-123') + ->willReturn([$route1->reveal(), $route2->reveal()]); + + $entityManager->remove($route1->reveal())->shouldBeCalled(); + $entityManager->remove($route2->reveal())->shouldBeCalled(); + + $this->routeRemover->preRemove($event); + } + + public function testPreRemoveNoMappingConfigured(): void + { + $object = new Example(); + $property = new \ReflectionProperty(Example::class, 'id'); + $property->setAccessible(true); + $property->setValue($object, '123-123-123'); + + $this->contentMetadataInspector->getDimensionContentClass(Example::class) + ->willReturn(self::class); // For testing purpose we return the wrong dimension content class + + $entityManager = $this->prophesize(EntityManagerInterface::class); + $event = new LifecycleEventArgs($object, $entityManager->reveal()); + + $this->routeRepository->findAllByEntity(Argument::cetera())->shouldNotBeCalled(); + + $this->routeRemover->preRemove($event); + } + + public function testPreRemoveNoContentRichEntity(): void + { + $object = new \stdClass(); + + $entityManager = $this->prophesize(EntityManagerInterface::class); + $event = new LifecycleEventArgs($object, $entityManager->reveal()); + + $this->routeRepository->findAllByEntity(Argument::cetera())->shouldNotBeCalled(); + + $this->routeRemover->preRemove($event); + } +}