docs: add implementation plan for awards in database
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
881
docs/superpowers/plans/2026-04-01-awards-bdd.md
Normal file
881
docs/superpowers/plans/2026-04-01-awards-bdd.md
Normal file
@@ -0,0 +1,881 @@
|
||||
# Awards en BDD — Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Persist actor awards in the database during film import and use them for hint generation instead of live Wikidata calls.
|
||||
|
||||
**Architecture:** Two new Doctrine entities (`AwardType`, `Award`) plus a `awardsImported` flag on `Actor`. A new `AwardImporter` service handles Wikidata fetching + pattern-based type resolution during the existing batch import flow. `GameGridGenerator` switches from Wikidata calls to DB queries for award hints.
|
||||
|
||||
**Tech Stack:** Symfony 7, Doctrine ORM, PHPUnit 12, PostgreSQL (RANDOM() function used in existing queries)
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
| Action | Path | Responsibility |
|
||||
|--------|------|---------------|
|
||||
| Create | `src/Entity/AwardType.php` | Doctrine entity for award types |
|
||||
| Create | `src/Entity/Award.php` | Doctrine entity for individual awards |
|
||||
| Create | `src/Repository/AwardTypeRepository.php` | AwardType queries |
|
||||
| Create | `src/Repository/AwardRepository.php` | Award queries (random by actor) |
|
||||
| Create | `src/Service/AwardImporter.php` | Orchestrates Wikidata fetch + type resolution + persist |
|
||||
| Create | `tests/Service/AwardImporterTest.php` | Unit tests for import logic |
|
||||
| Create | `tests/Service/GameGridGeneratorTest.php` | Unit tests for updated hint generation |
|
||||
| Modify | `src/Entity/Actor.php` | Add `awardsImported` bool field + `awards` collection |
|
||||
| Modify | `src/MessageHandler/ImportFilmsBatchMessageHandler.php` | Call `AwardImporter` after actor sync |
|
||||
| Modify | `src/Service/GameGridGenerator.php` | Replace Wikidata call with DB query |
|
||||
| Create | `migrations/VersionXXX.php` | Generated by doctrine:migrations:diff |
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Create `AwardType` entity
|
||||
|
||||
**Files:**
|
||||
- Create: `src/Entity/AwardType.php`
|
||||
- Create: `src/Repository/AwardTypeRepository.php`
|
||||
|
||||
- [ ] **Step 1: Create the entity**
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\AwardTypeRepository;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity(repositoryClass: AwardTypeRepository::class)]
|
||||
class AwardType
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
private string $name;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
private string $pattern;
|
||||
|
||||
/** @var Collection<int, Award> */
|
||||
#[ORM\OneToMany(targetEntity: Award::class, mappedBy: 'awardType')]
|
||||
private Collection $awards;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->awards = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function setName(string $name): static
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPattern(): string
|
||||
{
|
||||
return $this->pattern;
|
||||
}
|
||||
|
||||
public function setPattern(string $pattern): static
|
||||
{
|
||||
$this->pattern = $pattern;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @return Collection<int, Award> */
|
||||
public function getAwards(): Collection
|
||||
{
|
||||
return $this->awards;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Create the repository**
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\AwardType;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/** @extends ServiceEntityRepository<AwardType> */
|
||||
class AwardTypeRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, AwardType::class);
|
||||
}
|
||||
|
||||
/** @return list<AwardType> */
|
||||
public function findAll(): array
|
||||
{
|
||||
return parent::findAll();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add src/Entity/AwardType.php src/Repository/AwardTypeRepository.php
|
||||
git commit -m "feat: add AwardType entity and repository"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Create `Award` entity
|
||||
|
||||
**Files:**
|
||||
- Create: `src/Entity/Award.php`
|
||||
- Create: `src/Repository/AwardRepository.php`
|
||||
|
||||
- [ ] **Step 1: Create the entity**
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\AwardRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity(repositoryClass: AwardRepository::class)]
|
||||
class Award
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: AwardType::class, inversedBy: 'awards')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private AwardType $awardType;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Actor::class, inversedBy: 'awards')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private Actor $actor;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
private string $name;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?int $year = null;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getAwardType(): AwardType
|
||||
{
|
||||
return $this->awardType;
|
||||
}
|
||||
|
||||
public function setAwardType(AwardType $awardType): static
|
||||
{
|
||||
$this->awardType = $awardType;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getActor(): Actor
|
||||
{
|
||||
return $this->actor;
|
||||
}
|
||||
|
||||
public function setActor(Actor $actor): static
|
||||
{
|
||||
$this->actor = $actor;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function setName(string $name): static
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getYear(): ?int
|
||||
{
|
||||
return $this->year;
|
||||
}
|
||||
|
||||
public function setYear(?int $year): static
|
||||
{
|
||||
$this->year = $year;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Create the repository with `findOneRandomByActor`**
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Award;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/** @extends ServiceEntityRepository<Award> */
|
||||
class AwardRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, Award::class);
|
||||
}
|
||||
|
||||
public function findOneRandomByActor(int $actorId): ?Award
|
||||
{
|
||||
return $this->createQueryBuilder('a')
|
||||
->andWhere('a.actor = :actorId')
|
||||
->setParameter('actorId', $actorId)
|
||||
->orderBy('RANDOM()')
|
||||
->setMaxResults(1)
|
||||
->getQuery()
|
||||
->getOneOrNullResult();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add src/Entity/Award.php src/Repository/AwardRepository.php
|
||||
git commit -m "feat: add Award entity and repository"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Add `awardsImported` flag and `awards` collection to `Actor`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/Entity/Actor.php`
|
||||
|
||||
- [ ] **Step 1: Add the `awardsImported` column and `awards` OneToMany relation**
|
||||
|
||||
Add these properties after the existing `$tmdbId` property:
|
||||
|
||||
```php
|
||||
#[ORM\Column(options: ['default' => false])]
|
||||
private bool $awardsImported = false;
|
||||
|
||||
/** @var Collection<int, Award> */
|
||||
#[ORM\OneToMany(targetEntity: Award::class, mappedBy: 'actor')]
|
||||
private Collection $awards;
|
||||
```
|
||||
|
||||
Add `use App\Entity\Award;` import if not already present (it's in the same namespace, so not needed).
|
||||
|
||||
In the constructor, add:
|
||||
|
||||
```php
|
||||
$this->awards = new ArrayCollection();
|
||||
```
|
||||
|
||||
Add these methods at the end of the class:
|
||||
|
||||
```php
|
||||
public function isAwardsImported(): bool
|
||||
{
|
||||
return $this->awardsImported;
|
||||
}
|
||||
|
||||
public function setAwardsImported(bool $awardsImported): static
|
||||
{
|
||||
$this->awardsImported = $awardsImported;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @return Collection<int, Award> */
|
||||
public function getAwards(): Collection
|
||||
{
|
||||
return $this->awards;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add src/Entity/Actor.php
|
||||
git commit -m "feat: add awardsImported flag and awards relation to Actor"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Generate and run the migration
|
||||
|
||||
**Files:**
|
||||
- Create: `migrations/VersionXXX.php` (auto-generated)
|
||||
|
||||
- [ ] **Step 1: Generate the migration**
|
||||
|
||||
```bash
|
||||
php bin/console doctrine:migrations:diff
|
||||
```
|
||||
|
||||
Expected: a new migration file is created in `migrations/` with CREATE TABLE for `award_type` and `award`, and ALTER TABLE for `actor` adding `awards_imported`.
|
||||
|
||||
- [ ] **Step 2: Review the generated migration**
|
||||
|
||||
Open the generated file and verify it contains:
|
||||
- `CREATE TABLE award_type (id SERIAL, name VARCHAR(255), pattern VARCHAR(255), PRIMARY KEY(id))`
|
||||
- `CREATE TABLE award (id SERIAL, award_type_id INT NOT NULL, actor_id INT NOT NULL, name VARCHAR(255), year INT DEFAULT NULL, PRIMARY KEY(id))` with foreign keys
|
||||
- `ALTER TABLE actor ADD awards_imported BOOLEAN NOT NULL DEFAULT false`
|
||||
|
||||
- [ ] **Step 3: Run the migration**
|
||||
|
||||
```bash
|
||||
php bin/console doctrine:migrations:migrate --no-interaction
|
||||
```
|
||||
|
||||
Expected: migration executes successfully.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add migrations/
|
||||
git commit -m "feat: add migration for award_type, award tables and actor.awards_imported"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Create `AwardImporter` service
|
||||
|
||||
**Files:**
|
||||
- Create: `src/Service/AwardImporter.php`
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
Create `tests/Service/AwardImporterTest.php`:
|
||||
|
||||
```php
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the test to verify it fails**
|
||||
|
||||
```bash
|
||||
php bin/phpunit tests/Service/AwardImporterTest.php
|
||||
```
|
||||
|
||||
Expected: FAIL — `AwardImporter` class not found.
|
||||
|
||||
- [ ] **Step 3: Write the implementation**
|
||||
|
||||
Create `src/Service/AwardImporter.php`:
|
||||
|
||||
```php
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run the tests to verify they pass**
|
||||
|
||||
```bash
|
||||
php bin/phpunit tests/Service/AwardImporterTest.php
|
||||
```
|
||||
|
||||
Expected: all 5 tests PASS.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/Service/AwardImporter.php tests/Service/AwardImporterTest.php
|
||||
git commit -m "feat: add AwardImporter service with tests"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Integrate `AwardImporter` into the import batch handler
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/MessageHandler/ImportFilmsBatchMessageHandler.php`
|
||||
|
||||
- [ ] **Step 1: Add `AwardImporter` dependency**
|
||||
|
||||
In the constructor, add:
|
||||
|
||||
```php
|
||||
private AwardImporter $awardImporter,
|
||||
```
|
||||
|
||||
Add the import at the top:
|
||||
|
||||
```php
|
||||
use App\Service\AwardImporter;
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Call `AwardImporter` after actor sync**
|
||||
|
||||
In the `__invoke` method, after `$this->actorSyncer->syncActorsForMovie($movie);` (line 63) and **before** the existing `$this->em->flush()` (line 77), add:
|
||||
|
||||
```php
|
||||
$this->actorSyncer->syncActorsForMovie($movie);
|
||||
|
||||
// Import awards for actors of this movie
|
||||
foreach ($movie->getActors() as $role) {
|
||||
$this->awardImporter->importForActor($role->getActor());
|
||||
}
|
||||
```
|
||||
|
||||
`Movie::getActors()` returns `Collection<MovieRole>`. The `ActorSyncer` persists actors/roles in memory before flush, so the collection is hydrated at this point. The existing `$this->em->flush()` on line 77 will persist both the roles and the new awards in a single flush. The `$this->em->clear()` on line 87 happens after, so all entities are still available.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add src/MessageHandler/ImportFilmsBatchMessageHandler.php
|
||||
git commit -m "feat: import actor awards during film batch import"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Update `GameGridGenerator` to use DB for award hints
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/Service/GameGridGenerator.php`
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
Create `tests/Service/GameGridGeneratorTest.php` — test only the hint resolution, not the full game generation (which requires DB):
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Service;
|
||||
|
||||
use App\Entity\Actor;
|
||||
use App\Entity\Award;
|
||||
use App\Entity\AwardType;
|
||||
use App\Entity\GameRow;
|
||||
use App\Repository\ActorRepository;
|
||||
use App\Repository\AwardRepository;
|
||||
use App\Repository\MovieRepository;
|
||||
use App\Repository\MovieRoleRepository;
|
||||
use App\Service\GameGridGenerator;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class GameGridGeneratorTest extends TestCase
|
||||
{
|
||||
public function testResolveHintTextForAward(): void
|
||||
{
|
||||
$awardType = new AwardType();
|
||||
$awardType->setName('Oscar')->setPattern('Academy Award');
|
||||
|
||||
$actor = new Actor();
|
||||
$actor->setName('Test Actor');
|
||||
|
||||
$award = new Award();
|
||||
$award->setName('Academy Award for Best Actor');
|
||||
$award->setYear(2020);
|
||||
$award->setActor($actor);
|
||||
$award->setAwardType($awardType);
|
||||
|
||||
$awardRepository = $this->createMock(AwardRepository::class);
|
||||
$awardRepository->method('find')->with(42)->willReturn($award);
|
||||
|
||||
$generator = new GameGridGenerator(
|
||||
$this->createMock(ActorRepository::class),
|
||||
$this->createMock(MovieRoleRepository::class),
|
||||
$this->createMock(MovieRepository::class),
|
||||
$awardRepository,
|
||||
$this->createMock(EntityManagerInterface::class),
|
||||
);
|
||||
|
||||
$row = new GameRow();
|
||||
$row->setHintType('award');
|
||||
$row->setHintData('42');
|
||||
|
||||
// Use reflection to test the private resolveHintText method
|
||||
$method = new \ReflectionMethod($generator, 'resolveHintText');
|
||||
$result = $method->invoke($generator, $row);
|
||||
|
||||
$this->assertSame('Academy Award for Best Actor (2020)', $result);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the test to verify it fails**
|
||||
|
||||
```bash
|
||||
php bin/phpunit tests/Service/GameGridGeneratorTest.php
|
||||
```
|
||||
|
||||
Expected: FAIL — constructor signature mismatch (no `AwardRepository` param yet).
|
||||
|
||||
- [ ] **Step 3: Update `GameGridGenerator`**
|
||||
|
||||
Replace `WikidataGateway` with `AwardRepository` in the constructor. Full changes to `src/Service/GameGridGenerator.php`:
|
||||
|
||||
**Replace the imports:**
|
||||
- Remove: `use App\Gateway\WikidataGateway;`
|
||||
- Add: `use App\Repository\AwardRepository;`
|
||||
|
||||
**Replace the constructor:**
|
||||
|
||||
```php
|
||||
public function __construct(
|
||||
private readonly ActorRepository $actorRepository,
|
||||
private readonly MovieRoleRepository $movieRoleRepository,
|
||||
private readonly MovieRepository $movieRepository,
|
||||
private readonly AwardRepository $awardRepository,
|
||||
private readonly EntityManagerInterface $em,
|
||||
) {}
|
||||
```
|
||||
|
||||
**Replace the `case 'award':` block in `resolveHint()`** (lines 175-185):
|
||||
|
||||
```php
|
||||
case 'award':
|
||||
$award = $this->awardRepository->findOneRandomByActor($rowActor->getId());
|
||||
if ($award === null) {
|
||||
return null;
|
||||
}
|
||||
return ['type' => 'award', 'data' => (string) $award->getId()];
|
||||
```
|
||||
|
||||
**Replace the `'award'` case in `resolveHintText()`** (line 203):
|
||||
|
||||
```php
|
||||
'award' => $this->resolveAwardHintText((int) $data),
|
||||
```
|
||||
|
||||
**Add this private method:**
|
||||
|
||||
```php
|
||||
private function resolveAwardHintText(int $awardId): ?string
|
||||
{
|
||||
$award = $this->awardRepository->find($awardId);
|
||||
if ($award === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$text = $award->getName();
|
||||
if ($award->getYear() !== null) {
|
||||
$text .= ' (' . $award->getYear() . ')';
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run the tests**
|
||||
|
||||
```bash
|
||||
php bin/phpunit tests/Service/GameGridGeneratorTest.php
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Run all tests**
|
||||
|
||||
```bash
|
||||
php bin/phpunit
|
||||
```
|
||||
|
||||
Expected: all tests PASS.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add src/Service/GameGridGenerator.php tests/Service/GameGridGeneratorTest.php
|
||||
git commit -m "feat: use DB awards instead of live Wikidata calls for hint generation"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 8: Manual verification
|
||||
|
||||
- [ ] **Step 1: Verify the schema is in sync**
|
||||
|
||||
```bash
|
||||
php bin/console doctrine:schema:validate
|
||||
```
|
||||
|
||||
Expected: schema is in sync with mapping files.
|
||||
|
||||
- [ ] **Step 2: Verify the app boots**
|
||||
|
||||
```bash
|
||||
php bin/console cache:clear
|
||||
```
|
||||
|
||||
Expected: no errors.
|
||||
|
||||
- [ ] **Step 3: Run all tests one final time**
|
||||
|
||||
```bash
|
||||
php bin/phpunit
|
||||
```
|
||||
|
||||
Expected: all tests PASS.
|
||||
|
||||
- [ ] **Step 4: Final commit if any fixes were needed**
|
||||
|
||||
Only if adjustments were made during verification.
|
||||
Reference in New Issue
Block a user