diff --git a/scraper.php b/scraper.php index 3fd13d6a..0168d3ae 100644 --- a/scraper.php +++ b/scraper.php @@ -6,6 +6,7 @@ use BOA\Programs\ProgramScraper; use BOA\Programs\ProgramStorage; +use BOA\Programs\ScraperAdapter; use BVP\Scraper\Scraper; use Carbon\CarbonImmutable as Carbon; @@ -17,9 +18,9 @@ // v2 の場合のみ ProgramScraper を利用して出走表データを取得 if ($version === 'v2') { - $scraper = new ProgramScraper( - Scraper::getInstance() - ); + $scraperInstance = Scraper::getInstance(); + $scraperAdapter = new ScraperAdapter($scraperInstance); + $scraper = new ProgramScraper($scraperAdapter); // 指定日付の出走表データをスクレイピング $programs = $scraper->scrape($date); diff --git a/src/ProgramScraper.php b/src/ProgramScraper.php index c05e1e84..3eb9d051 100644 --- a/src/ProgramScraper.php +++ b/src/ProgramScraper.php @@ -4,7 +4,6 @@ namespace BOA\Programs; -use BVP\Scraper\Scraper; use Carbon\CarbonImmutable as Carbon; use Carbon\CarbonInterface; @@ -14,9 +13,9 @@ final class ProgramScraper { /** - * @param \BVP\Scraper\Scraper $scraper + * @param \BOA\Programs\ScraperInterface $scraper */ - public function __construct(private readonly Scraper $scraper) + public function __construct(private readonly ScraperInterface $scraper) { // } diff --git a/src/ScraperAdapter.php b/src/ScraperAdapter.php new file mode 100644 index 00000000..84453169 --- /dev/null +++ b/src/ScraperAdapter.php @@ -0,0 +1,72 @@ + + * } + * @psalm-type ScrapedRaces = array + * @psalm-type ScrapedStadiumRaces = array + * + * @param \Carbon\CarbonInterface $date + * @return ScrapedStadiumRaces + */ + #[\Override] + public function scrapePrograms(CarbonInterface $date): array + { + /** @psalm-var ScrapedStadiumRaces */ + return $this->scraper->scrapePrograms($date); + } +} diff --git a/src/ScraperInterface.php b/src/ScraperInterface.php new file mode 100644 index 00000000..e58982b2 --- /dev/null +++ b/src/ScraperInterface.php @@ -0,0 +1,58 @@ + + * } + * @psalm-type ScrapedRaces = array + * @psalm-type ScrapedStadiumRaces = array + * + * @param \Carbon\CarbonInterface $date + * @return ScrapedStadiumRaces + */ + public function scrapePrograms(CarbonInterface $date): array; +} diff --git a/tests/ProgramScraperTest.php b/tests/ProgramScraperTest.php new file mode 100644 index 00000000..aa5e10b2 --- /dev/null +++ b/tests/ProgramScraperTest.php @@ -0,0 +1,120 @@ +createMock(ScraperInterface::class); + $mockScraper->method('scrapePrograms') + ->with(Carbon::create(2025, 7, 15)) + ->willReturn([ + $this->testScrapeData(1), + ]); + $scraper = new ProgramScraper($mockScraper); + $programs = $scraper->scrape(Carbon::create(2025, 7, 15)); + $this->assertSame($this->testScrapeData(0), $programs); + } + + /** + * @psalm-type NormalizedBoat array{ + * racer_boat_number: int, + * racer_name: string, + * racer_number: int, + * racer_class_number: int, + * racer_branch_number: int, + * racer_birthplace_number: int, + * racer_age: int, + * racer_weight: int|float, + * racer_flying_count: int, + * racer_late_count: int, + * racer_average_start_timing: float, + * racer_national_top_1_percent: float, + * racer_national_top_2_percent: float, + * racer_national_top_3_percent: float, + * racer_local_top_1_percent: float, + * racer_local_top_2_percent: float, + * racer_local_top_3_percent: float, + * racer_assigned_motor_number: int, + * racer_assigned_motor_top_2_percent: float, + * racer_assigned_motor_top_3_percent: float, + * racer_assigned_boat_number: int, + * racer_assigned_boat_top_2_percent: float, + * racer_assigned_boat_top_3_percent: float + * } + * + * @psalm-type NormalizedRace array{ + * race_date: string, + * race_stadium_number: int, + * race_number: int, + * race_closed_at: string, + * race_grade_number: int, + * race_title: string, + * race_subtitle: string, + * race_distance: int, + * boats: array + * } + * + * @psalm-type NormalizedRaces array + * + * @param int $keyIndex + * @return NormalizedRaces + */ + private function testScrapeData(int $keyIndex): array + { + return [ + $keyIndex => [ + 'race_date' => '2025-07-15', + 'race_stadium_number' => 1, + 'race_number' => 1, + 'race_closed_at' => '2025-07-15 15:17:00', + 'race_grade_number' => 5, + 'race_title' => 'にっぽん未来プロジェクト競走in桐生', + 'race_subtitle' => '予選', + 'race_distance' => 1800, + 'boats' => [ + $keyIndex => [ + 'racer_boat_number' => 1, + 'racer_name' => '松本 浩貴', + 'racer_number' => 3860, + 'racer_class_number' => 3, + 'racer_branch_number' => 11, + 'racer_birthplace_number' => 11, + 'racer_age' => 52, + 'racer_weight' => 52, + 'racer_flying_count' => 0, + 'racer_late_count' => 0, + 'racer_average_start_timing' => 0.19, + 'racer_national_top_1_percent' => 4.09, + 'racer_national_top_2_percent' => 18.48, + 'racer_national_top_3_percent' => 39.13, + 'racer_local_top_1_percent' => 4.85, + 'racer_local_top_2_percent' => 33.33, + 'racer_local_top_3_percent' => 40.74, + 'racer_assigned_motor_number' => 24, + 'racer_assigned_motor_top_2_percent' => 28, + 'racer_assigned_motor_top_3_percent' => 42.67, + 'racer_assigned_boat_number' => 23, + 'racer_assigned_boat_top_2_percent' => 30.47, + 'racer_assigned_boat_top_3_percent' => 51.56, + ], + ], + ], + ]; + } +} diff --git a/tests/ProgramStorageTest.php b/tests/ProgramStorageTest.php new file mode 100644 index 00000000..d4d321a1 --- /dev/null +++ b/tests/ProgramStorageTest.php @@ -0,0 +1,105 @@ +tempDir = sys_get_temp_dir() . '/program_storage_test_' . uniqid(); + mkdir($this->tempDir, 0777, true); + } + + /** + * @return void + */ + protected function tearDown(): void + { + if (is_dir($this->tempDir)) { + $files = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($this->tempDir, \FilesystemIterator::SKIP_DOTS), + \RecursiveIteratorIterator::CHILD_FIRST + ); + + foreach ($files as $file) { + $file->isDir() ? rmdir($file->getRealPath()) : unlink($file->getRealPath()); + } + + rmdir($this->tempDir); + } + } + + /** + * @return void + */ + public function testSave(): void + { + $storage = new ProgramStorage(); + $path = $this->tempDir . '/programs.json'; + + $programs = [ + [ + 'race_date' => '2025-07-15', + 'race_stadium_number' => 1, + 'race_number' => 1, + 'race_closed_at' => '2025-07-15 15:17:00', + 'race_grade_number' => 5, + 'race_title' => 'にっぽん未来プロジェクト競走in桐生', + 'race_subtitle' => '予選', + 'race_distance' => 1800, + 'boats' => [ + [ + 'racer_boat_number' => 1, + 'racer_name' => '松本 浩貴', + 'racer_number' => 3860, + 'racer_class_number' => 3, + 'racer_branch_number' => 11, + 'racer_birthplace_number' => 11, + 'racer_age' => 52, + 'racer_weight' => 52, + 'racer_flying_count' => 0, + 'racer_late_count' => 0, + 'racer_average_start_timing' => 0.19, + 'racer_national_top_1_percent' => 4.09, + 'racer_national_top_2_percent' => 18.48, + 'racer_national_top_3_percent' => 39.13, + 'racer_local_top_1_percent' => 4.85, + 'racer_local_top_2_percent' => 33.33, + 'racer_local_top_3_percent' => 40.74, + 'racer_assigned_motor_number' => 24, + 'racer_assigned_motor_top_2_percent' => 28, + 'racer_assigned_motor_top_3_percent' => 42.67, + 'racer_assigned_boat_number' => 23, + 'racer_assigned_boat_top_2_percent' => 30.47, + 'racer_assigned_boat_top_3_percent' => 51.56, + ], + ], + ], + ]; + + $storage->save($programs, $path); + + $this->assertFileExists($path); + + $content = json_decode(file_get_contents($path), true); + $this->assertArrayHasKey('programs', $content); + $this->assertSame($programs, $content['programs']); + } +}