feat: add AwardImporter service with tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
thibaud-leclere
2026-04-01 14:27:15 +02:00
parent d4d2272396
commit 8aa33ccefc
2 changed files with 228 additions and 0 deletions

View File

@@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace App\Service;
use App\Entity\Actor;
use App\Entity\Award;
use App\Entity\AwardType;
use App\Gateway\WikidataGateway;
use App\Repository\AwardTypeRepository;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
readonly class AwardImporter
{
public function __construct(
private WikidataGateway $wikidataGateway,
private AwardTypeRepository $awardTypeRepository,
private EntityManagerInterface $em,
private ?LoggerInterface $logger = null,
) {}
public function importForActor(Actor $actor): void
{
if ($actor->isAwardsImported()) {
return;
}
try {
$wikidataAwards = $this->wikidataGateway->getAwards($actor);
} catch (\Throwable $e) {
$this->logger?->warning('Failed to fetch awards from Wikidata', [
'actor' => $actor->getName(),
'error' => $e->getMessage(),
]);
return;
}
$knownTypes = $this->awardTypeRepository->findAll();
foreach ($wikidataAwards as $wikidataAward) {
$awardType = $this->resolveAwardType($wikidataAward['name'], $knownTypes);
$award = new Award();
$award->setName($wikidataAward['name']);
$award->setYear($wikidataAward['year']);
$award->setActor($actor);
$award->setAwardType($awardType);
$this->em->persist($award);
}
$actor->setAwardsImported(true);
}
/**
* @param list<AwardType> $knownTypes
*/
private function resolveAwardType(string $awardName, array &$knownTypes): AwardType
{
foreach ($knownTypes as $type) {
if (str_contains($awardName, $type->getPattern())) {
return $type;
}
}
$newType = new AwardType();
$prefix = $this->extractPrefix($awardName);
$newType->setName($prefix);
$newType->setPattern($prefix);
$this->em->persist($newType);
$knownTypes[] = $newType;
return $newType;
}
private function extractPrefix(string $awardName): string
{
// Extract text before " for " or " pour " (common patterns in award names)
if (preg_match('/^(.+?)\s+(?:for|pour)\s+/i', $awardName, $matches)) {
return trim($matches[1]);
}
return $awardName;
}
}

View File

@@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
namespace App\Tests\Service;
use App\Entity\Actor;
use App\Entity\Award;
use App\Entity\AwardType;
use App\Gateway\WikidataGateway;
use App\Repository\AwardTypeRepository;
use App\Service\AwardImporter;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\TestCase;
class AwardImporterTest extends TestCase
{
private AwardImporter $importer;
private WikidataGateway&\PHPUnit\Framework\MockObject\MockObject $wikidataGateway;
private AwardTypeRepository&\PHPUnit\Framework\MockObject\MockObject $awardTypeRepository;
private EntityManagerInterface&\PHPUnit\Framework\MockObject\MockObject $em;
protected function setUp(): void
{
$this->wikidataGateway = $this->createMock(WikidataGateway::class);
$this->awardTypeRepository = $this->createMock(AwardTypeRepository::class);
$this->em = $this->createMock(EntityManagerInterface::class);
$this->importer = new AwardImporter(
$this->wikidataGateway,
$this->awardTypeRepository,
$this->em,
);
}
public function testSkipsActorWithAwardsAlreadyImported(): void
{
$actor = $this->createActorWithFlag(awardsImported: true);
$this->wikidataGateway->expects($this->never())->method('getAwards');
$this->importer->importForActor($actor);
}
public function testImportsAwardsAndSetsFlag(): void
{
$actor = $this->createActorWithFlag(awardsImported: false);
$this->wikidataGateway->method('getAwards')->willReturn([
['name' => 'Academy Award for Best Actor', 'year' => 2020],
]);
$existingType = new AwardType();
$existingType->setName('Oscar')->setPattern('Academy Award');
$this->awardTypeRepository->method('findAll')->willReturn([$existingType]);
$persisted = [];
$this->em->method('persist')->willReturnCallback(function ($entity) use (&$persisted) {
$persisted[] = $entity;
});
$this->importer->importForActor($actor);
$this->assertTrue($actor->isAwardsImported());
$this->assertCount(1, $persisted);
$this->assertInstanceOf(Award::class, $persisted[0]);
$this->assertSame('Academy Award for Best Actor', $persisted[0]->getName());
$this->assertSame(2020, $persisted[0]->getYear());
$this->assertSame($existingType, $persisted[0]->getAwardType());
$this->assertSame($actor, $persisted[0]->getActor());
}
public function testCreatesNewAwardTypeWhenNoPatternMatches(): void
{
$actor = $this->createActorWithFlag(awardsImported: false);
$this->wikidataGateway->method('getAwards')->willReturn([
['name' => 'Screen Actors Guild Award for Outstanding Performance', 'year' => 2019],
]);
$this->awardTypeRepository->method('findAll')->willReturn([]);
$persisted = [];
$this->em->method('persist')->willReturnCallback(function ($entity) use (&$persisted) {
$persisted[] = $entity;
});
$this->importer->importForActor($actor);
$this->assertTrue($actor->isAwardsImported());
// Should persist both a new AwardType and the Award
$this->assertCount(2, $persisted);
$newType = $persisted[0];
$this->assertInstanceOf(AwardType::class, $newType);
$this->assertSame('Screen Actors Guild Award', $newType->getName());
$this->assertSame('Screen Actors Guild Award', $newType->getPattern());
$award = $persisted[1];
$this->assertInstanceOf(Award::class, $award);
$this->assertSame($newType, $award->getAwardType());
}
public function testDoesNotSetFlagOnWikidataError(): void
{
$actor = $this->createActorWithFlag(awardsImported: false);
$this->wikidataGateway->method('getAwards')
->willThrowException(new \RuntimeException('Wikidata timeout'));
$this->importer->importForActor($actor);
$this->assertFalse($actor->isAwardsImported());
}
public function testHandlesActorWithNoAwards(): void
{
$actor = $this->createActorWithFlag(awardsImported: false);
$this->wikidataGateway->method('getAwards')->willReturn([]);
$this->awardTypeRepository->method('findAll')->willReturn([]);
$this->em->expects($this->never())->method('persist');
$this->importer->importForActor($actor);
$this->assertTrue($actor->isAwardsImported());
}
private function createActorWithFlag(bool $awardsImported): Actor
{
$actor = new Actor();
$actor->setName('Test Actor');
$actor->setAwardsImported($awardsImported);
return $actor;
}
}