Skip to content

Commit 6f3fb24

Browse files
authored
Merge pull request #255 from dotkernel/issue-254
Update post slug to Autologin using Cookie
2 parents 5fe2faa + dc1c07c commit 6f3fb24

3 files changed

Lines changed: 221 additions & 7 deletions

File tree

‎src/App/src/Fixture/PostLoader.php‎

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,17 +55,20 @@ public function load(ObjectManager $manager): void
5555
}
5656

5757
/** @var Author $author */
58-
$author = $this->getReference('author_' . $authorSlug, Author::class);
59-
$title = html_entity_decode($articleData['post_title'], ENT_QUOTES, 'UTF-8');
60-
$slug = $this->slugify($title);
58+
$author = $this->getReference('author_' . $authorSlug, Author::class);
59+
$title = html_entity_decode($articleData['post_title'], ENT_QUOTES, 'UTF-8');
60+
$autoSlug = $this->slugify($title);
6161

62-
if (isset($usedSlugs[$slug])) {
63-
$usedSlugs[$slug]++;
64-
$slug .= '-' . $usedSlugs[$slug];
62+
if (isset($usedSlugs[$autoSlug])) {
63+
$usedSlugs[$autoSlug]++;
64+
$autoSlug .= '-' . $usedSlugs[$autoSlug];
6565
} else {
66-
$usedSlugs[$slug] = 1;
66+
$usedSlugs[$autoSlug] = 1;
6767
}
6868

69+
$customSlug = trim((string) ($articleData['post_slug'] ?? ''));
70+
$slug = $customSlug !== '' ? $customSlug : $autoSlug;
71+
6972
$status = match ($articleData['post_status']) {
7073
'published' => PostStatusEnum::Published,
7174
'private' => PostStatusEnum::Private,
@@ -85,6 +88,10 @@ public function load(ObjectManager $manager): void
8588
$openGraphImg = $articleData['opengraph_img'] ?? null;
8689

8790
$article = $repository->findOneBy(['slug' => $slug]);
91+
if ($article === null && $slug !== $autoSlug) {
92+
// post was imported before post_slug was set: find it by the title-based slug
93+
$article = $repository->findOneBy(['slug' => $autoSlug]);
94+
}
8895

8996
if ($article === null) {
9097
$article = new Post();
@@ -105,6 +112,10 @@ public function load(ObjectManager $manager): void
105112
} else {
106113
$changed = false;
107114

115+
if ($article->getSlug() !== $slug) {
116+
$article->setSlug($slug);
117+
$changed = true;
118+
}
108119
if ($article->getTitle() !== $title) {
109120
$article->setTitle($title);
110121
$changed = true;

‎src/App/src/Fixture/articles_cleaned.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -936,6 +936,7 @@
936936
},
937937
{
938938
"post_title": "Autologin using Cookie / Remember Me in Dotkernel",
939+
"post_slug": "autologin-cookie-remember-me-feature",
939940
"post_date": "2022-07-18 09:12:59",
940941
"post_status": "published",
941942
"author": {
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace LightTest\Unit\App\Fixture;
6+
7+
use DateTimeImmutable;
8+
use Doctrine\Common\DataFixtures\ReferenceRepository;
9+
use Doctrine\ORM\EntityManagerInterface;
10+
use Doctrine\ORM\EntityRepository;
11+
use Doctrine\ORM\Mapping\ClassMetadata;
12+
use Doctrine\ORM\UnitOfWork;
13+
use Light\App\Fixture\AuthorLoader;
14+
use Light\App\Fixture\CategoryLoader;
15+
use Light\App\Fixture\PostLoader;
16+
use Light\Blog\Entity\Author;
17+
use Light\Blog\Entity\Category;
18+
use Light\Blog\Entity\Post;
19+
use LightTest\Unit\UnitTest;
20+
use PHPUnit\Framework\MockObject\Exception;
21+
use PHPUnit\Framework\MockObject\MockObject;
22+
23+
use function array_count_values;
24+
use function dirname;
25+
use function file_get_contents;
26+
use function json_decode;
27+
use function preg_quote;
28+
use function preg_replace;
29+
use function strtolower;
30+
use function trim;
31+
32+
/**
33+
* Runs PostLoader against the real articles_cleaned.json, using the
34+
* "Autologin using Cookie / Remember Me in Dotkernel" article that sets post_slug.
35+
*/
36+
class PostLoaderTest extends UnitTest
37+
{
38+
private const string TITLE = 'Autologin using Cookie / Remember Me in Dotkernel';
39+
private const string CUSTOM_SLUG = 'autologin-cookie-remember-me-feature';
40+
private const string TITLE_SLUG = 'autologin-using-cookie-remember-me-in-dotkernel';
41+
42+
/**
43+
* @throws Exception
44+
*/
45+
public function testLoadCreatesPostWithCustomSlugWhenNothingMatchesInDb(): void
46+
{
47+
$lookups = [];
48+
$manager = $this->createEntityManager($this->createRepository([], $lookups));
49+
50+
$persistedSlugs = [];
51+
$manager->expects($this->atLeastOnce())->method('persist')
52+
->willReturnCallback(function (Post $post) use (&$persistedSlugs): void {
53+
$persistedSlugs[$post->getTitle()] = $post->getSlug();
54+
});
55+
56+
$this->expectOutputRegex('/CREATE: ' . preg_quote(self::TITLE, '/') . '\n/');
57+
$this->runLoader($manager);
58+
59+
$this->assertSame(self::CUSTOM_SLUG, $persistedSlugs[self::TITLE]);
60+
$this->assertNotContains(self::TITLE_SLUG, $persistedSlugs);
61+
62+
// Not found by post_slug, so the title-based slug is tried before creating.
63+
$counts = array_count_values($lookups);
64+
$this->assertSame(1, $counts[self::CUSTOM_SLUG]);
65+
$this->assertSame(1, $counts[self::TITLE_SLUG]);
66+
}
67+
68+
/**
69+
* @throws Exception
70+
*/
71+
public function testLoadUpdatesPostFoundByCustomSlug(): void
72+
{
73+
$existing = $this->existingPost(self::CUSTOM_SLUG, 'Old title');
74+
75+
$lookups = [];
76+
$manager = $this->createEntityManager($this->createRepository([self::CUSTOM_SLUG => $existing], $lookups));
77+
$manager->expects($this->atLeastOnce())->method('flush');
78+
79+
$this->expectOutputRegex('/UPDATE: ' . preg_quote(self::TITLE, '/') . '\n/');
80+
$this->runLoader($manager);
81+
82+
$this->assertSame(self::CUSTOM_SLUG, $existing->getSlug());
83+
$this->assertSame(self::TITLE, $existing->getTitle());
84+
$this->assertNotContains(self::TITLE_SLUG, $lookups);
85+
}
86+
87+
/**
88+
* @throws Exception
89+
*/
90+
public function testLoadRenamesPostImportedUnderTitleSlugToCustomSlug(): void
91+
{
92+
// Imported before post_slug was added to the JSON.
93+
$existing = $this->existingPost(self::TITLE_SLUG, self::TITLE);
94+
95+
$lookups = [];
96+
$manager = $this->createEntityManager($this->createRepository([self::TITLE_SLUG => $existing], $lookups));
97+
$manager->expects($this->atLeastOnce())->method('flush');
98+
99+
$this->expectOutputRegex('/UPDATE: ' . preg_quote(self::TITLE, '/') . '\n/');
100+
$this->runLoader($manager);
101+
102+
$this->assertSame(self::CUSTOM_SLUG, $existing->getSlug());
103+
}
104+
105+
public function testDependenciesAndOrder(): void
106+
{
107+
$loader = new PostLoader();
108+
109+
$this->assertSame([AuthorLoader::class, CategoryLoader::class], $loader->getDependencies());
110+
$this->assertSame(3, $loader->getOrder());
111+
}
112+
113+
private function existingPost(string $slug, string $title): Post
114+
{
115+
$post = new Post();
116+
$post->setSlug($slug);
117+
$post->setTitle($title);
118+
$post->setPostDate(new DateTimeImmutable());
119+
$post->setCategory(new Category());
120+
$post->setAuthor(new Author());
121+
$post->setExcerpt('');
122+
123+
return $post;
124+
}
125+
126+
/**
127+
* @param array<string, Post> $postsBySlug
128+
* @param list<string> $lookups
129+
* @return EntityRepository<Post>
130+
* @throws Exception
131+
*/
132+
private function createRepository(array $postsBySlug, array &$lookups): EntityRepository
133+
{
134+
$repository = $this->createStub(EntityRepository::class);
135+
$repository->method('findOneBy')->willReturnCallback(
136+
function (array $criteria) use ($postsBySlug, &$lookups): ?Post {
137+
$lookups[] = $criteria['slug'];
138+
return $postsBySlug[$criteria['slug']] ?? null;
139+
}
140+
);
141+
142+
return $repository;
143+
}
144+
145+
/**
146+
* Registers the category and author references the real articles_cleaned.json needs.
147+
*/
148+
private function runLoader(EntityManagerInterface $manager): void
149+
{
150+
$contents = file_get_contents(dirname(__DIR__, 4) . '/src/App/src/Fixture/articles_cleaned.json');
151+
$this->assertIsString($contents);
152+
153+
/** @var list<array{slug: string, articles: list<array{author?: array{display_name?: string}}>}> $categories */
154+
$categories = json_decode($contents, true);
155+
156+
$referenceRepository = new ReferenceRepository($manager);
157+
foreach ($categories as $category) {
158+
$referenceRepository->setReference('category_' . $category['slug'], new Category());
159+
160+
foreach ($category['articles'] as $article) {
161+
$authorName = $article['author']['display_name'] ?? null;
162+
if ($authorName !== null) {
163+
$referenceRepository->setReference('author_' . $this->slugify($authorName), new Author());
164+
}
165+
}
166+
}
167+
168+
$loader = new PostLoader();
169+
$loader->setReferenceRepository($referenceRepository);
170+
$loader->load($manager);
171+
}
172+
173+
private function slugify(string $text): string
174+
{
175+
$text = strtolower(trim($text));
176+
$text = preg_replace('/[^a-z0-9]+/', '-', $text) ?? '';
177+
return trim($text, '-');
178+
}
179+
180+
/**
181+
* @param EntityRepository<Post> $repository
182+
* @throws Exception
183+
*/
184+
private function createEntityManager(EntityRepository $repository): EntityManagerInterface&MockObject
185+
{
186+
$manager = $this->createMock(EntityManagerInterface::class);
187+
$manager->method('getRepository')->willReturn($repository);
188+
$manager->method('contains')->willReturn(true);
189+
190+
$unitOfWork = $this->createStub(UnitOfWork::class);
191+
$unitOfWork->method('isInIdentityMap')->willReturn(false);
192+
$manager->method('getUnitOfWork')->willReturn($unitOfWork);
193+
194+
$manager->method('getClassMetadata')->willReturnCallback(function (string $class) {
195+
$metadata = $this->createStub(ClassMetadata::class);
196+
$metadata->method('getName')->willReturn($class);
197+
return $metadata;
198+
});
199+
200+
return $manager;
201+
}
202+
}

0 commit comments

Comments
 (0)