diff --git a/src/Service/AwardImporter.php b/src/Service/AwardImporter.php new file mode 100644 index 0000000..b2d106b --- /dev/null +++ b/src/Service/AwardImporter.php @@ -0,0 +1,89 @@ +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 $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; + } +} diff --git a/tests/Service/AwardImporterTest.php b/tests/Service/AwardImporterTest.php new file mode 100644 index 0000000..b6fdb61 --- /dev/null +++ b/tests/Service/AwardImporterTest.php @@ -0,0 +1,139 @@ +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; + } +}