feat: suite du passage en DDD de Scraping

This commit is contained in:
ext.jeremy.guillot@maxicoffee.domains
2025-02-01 13:59:37 +01:00
committed by ThysTips
parent 0e3d72cc5e
commit 97d7bcf061
4 changed files with 190 additions and 44 deletions

View File

@@ -3,7 +3,9 @@
namespace App\Domain\Scraping\Infrastructure\Persistence;
use App\Domain\Scraping\Domain\Model\ScrapingJob;
use App\Domain\Scraping\Domain\Model\ScrapingStatus;
use App\Domain\Scraping\Domain\Repository\ScrapingJobRepositoryInterface;
use App\Domain\Scraping\Infrastructure\Persistence\Entity\ScrapingJobEntity;
use Doctrine\ORM\EntityManagerInterface;
class DoctrineScrapingJobRepository implements ScrapingJobRepositoryInterface
@@ -14,38 +16,48 @@ class DoctrineScrapingJobRepository implements ScrapingJobRepositoryInterface
public function save(ScrapingJob $job): void
{
$this->entityManager->persist($job);
$entity = ScrapingJobEntity::fromDomain($job);
$this->entityManager->persist($entity);
$this->entityManager->flush();
}
public function findById(string $id): ?ScrapingJob
{
return $this->entityManager->getRepository(ScrapingJob::class)->find($id);
$entity = $this->entityManager->getRepository(ScrapingJobEntity::class)
->find($id);
return $entity?->toDomain();
}
public function findByChapterId(string $chapterId): ?ScrapingJob
{
return $this->entityManager->getRepository(ScrapingJob::class)
$entity = $this->entityManager->getRepository(ScrapingJobEntity::class)
->findOneBy(['chapterId' => $chapterId]);
return $entity?->toDomain();
}
public function findPendingJobs(): array
{
return $this->entityManager->getRepository(ScrapingJob::class)
$entities = $this->entityManager->getRepository(ScrapingJobEntity::class)
->createQueryBuilder('sj')
->where('sj.status = :status')
->setParameter('status', 'pending')
->setParameter('status', ScrapingStatus::PENDING->value)
->getQuery()
->getResult();
return array_map(fn(ScrapingJobEntity $entity) => $entity->toDomain(), $entities);
}
public function findInProgressJobs(): array
{
return $this->entityManager->getRepository(ScrapingJob::class)
$entities = $this->entityManager->getRepository(ScrapingJobEntity::class)
->createQueryBuilder('sj')
->where('sj.status = :status')
->setParameter('status', 'in_progress')
->setParameter('status', ScrapingStatus::IN_PROGRESS->value)
->getQuery()
->getResult();
return array_map(fn(ScrapingJobEntity $entity) => $entity->toDomain(), $entities);
}
}

View File

@@ -0,0 +1,94 @@
<?php
namespace App\Domain\Scraping\Infrastructure\Service\Scraper;
use App\Domain\Scraping\Domain\Contract\ScraperInterface;
use App\Domain\Scraping\Domain\Model\ScrapingJob;
use App\Domain\Scraping\Domain\Event\PageScrapingProgressed;
use App\Domain\Scraping\Domain\Event\ChapterScrapingCompleted;
use App\Domain\Scraping\Domain\Event\ChapterScrapingStarted;
use App\Domain\Scraping\Domain\Model\ScrapingProgress;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
abstract class AbstractScraper implements ScraperInterface
{
public function __construct(
protected readonly HttpClientInterface $httpClient,
protected readonly EventDispatcherInterface $eventDispatcher,
protected readonly string $tempDir
) {}
public function createScrapingJob(string $chapterId, string $sourceId): ScrapingJob
{
return new ScrapingJob(
uniqid('scraping_'),
$chapterId,
$sourceId
);
}
public function scrape(ScrapingJob $job): void
{
try {
$this->eventDispatcher->dispatch(new ChapterScrapingStarted($job->getId()));
$tempDir = $this->createTempDirectory($job);
$pageData = $this->scrapePages($job);
foreach ($pageData as $page) {
$this->downloadPage($job, $page, $tempDir);
}
$job->complete();
$this->eventDispatcher->dispatch(
new ChapterScrapingCompleted($job->getId(), $job->getPages())
);
$this->cleanupTempDirectory($tempDir);
} catch (\Exception $e) {
$job->fail();
throw $e;
}
}
abstract protected function scrapePages(ScrapingJob $job): array;
protected function createTempDirectory(ScrapingJob $job): string
{
$tempDir = $this->tempDir . '/' . uniqid('scraping_' . $job->getId() . '_');
if (!mkdir($tempDir) && !is_dir($tempDir)) {
throw new \RuntimeException("Failed to create temporary directory: $tempDir");
}
return $tempDir;
}
protected function cleanupTempDirectory(string $tempDir): void
{
if (is_dir($tempDir)) {
$files = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($tempDir, \RecursiveDirectoryIterator::SKIP_DOTS),
\RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($files as $file) {
if ($file->isDir()) {
rmdir($file->getRealPath());
} else {
unlink($file->getRealPath());
}
}
rmdir($tempDir);
}
}
protected function dispatchProgressEvent(ScrapingJob $job, int $current, int $total): void
{
$progress = new ScrapingProgress($current, $total);
$this->eventDispatcher->dispatch(
new PageScrapingProgressed($job->getId(), $progress)
);
}
}

View File

@@ -2,60 +2,62 @@
namespace App\Domain\Scraping\Infrastructure\Service\Scraper;
use App\Domain\Scraping\Domain\Contract\ScraperInterface as ContractScraperInterface;
use App\Domain\Scraping\Domain\Service\ScraperInterface;
use App\Domain\Scraping\Domain\Model\ScrapingJob;
use App\Domain\Scraping\Domain\Model\ValueObject\ImageUrl;
use App\Domain\Scraping\Domain\Model\ValueObject\PageNumber;
use App\Domain\Scraping\Domain\Event\PageScrapingProgressed;
use App\Domain\Scraping\Domain\Event\ChapterScrapingCompleted;
use Symfony\Component\DomCrawler\Crawler;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class HtmlScraper implements ContractScraperInterface
class HtmlScraper extends AbstractScraper
{
public function __construct(
private readonly HttpClientInterface $httpClient,
private readonly EventDispatcherInterface $eventDispatcher
) {}
public function createScrapingJob(string $chapterId, string $sourceId): ScrapingJob
protected function scrapePages(ScrapingJob $job): array
{
return new ScrapingJob(
uniqid('scraping_'),
$chapterId,
$sourceId
);
}
public function scrape(ScrapingJob $job): void
{
$url = $this->buildUrl($job); // À implémenter selon votre logique
$url = $this->buildUrl($job);
$response = $this->httpClient->request('GET', $url);
$crawler = new Crawler($response->getContent());
$images = $crawler->filter('img.manga-page'); // Adapter selon le site cible
$images = $crawler->filter('img.manga-page'); // Adapter selon le site
$pageNumber = 1;
$images->each(function (Crawler $image) use ($job, $pageNumber) {
$imageUrl = new ImageUrl($image->attr('src'));
$job->addPage(new PageNumber($pageNumber), $imageUrl);
$this->eventDispatcher->dispatch(
new PageScrapingProgressed($job->getId(), $job->getProgress())
);
$pageNumber++;
$pages = [];
$images->each(function (Crawler $image) use (&$pages) {
$pages[] = [
'url' => $image->attr('src'),
'number' => count($pages) + 1
];
});
$this->eventDispatcher->dispatch(
new ChapterScrapingCompleted($job->getId(), $job->getPages())
return $pages;
}
protected function downloadPage(ScrapingJob $job, array $page, string $tempDir): void
{
$imageUrl = new ImageUrl($page['url']);
$pageNumber = new PageNumber($page['number']);
$fileName = sprintf('%s/%03d.%s',
$tempDir,
$pageNumber->getValue(),
$imageUrl->getExtension()
);
$response = $this->httpClient->request('GET', $imageUrl->getValue());
file_put_contents($fileName, $response->getContent());
$job->addPage($pageNumber, $imageUrl);
$this->dispatchProgressEvent($job, $page['number'], count($pages));
}
public function supports(string $sourceType): bool
{
return $sourceType === 'html';
}
}
private function buildUrl(ScrapingJob $job): string
{
// À implémenter selon votre logique de construction d'URL
// Vous aurez probablement besoin d'injecter un service pour récupérer les informations du chapitre
return sprintf('https://example.com/manga/%s/chapter/%s',
$job->getMangaId(),
$job->getChapterId()
);
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace App\Domain\Scraping\Infrastructure\Service\Scraper;
use App\Domain\Scraping\Domain\Model\ScrapingJob;
use Symfony\Component\Panther\Client as PantherClient;
class JavascriptScraper extends AbstractScraper
{
protected function scrapePages(ScrapingJob $job): array
{
$client = PantherClient::createChromeClient();
try {
$url = $this->buildUrl($job);
$crawler = $client->request('GET', $url);
// Attendre que les images soient chargées
$crawler->waitFor('img.manga-page');
$pages = [];
$crawler->filter('img.manga-page')->each(function ($image) use (&$pages) {
$pages[] = [
'url' => $image->attr('src'),
'number' => count($pages) + 1
];
});
return $pages;
} finally {
$client->quit();
}
}
public function supports(string $sourceType): bool
{
return $sourceType === 'javascript';
}
}