Skip to content

Commit 825add0

Browse files
committed
Complete sitemap
1 parent 2be79d6 commit 825add0

2 files changed

Lines changed: 204 additions & 3 deletions

File tree

src/App/src/Fixture/PostTagLoader.php

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,17 @@
1818

1919
class PostTagLoader extends Fixture implements DependentFixtureInterface
2020
{
21+
public function __construct(
22+
private readonly string $jsonFile = __DIR__ . '/articles_cleaned.json',
23+
) {
24+
}
25+
2126
public function load(ObjectManager $manager): void
2227
{
23-
$jsonFile = __DIR__ . '/articles_cleaned.json';
24-
$contents = file_get_contents($jsonFile);
28+
$contents = file_get_contents($this->jsonFile);
2529

2630
if ($contents === false) {
27-
throw new RuntimeException("Unable to read file: {$jsonFile}");
31+
throw new RuntimeException("Unable to read file: {$this->jsonFile}");
2832
}
2933

3034
$categories = json_decode($contents, true);
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace LightTest\Unit\App\Fixture;
6+
7+
use Doctrine\Common\DataFixtures\ReferenceRepository;
8+
use Doctrine\ORM\EntityManagerInterface;
9+
use Doctrine\ORM\EntityRepository;
10+
use Doctrine\ORM\Mapping\ClassMetadata;
11+
use Doctrine\ORM\UnitOfWork;
12+
use Light\App\Fixture\PostTagLoader;
13+
use Light\Blog\Entity\Post;
14+
use Light\Blog\Entity\PostTag;
15+
use Light\Blog\Entity\Tag;
16+
use LightTest\Unit\UnitTest;
17+
use PHPUnit\Framework\MockObject\Exception;
18+
use PHPUnit\Framework\MockObject\MockObject;
19+
20+
use function bin2hex;
21+
use function dirname;
22+
use function file_put_contents;
23+
use function is_a;
24+
use function is_dir;
25+
use function is_file;
26+
use function json_encode;
27+
use function mkdir;
28+
use function random_bytes;
29+
use function rmdir;
30+
use function sprintf;
31+
use function sys_get_temp_dir;
32+
use function unlink;
33+
34+
use const DIRECTORY_SEPARATOR;
35+
36+
class PostTagLoaderTest extends UnitTest
37+
{
38+
private string $jsonFile;
39+
40+
protected function setUp(): void
41+
{
42+
parent::setUp();
43+
44+
$this->jsonFile = sprintf(
45+
'%s%slight-post-tag-loader-%s%sarticles_cleaned.json',
46+
sys_get_temp_dir(),
47+
DIRECTORY_SEPARATOR,
48+
bin2hex(random_bytes(8)),
49+
DIRECTORY_SEPARATOR
50+
);
51+
52+
mkdir(dirname($this->jsonFile), 0775, true);
53+
}
54+
55+
protected function tearDown(): void
56+
{
57+
if (is_file($this->jsonFile)) {
58+
unlink($this->jsonFile);
59+
}
60+
61+
$directory = dirname($this->jsonFile);
62+
if (is_dir($directory)) {
63+
rmdir($directory);
64+
}
65+
66+
parent::tearDown();
67+
}
68+
69+
/**
70+
* @throws Exception
71+
*/
72+
public function testLoadRemovesPostTagLinksForTagsNoLongerListedOnTheArticle(): void
73+
{
74+
// The article now only lists "htaccess" — "admin" was removed from the JSON.
75+
$this->writeArticles([
76+
['post_title' => 'A post', 'tags' => [['slug' => 'htaccess', 'name' => 'htaccess']]],
77+
]);
78+
79+
$post = $this->createStub(Post::class);
80+
$post->method('getTitle')->willReturn('A post');
81+
82+
$htaccessTag = $this->createStub(Tag::class);
83+
$htaccessTag->method('getSlug')->willReturn('htaccess');
84+
$htaccessTag->method('getName')->willReturn('htaccess');
85+
86+
$adminTag = $this->createStub(Tag::class);
87+
$adminTag->method('getSlug')->willReturn('admin');
88+
$adminTag->method('getName')->willReturn('admin');
89+
90+
$existingHtaccessLink = $this->createStub(PostTag::class);
91+
$existingHtaccessLink->method('getTag')->willReturn($htaccessTag);
92+
93+
$existingAdminLink = $this->createStub(PostTag::class);
94+
$existingAdminLink->method('getTag')->willReturn($adminTag);
95+
96+
// The DB still has links to both tags from a previous run.
97+
$repository = $this->createStub(EntityRepository::class);
98+
$repository->method('findOneBy')->willReturn($existingHtaccessLink);
99+
$repository->method('findBy')->willReturn([$existingHtaccessLink, $existingAdminLink]);
100+
101+
$manager = $this->createEntityManager($repository);
102+
$manager->expects($this->once())->method('remove')->with($existingAdminLink);
103+
$manager->expects($this->once())->method('flush');
104+
105+
$referenceRepository = $this->createReferenceRepository($manager);
106+
$referenceRepository->addReference('post_1', $post);
107+
$referenceRepository->addReference('tag_htaccess', $htaccessTag);
108+
109+
$loader = new PostTagLoader($this->jsonFile);
110+
$loader->setReferenceRepository($referenceRepository);
111+
$loader->load($manager);
112+
}
113+
114+
/**
115+
* @throws Exception
116+
*/
117+
public function testLoadDoesNotRemoveLinksForTagsStillListedOnTheArticle(): void
118+
{
119+
$this->writeArticles([
120+
['post_title' => 'A post', 'tags' => [['slug' => 'htaccess', 'name' => 'htaccess']]],
121+
]);
122+
123+
$post = $this->createStub(Post::class);
124+
$post->method('getTitle')->willReturn('A post');
125+
126+
$htaccessTag = $this->createStub(Tag::class);
127+
$htaccessTag->method('getSlug')->willReturn('htaccess');
128+
$htaccessTag->method('getName')->willReturn('htaccess');
129+
130+
$existingHtaccessLink = $this->createStub(PostTag::class);
131+
$existingHtaccessLink->method('getTag')->willReturn($htaccessTag);
132+
133+
$repository = $this->createStub(EntityRepository::class);
134+
$repository->method('findOneBy')->willReturn($existingHtaccessLink);
135+
$repository->method('findBy')->willReturn([$existingHtaccessLink]);
136+
137+
$manager = $this->createEntityManager($repository);
138+
$manager->expects($this->never())->method('remove');
139+
140+
$referenceRepository = $this->createReferenceRepository($manager);
141+
$referenceRepository->addReference('post_1', $post);
142+
$referenceRepository->addReference('tag_htaccess', $htaccessTag);
143+
144+
$loader = new PostTagLoader($this->jsonFile);
145+
$loader->setReferenceRepository($referenceRepository);
146+
$loader->load($manager);
147+
}
148+
149+
/**
150+
* @param list<array{post_title: string, tags: list<array{slug: string, name: string}>}> $articles
151+
*/
152+
private function writeArticles(array $articles): void
153+
{
154+
file_put_contents($this->jsonFile, json_encode([
155+
['slug' => 'category', 'articles' => $articles],
156+
]));
157+
}
158+
159+
/**
160+
* @param EntityRepository<PostTag> $repository
161+
* @throws Exception
162+
*/
163+
private function createEntityManager(EntityRepository $repository): EntityManagerInterface&MockObject
164+
{
165+
$manager = $this->createMock(EntityManagerInterface::class);
166+
$manager->method('getRepository')->willReturn($repository);
167+
$manager->method('contains')->willReturn(true);
168+
169+
$unitOfWork = $this->createStub(UnitOfWork::class);
170+
$unitOfWork->method('isInIdentityMap')->willReturn(false);
171+
$manager->method('getUnitOfWork')->willReturn($unitOfWork);
172+
173+
// PHPUnit stubs are dynamically-generated *subclasses* of the entity they stub — mirror
174+
// what Doctrine's real getClassMetadata() does for proxies: resolve back to the real
175+
// mapped class, otherwise ReferenceRepository stores/looks up references under the
176+
// wrong (generated) class name and every hasReference() check silently returns false.
177+
$manager->method('getClassMetadata')->willReturnCallback(function (string $class) {
178+
$realClass = match (true) {
179+
is_a($class, Post::class, true) => Post::class,
180+
is_a($class, Tag::class, true) => Tag::class,
181+
is_a($class, PostTag::class, true) => PostTag::class,
182+
default => $class,
183+
};
184+
185+
$metadata = $this->createStub(ClassMetadata::class);
186+
$metadata->method('getName')->willReturn($realClass);
187+
return $metadata;
188+
});
189+
190+
return $manager;
191+
}
192+
193+
private function createReferenceRepository(EntityManagerInterface $manager): ReferenceRepository
194+
{
195+
return new ReferenceRepository($manager);
196+
}
197+
}

0 commit comments

Comments
 (0)