diff --git a/src/App/src/Fixture/PostLoader.php b/src/App/src/Fixture/PostLoader.php index 96474a44..6c08dd98 100644 --- a/src/App/src/Fixture/PostLoader.php +++ b/src/App/src/Fixture/PostLoader.php @@ -55,17 +55,20 @@ public function load(ObjectManager $manager): void } /** @var Author $author */ - $author = $this->getReference('author_' . $authorSlug, Author::class); - $title = html_entity_decode($articleData['post_title'], ENT_QUOTES, 'UTF-8'); - $slug = $this->slugify($title); + $author = $this->getReference('author_' . $authorSlug, Author::class); + $title = html_entity_decode($articleData['post_title'], ENT_QUOTES, 'UTF-8'); + $autoSlug = $this->slugify($title); - if (isset($usedSlugs[$slug])) { - $usedSlugs[$slug]++; - $slug .= '-' . $usedSlugs[$slug]; + if (isset($usedSlugs[$autoSlug])) { + $usedSlugs[$autoSlug]++; + $autoSlug .= '-' . $usedSlugs[$autoSlug]; } else { - $usedSlugs[$slug] = 1; + $usedSlugs[$autoSlug] = 1; } + $customSlug = trim((string) ($articleData['post_slug'] ?? '')); + $slug = $customSlug !== '' ? $customSlug : $autoSlug; + $status = match ($articleData['post_status']) { 'published' => PostStatusEnum::Published, 'private' => PostStatusEnum::Private, @@ -85,6 +88,10 @@ public function load(ObjectManager $manager): void $openGraphImg = $articleData['opengraph_img'] ?? null; $article = $repository->findOneBy(['slug' => $slug]); + if ($article === null && $slug !== $autoSlug) { + // post was imported before post_slug was set: find it by the title-based slug + $article = $repository->findOneBy(['slug' => $autoSlug]); + } if ($article === null) { $article = new Post(); @@ -105,6 +112,10 @@ public function load(ObjectManager $manager): void } else { $changed = false; + if ($article->getSlug() !== $slug) { + $article->setSlug($slug); + $changed = true; + } if ($article->getTitle() !== $title) { $article->setTitle($title); $changed = true; diff --git a/src/App/src/Fixture/articles_cleaned.json b/src/App/src/Fixture/articles_cleaned.json index ff1fb05b..a98c1527 100644 --- a/src/App/src/Fixture/articles_cleaned.json +++ b/src/App/src/Fixture/articles_cleaned.json @@ -936,6 +936,7 @@ }, { "post_title": "Autologin using Cookie / Remember Me in Dotkernel", + "post_slug": "autologin-cookie-remember-me-feature", "post_date": "2022-07-18 09:12:59", "post_status": "published", "author": { diff --git a/test/Unit/App/Fixture/PostLoaderTest.php b/test/Unit/App/Fixture/PostLoaderTest.php new file mode 100644 index 00000000..f5bf88dc --- /dev/null +++ b/test/Unit/App/Fixture/PostLoaderTest.php @@ -0,0 +1,202 @@ +createEntityManager($this->createRepository([], $lookups)); + + $persistedSlugs = []; + $manager->expects($this->atLeastOnce())->method('persist') + ->willReturnCallback(function (Post $post) use (&$persistedSlugs): void { + $persistedSlugs[$post->getTitle()] = $post->getSlug(); + }); + + $this->expectOutputRegex('/CREATE: ' . preg_quote(self::TITLE, '/') . '\n/'); + $this->runLoader($manager); + + $this->assertSame(self::CUSTOM_SLUG, $persistedSlugs[self::TITLE]); + $this->assertNotContains(self::TITLE_SLUG, $persistedSlugs); + + // Not found by post_slug, so the title-based slug is tried before creating. + $counts = array_count_values($lookups); + $this->assertSame(1, $counts[self::CUSTOM_SLUG]); + $this->assertSame(1, $counts[self::TITLE_SLUG]); + } + + /** + * @throws Exception + */ + public function testLoadUpdatesPostFoundByCustomSlug(): void + { + $existing = $this->existingPost(self::CUSTOM_SLUG, 'Old title'); + + $lookups = []; + $manager = $this->createEntityManager($this->createRepository([self::CUSTOM_SLUG => $existing], $lookups)); + $manager->expects($this->atLeastOnce())->method('flush'); + + $this->expectOutputRegex('/UPDATE: ' . preg_quote(self::TITLE, '/') . '\n/'); + $this->runLoader($manager); + + $this->assertSame(self::CUSTOM_SLUG, $existing->getSlug()); + $this->assertSame(self::TITLE, $existing->getTitle()); + $this->assertNotContains(self::TITLE_SLUG, $lookups); + } + + /** + * @throws Exception + */ + public function testLoadRenamesPostImportedUnderTitleSlugToCustomSlug(): void + { + // Imported before post_slug was added to the JSON. + $existing = $this->existingPost(self::TITLE_SLUG, self::TITLE); + + $lookups = []; + $manager = $this->createEntityManager($this->createRepository([self::TITLE_SLUG => $existing], $lookups)); + $manager->expects($this->atLeastOnce())->method('flush'); + + $this->expectOutputRegex('/UPDATE: ' . preg_quote(self::TITLE, '/') . '\n/'); + $this->runLoader($manager); + + $this->assertSame(self::CUSTOM_SLUG, $existing->getSlug()); + } + + public function testDependenciesAndOrder(): void + { + $loader = new PostLoader(); + + $this->assertSame([AuthorLoader::class, CategoryLoader::class], $loader->getDependencies()); + $this->assertSame(3, $loader->getOrder()); + } + + private function existingPost(string $slug, string $title): Post + { + $post = new Post(); + $post->setSlug($slug); + $post->setTitle($title); + $post->setPostDate(new DateTimeImmutable()); + $post->setCategory(new Category()); + $post->setAuthor(new Author()); + $post->setExcerpt(''); + + return $post; + } + + /** + * @param array $postsBySlug + * @param list $lookups + * @return EntityRepository + * @throws Exception + */ + private function createRepository(array $postsBySlug, array &$lookups): EntityRepository + { + $repository = $this->createStub(EntityRepository::class); + $repository->method('findOneBy')->willReturnCallback( + function (array $criteria) use ($postsBySlug, &$lookups): ?Post { + $lookups[] = $criteria['slug']; + return $postsBySlug[$criteria['slug']] ?? null; + } + ); + + return $repository; + } + + /** + * Registers the category and author references the real articles_cleaned.json needs. + */ + private function runLoader(EntityManagerInterface $manager): void + { + $contents = file_get_contents(dirname(__DIR__, 4) . '/src/App/src/Fixture/articles_cleaned.json'); + $this->assertIsString($contents); + + /** @var list}> $categories */ + $categories = json_decode($contents, true); + + $referenceRepository = new ReferenceRepository($manager); + foreach ($categories as $category) { + $referenceRepository->setReference('category_' . $category['slug'], new Category()); + + foreach ($category['articles'] as $article) { + $authorName = $article['author']['display_name'] ?? null; + if ($authorName !== null) { + $referenceRepository->setReference('author_' . $this->slugify($authorName), new Author()); + } + } + } + + $loader = new PostLoader(); + $loader->setReferenceRepository($referenceRepository); + $loader->load($manager); + } + + private function slugify(string $text): string + { + $text = strtolower(trim($text)); + $text = preg_replace('/[^a-z0-9]+/', '-', $text) ?? ''; + return trim($text, '-'); + } + + /** + * @param EntityRepository $repository + * @throws Exception + */ + private function createEntityManager(EntityRepository $repository): EntityManagerInterface&MockObject + { + $manager = $this->createMock(EntityManagerInterface::class); + $manager->method('getRepository')->willReturn($repository); + $manager->method('contains')->willReturn(true); + + $unitOfWork = $this->createStub(UnitOfWork::class); + $unitOfWork->method('isInIdentityMap')->willReturn(false); + $manager->method('getUnitOfWork')->willReturn($unitOfWork); + + $manager->method('getClassMetadata')->willReturnCallback(function (string $class) { + $metadata = $this->createStub(ClassMetadata::class); + $metadata->method('getName')->willReturn($class); + return $metadata; + }); + + return $manager; + } +}