1 Commits

Author SHA1 Message Date
4e30af6a16 Merge pull request 'refactor: supprimer tout le code legacy MVC/Twig/Stimulus' (#34) from refactor/remove-legacy-code into main
All checks were successful
Deploy / deploy (push) Successful in 1m7s
Reviewed-on: #34
2026-03-26 17:01:34 +01:00
377 changed files with 8371 additions and 6181 deletions

View File

@@ -1,7 +1,7 @@
#syntax=docker/dockerfile:1.4 #syntax=docker/dockerfile:1.4
# Versions # Versions
FROM dunglas/frankenphp:1-php8.4 AS frankenphp_upstream FROM dunglas/frankenphp:1-php8.3 AS frankenphp_upstream
# The different stages of this Dockerfile are meant to be built into separate images # The different stages of this Dockerfile are meant to be built into separate images
# https://docs.docker.com/develop/develop-images/multistage-build/#stop-at-a-specific-build-stage # https://docs.docker.com/develop/develop-images/multistage-build/#stop-at-a-specific-build-stage

View File

@@ -242,17 +242,8 @@ watch(() => props.source, (newSource) => {
} }
}, { immediate: true }); }, { immediate: true });
const buildPayload = (formData) => {
const data = { ...formData };
const raw = data.testChapterNumber;
data.testChapterNumber = (raw === '' || raw === null || raw === undefined)
? null
: parseFloat(raw);
return data;
};
const handleSubmit = () => { const handleSubmit = () => {
emit('submit', buildPayload(form.value)); emit('submit', { ...form.value });
}; };
defineExpose({ submitForm: handleSubmit }); defineExpose({ submitForm: handleSubmit });
@@ -261,7 +252,7 @@ const testConfiguration = async () => {
testing.value = true; testing.value = true;
try { try {
await emit('test', { await emit('test', {
configuration: buildPayload(form.value), configuration: { ...form.value },
testData: { testData: {
mangaSlug: form.value.testSlug, mangaSlug: form.value.testSlug,
chapterNumber: form.value.testChapterNumber, chapterNumber: form.value.testChapterNumber,

View File

@@ -3,50 +3,57 @@
"type": "project", "type": "project",
"license": "MIT", "license": "MIT",
"description": "A minimal Symfony project recommended to create bare bones applications", "description": "A minimal Symfony project recommended to create bare bones applications",
"minimum-stability": "dev", "minimum-stability": "stable",
"prefer-stable": true, "prefer-stable": true,
"require": { "require": {
"php": ">=8.4.0", "php": ">=8.3.1",
"ext-ctype": "*", "ext-ctype": "*",
"ext-curl": "*", "ext-curl": "*",
"ext-gd": "*", "ext-gd": "*",
"ext-iconv": "*", "ext-iconv": "*",
"ext-zip": "*", "ext-zip": "*",
"api-platform/core": "^4.0", "api-platform/core": "^3.2",
"doctrine/dbal": "^4", "doctrine/dbal": "^3",
"doctrine/doctrine-bundle": "^3.0", "doctrine/doctrine-bundle": "^2.11",
"doctrine/doctrine-migrations-bundle": "^3.3", "doctrine/doctrine-migrations-bundle": "^3.3",
"doctrine/orm": "^3.0", "doctrine/orm": "^2.17",
"guzzlehttp/guzzle": "^7.8", "guzzlehttp/guzzle": "^7.8",
"intervention/image": "^3.7", "intervention/image": "^3.7",
"nelmio/cors-bundle": "^2.4", "nelmio/cors-bundle": "^2.4",
"phpdocumentor/reflection-docblock": "^5.3", "phpdocumentor/reflection-docblock": "^5.3",
"phpstan/phpdoc-parser": "^1.25", "phpstan/phpdoc-parser": "^1.25",
"ramsey/uuid": "^4.7", "ramsey/uuid": "^4.7",
"symfony/asset": "8.0.*", "runtime/frankenphp-symfony": "^0.2.0",
"symfony/console": "8.0.*", "symfony/asset": "7.0.*",
"symfony/css-selector": "8.0.*", "symfony/console": "7.0.*",
"symfony/doctrine-messenger": "8.0.*", "symfony/css-selector": "7.0.*",
"symfony/dotenv": "8.0.*", "symfony/doctrine-messenger": "7.0.*",
"symfony/expression-language": "8.0.*", "symfony/dotenv": "7.0.*",
"symfony/expression-language": "7.0.*",
"symfony/flex": "^2", "symfony/flex": "^2",
"symfony/framework-bundle": "8.0.*", "symfony/form": "7.0.*",
"symfony/http-client": "8.0.*", "symfony/framework-bundle": "7.0.*",
"symfony/mercure-bundle": "^0.4", "symfony/http-client": "7.0.*",
"symfony/messenger": "8.0.*", "symfony/mercure-bundle": "^0.3.9",
"symfony/mime": "8.0.*", "symfony/messenger": "7.0.*",
"symfony/monolog-bundle": "^4.0", "symfony/mime": "7.0.*",
"symfony/monolog-bundle": "^3.10",
"symfony/panther": "^2.1", "symfony/panther": "^2.1",
"symfony/property-access": "8.0.*", "symfony/property-access": "7.0.*",
"symfony/property-info": "8.0.*", "symfony/property-info": "7.0.*",
"symfony/runtime": "8.0.*", "symfony/runtime": "7.0.*",
"symfony/scheduler": "8.0.*", "symfony/scheduler": "7.0.*",
"symfony/security-bundle": "8.0.*", "symfony/security-bundle": "7.0.*",
"symfony/serializer": "8.0.*", "symfony/serializer": "7.0.*",
"symfony/twig-bundle": "8.0.*", "symfony/stimulus-bundle": "^2.17",
"symfony/validator": "8.0.*", "symfony/twig-bundle": "7.0.*",
"symfony/ux-live-component": "^2.17",
"symfony/ux-react": "^2.23",
"symfony/ux-turbo": "^2.18",
"symfony/validator": "7.0.*",
"symfony/webpack-encore-bundle": "^2.1", "symfony/webpack-encore-bundle": "^2.1",
"symfony/yaml": "8.0.*", "symfony/yaml": "7.0.*",
"twig/extra-bundle": "^2.12|^3.0",
"twig/twig": "^2.12|^3.0", "twig/twig": "^2.12|^3.0",
"vich/uploader-bundle": "^2.7" "vich/uploader-bundle": "^2.7"
}, },
@@ -96,7 +103,7 @@
"extra": { "extra": {
"symfony": { "symfony": {
"allow-contrib": false, "allow-contrib": false,
"require": "8.0.*", "require": "7.0.*",
"docker": true "docker": true
} }
}, },
@@ -104,18 +111,18 @@
"dama/doctrine-test-bundle": "^8.2", "dama/doctrine-test-bundle": "^8.2",
"dbrekelmans/bdi": "^1.3", "dbrekelmans/bdi": "^1.3",
"deployer/deployer": "^7.5", "deployer/deployer": "^7.5",
"doctrine/doctrine-fixtures-bundle": "^4.0", "doctrine/doctrine-fixtures-bundle": "^3.5",
"friendsofphp/php-cs-fixer": "^3.48", "friendsofphp/php-cs-fixer": "^3.48",
"mtdowling/jmespath.php": "^2.7", "mtdowling/jmespath.php": "^2.7",
"phparkitect/phparkitect": "^0.8", "phparkitect/phparkitect": "^0.3.33",
"phpmd/phpmd": "3.x-dev", "phpmd/phpmd": "^2.15",
"phpunit/phpunit": "^10.5", "phpunit/phpunit": "^10.5",
"symfony/browser-kit": "8.0.*", "symfony/browser-kit": "7.0.*",
"symfony/maker-bundle": "^1.52", "symfony/maker-bundle": "^1.52",
"symfony/phpunit-bridge": "^8.0", "symfony/phpunit-bridge": "^7.0",
"symfony/stopwatch": "8.0.*", "symfony/stopwatch": "7.0.*",
"symfony/web-profiler-bundle": "8.0.*", "symfony/web-profiler-bundle": "7.0.*",
"zenstruck/browser": "^1.8", "zenstruck/browser": "^1.8",
"zenstruck/foundry": "^2.0" "zenstruck/foundry": "^1.36"
} }
} }

4335
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -14,7 +14,13 @@ return [
Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true], Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true],
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true], Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true],
Symfony\UX\TwigComponent\TwigComponentBundle::class => ['all' => true],
Symfony\UX\LiveComponent\LiveComponentBundle::class => ['all' => true],
Symfony\UX\StimulusBundle\StimulusBundle::class => ['all' => true],
Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
Symfony\Bundle\MercureBundle\MercureBundle::class => ['all' => true], Symfony\Bundle\MercureBundle\MercureBundle::class => ['all' => true],
Symfony\UX\Turbo\TurboBundle::class => ['all' => true],
DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true], DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true],
Symfony\UX\React\ReactBundle::class => ['all' => true],
Vich\UploaderBundle\VichUploaderBundle::class => ['all' => true], Vich\UploaderBundle\VichUploaderBundle::class => ['all' => true],
]; ];

View File

@@ -23,6 +23,8 @@ api_platform:
extra_properties: extra_properties:
standard_put: true standard_put: true
rfc_7807_compliant_errors: true rfc_7807_compliant_errors: true
event_listeners_backward_compatibility_layer: false
keep_legacy_inflector: false
mapping: mapping:
paths: paths:
- '%kernel.project_dir%/src/Domain/Scraping/Infrastructure/ApiPlatform/Dto' - '%kernel.project_dir%/src/Domain/Scraping/Infrastructure/ApiPlatform/Dto'

View File

@@ -3,6 +3,7 @@ doctrine:
connections: connections:
default: default:
url: '%env(resolve:DATABASE_URL)%' url: '%env(resolve:DATABASE_URL)%'
use_savepoints: true
profiling_collect_backtrace: '%kernel.debug%' profiling_collect_backtrace: '%kernel.debug%'
# IMPORTANT: You MUST configure your server version, # IMPORTANT: You MUST configure your server version,
@@ -10,6 +11,9 @@ doctrine:
#server_version: '16' #server_version: '16'
orm: orm:
auto_generate_proxy_classes: true
enable_lazy_ghost_objects: true
report_fields_where_declared: true
validate_xml_mapping: true validate_xml_mapping: true
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
auto_mapping: true auto_mapping: true
@@ -36,6 +40,7 @@ when@test:
dbal: dbal:
connections: connections:
default: default:
use_savepoints: true
# "TEST_TOKEN" is typically set by ParaTest # "TEST_TOKEN" is typically set by ParaTest
dbname_suffix: '_test%env(default::TEST_TOKEN)%' dbname_suffix: '_test%env(default::TEST_TOKEN)%'

View File

@@ -1,3 +0,0 @@
framework:
property_info:
with_constructor_extractor: true

View File

@@ -0,0 +1,5 @@
twig_component:
anonymous_template_directory: 'components/'
defaults:
# Namespace & directory for components
App\Twig\Components\: 'components/'

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
when@dev: when@dev:
_errors: _errors:
resource: '@FrameworkBundle/Resources/config/routing/errors.php' resource: '@FrameworkBundle/Resources/config/routing/errors.xml'
prefix: /_error prefix: /_error

View File

@@ -0,0 +1,5 @@
live_component:
resource: '@LiveComponentBundle/config/routes.php'
prefix: '/_components'
# adjust prefix to add localization to your components
#prefix: '/{_locale}/_components'

View File

@@ -1,8 +1,8 @@
when@dev: when@dev:
web_profiler_wdt: web_profiler_wdt:
resource: '@WebProfilerBundle/Resources/config/routing/wdt.php' resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml'
prefix: /_wdt prefix: /_wdt
web_profiler_profiler: web_profiler_profiler:
resource: '@WebProfilerBundle/Resources/config/routing/profiler.php' resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml'
prefix: /_profiler prefix: /_profiler

View File

@@ -1,5 +1,4 @@
<?php <?php
namespace Deployer; namespace Deployer;
require 'recipe/symfony.php'; require 'recipe/symfony.php';
@@ -37,13 +36,12 @@ task('deploy:vendors', function () {
$releaseDir = get('release_path'); $releaseDir = get('release_path');
$previousDir = get('previous_release'); $previousDir = get('previous_release');
if (null !== $previousDir) { if ($previousDir !== null) {
$lockUnchanged = test("diff -q $previousDir/composer.lock $releaseDir/composer.lock > /dev/null 2>&1"); $lockUnchanged = test("diff -q $previousDir/composer.lock $releaseDir/composer.lock > /dev/null 2>&1");
$vendorPopulated = test("[ -d $releaseDir/vendor/composer ]"); $vendorPopulated = test("[ -d $releaseDir/vendor/composer ]");
if ($lockUnchanged && $vendorPopulated) { if ($lockUnchanged && $vendorPopulated) {
writeln('<info>deploy:vendors skipped — composer.lock unchanged</info>'); writeln('<info>deploy:vendors skipped — composer.lock unchanged</info>');
return; return;
} }
} }
@@ -69,12 +67,12 @@ task('webpack_encore:build', function () {
$previousDir = get('previous_release'); // null au 1er déploiement $previousDir = get('previous_release'); // null au 1er déploiement
// --- COUCHE 1 : skip total si aucun fichier front-end n'a changé --- // --- COUCHE 1 : skip total si aucun fichier front-end n'a changé ---
if (null !== $previousDir) { if ($previousDir !== null) {
$watchList = ['assets', 'templates', 'package.json', 'package-lock.json', $watchList = ['assets', 'templates', 'package.json', 'package-lock.json',
'webpack.config.js', 'postcss.config.js', 'tailwind.config.js']; 'webpack.config.js', 'postcss.config.js', 'tailwind.config.js'];
$diffChecks = implode(' && ', array_map( $diffChecks = implode(' && ', array_map(
fn ($p) => "diff -rq --no-dereference $previousDir/$p $releaseDir/$p > /dev/null 2>&1", fn($p) => "diff -rq --no-dereference $previousDir/$p $releaseDir/$p > /dev/null 2>&1",
$watchList $watchList
)); ));
@@ -83,14 +81,13 @@ task('webpack_encore:build', function () {
if ($hasPreviousBuild && test("($diffChecks)")) { if ($hasPreviousBuild && test("($diffChecks)")) {
run("cp -al $previousDir/public/build $releaseDir/public/build"); run("cp -al $previousDir/public/build $releaseDir/public/build");
writeln('<info>webpack_encore:build skipped — no front-end files changed</info>'); writeln('<info>webpack_encore:build skipped — no front-end files changed</info>');
return; return;
} }
} }
// --- COUCHE 2 : skip npm install si package-lock.json inchangé --- // --- COUCHE 2 : skip npm install si package-lock.json inchangé ---
$needsNpmInstall = true; $needsNpmInstall = true;
if (null !== $previousDir) { if ($previousDir !== null) {
$lockUnchanged = test("diff -q $previousDir/package-lock.json $releaseDir/package-lock.json > /dev/null 2>&1"); $lockUnchanged = test("diff -q $previousDir/package-lock.json $releaseDir/package-lock.json > /dev/null 2>&1");
$nmPopulated = test("[ -d $sharedNodeModules/.bin ]"); $nmPopulated = test("[ -d $sharedNodeModules/.bin ]");
if ($lockUnchanged && $nmPopulated) { if ($lockUnchanged && $nmPopulated) {

View File

@@ -1,3 +1,4 @@
worker { worker {
file ./public/index.php file ./public/index.php
env APP_RUNTIME Runtime\FrankenPhpSymfony\Runtime
} }

View File

@@ -1,97 +0,0 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260326165659 extends AbstractMigration
{
public function getDescription(): string
{
return 'Migrate manga.genres column from PHP-serialized array to JSON';
}
public function preUp(Schema $schema): void
{
// Convert existing PHP-serialized data to JSON before changing the column type
$rows = $this->connection->fetchAllAssociative('SELECT id, genres FROM manga WHERE genres IS NOT NULL');
foreach ($rows as $row) {
$raw = $row['genres'];
// Skip if already valid JSON
json_decode($raw);
if (json_last_error() === JSON_ERROR_NONE) {
continue;
}
// Unserialize PHP format and re-encode as JSON
$value = @unserialize($raw);
if ($value === false && $raw !== 'b:0;') {
$value = [];
}
$this->connection->executeStatement(
'UPDATE manga SET genres = :json WHERE id = :id',
['json' => json_encode($value), 'id' => $row['id']]
);
}
}
public function up(Schema $schema): void
{
$this->addSql('COMMENT ON COLUMN api_token.expires_at IS \'\'');
$this->addSql('COMMENT ON COLUMN content_source.health_last_tested_at IS \'\'');
$this->addSql('COMMENT ON COLUMN failed_job.failed_at IS \'\'');
$this->addSql('COMMENT ON COLUMN job.created_at IS \'\'');
$this->addSql('COMMENT ON COLUMN job.started_at IS \'\'');
$this->addSql('COMMENT ON COLUMN job.completed_at IS \'\'');
$this->addSql('ALTER TABLE manga ALTER genres TYPE JSON USING genres::json');
$this->addSql('COMMENT ON COLUMN manga.genres IS \'\'');
$this->addSql('COMMENT ON COLUMN manga.created_at IS \'\'');
$this->addSql('COMMENT ON COLUMN manga.last_monitoring_check IS \'\'');
$this->addSql('COMMENT ON COLUMN manga_preferred_sources.created_at IS \'\'');
$this->addSql('COMMENT ON COLUMN manga_preferred_sources.updated_at IS \'\'');
$this->addSql('COMMENT ON COLUMN source.created_at IS \'\'');
$this->addSql('COMMENT ON COLUMN source.updated_at IS \'\'');
$this->addSql('DROP INDEX idx_75ea56e0e3bd61ce');
$this->addSql('DROP INDEX idx_75ea56e0fb7336f0');
$this->addSql('DROP INDEX idx_75ea56e016ba31db');
$this->addSql('ALTER TABLE messenger_messages ALTER id DROP DEFAULT');
$this->addSql('ALTER TABLE messenger_messages ALTER id ADD GENERATED BY DEFAULT AS IDENTITY');
$this->addSql('COMMENT ON COLUMN messenger_messages.created_at IS \'\'');
$this->addSql('COMMENT ON COLUMN messenger_messages.available_at IS \'\'');
$this->addSql('COMMENT ON COLUMN messenger_messages.delivered_at IS \'\'');
$this->addSql('CREATE INDEX IDX_75EA56E0FB7336F0E3BD61CE16BA31DBBF396750 ON messenger_messages (queue_name, available_at, delivered_at, id)');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('COMMENT ON COLUMN api_token.expires_at IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('COMMENT ON COLUMN content_source.health_last_tested_at IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('COMMENT ON COLUMN failed_job.failed_at IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('COMMENT ON COLUMN job.created_at IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('COMMENT ON COLUMN job.started_at IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('COMMENT ON COLUMN job.completed_at IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('ALTER TABLE manga ALTER genres TYPE TEXT');
$this->addSql('COMMENT ON COLUMN manga.genres IS \'(DC2Type:array)\'');
$this->addSql('COMMENT ON COLUMN manga.created_at IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('COMMENT ON COLUMN manga.last_monitoring_check IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('COMMENT ON COLUMN manga_preferred_sources.created_at IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('COMMENT ON COLUMN manga_preferred_sources.updated_at IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('DROP INDEX IDX_75EA56E0FB7336F0E3BD61CE16BA31DBBF396750');
$this->addSql('ALTER TABLE messenger_messages ALTER id SET DEFAULT nextval(\'messenger_messages_id_seq\'::regclass)');
$this->addSql('ALTER TABLE messenger_messages ALTER id DROP IDENTITY');
$this->addSql('COMMENT ON COLUMN messenger_messages.created_at IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('COMMENT ON COLUMN messenger_messages.available_at IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('COMMENT ON COLUMN messenger_messages.delivered_at IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('CREATE INDEX idx_75ea56e0e3bd61ce ON messenger_messages (available_at)');
$this->addSql('CREATE INDEX idx_75ea56e0fb7336f0 ON messenger_messages (queue_name)');
$this->addSql('CREATE INDEX idx_75ea56e016ba31db ON messenger_messages (delivered_at)');
$this->addSql('COMMENT ON COLUMN source.created_at IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('COMMENT ON COLUMN source.updated_at IS \'(DC2Type:datetime_immutable)\'');
}
}

3506
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,15 +2,26 @@
"devDependencies": { "devDependencies": {
"@babel/core": "^7.17.0", "@babel/core": "^7.17.0",
"@babel/preset-env": "^7.16.0", "@babel/preset-env": "^7.16.0",
"@babel/preset-react": "^7.26.3",
"@headlessui/vue": "^1.7.23", "@headlessui/vue": "^1.7.23",
"@heroicons/vue": "^2.2.0", "@heroicons/vue": "^2.2.0",
"@hotwired/stimulus": "^3.0.0",
"@hotwired/turbo": "^7.1.1 || ^8.0",
"@symfony/stimulus-bridge": "^3.2.0",
"@symfony/ux-live-component": "file:vendor/symfony/ux-live-component/assets",
"@symfony/ux-react": "file:vendor/symfony/ux-react/assets",
"@symfony/ux-turbo": "file:vendor/symfony/ux-turbo/assets",
"@symfony/webpack-encore": "^4.0.0", "@symfony/webpack-encore": "^4.0.0",
"@vue/compiler-sfc": "^3.5.13", "@vue/compiler-sfc": "^3.5.13",
"core-js": "^3.23.0", "core-js": "^3.23.0",
"daisyui": "^4.4.2",
"pinia": "^3.0.1", "pinia": "^3.0.1",
"react": "^18.0",
"react-dom": "^18.0",
"regenerator-runtime": "^0.13.9", "regenerator-runtime": "^0.13.9",
"sass": "^1.59.3", "sass": "^1.59.3",
"sass-loader": "^13.2.0", "sass-loader": "^13.2.0",
"stimulus-use": "^0.52.2",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-loader": "^17.4.2", "vue-loader": "^17.4.2",
"vue-router": "^4.5.0", "vue-router": "^4.5.0",
@@ -30,12 +41,18 @@
"@fortawesome/fontawesome-free": "^6.5.2", "@fortawesome/fontawesome-free": "^6.5.2",
"@fortawesome/fontawesome-svg-core": "^6.7.2", "@fortawesome/fontawesome-svg-core": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.7.2", "@fortawesome/free-solid-svg-icons": "^6.7.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"@tanstack/vue-query": "^5.71.0", "@tanstack/vue-query": "^5.71.0",
"alpinejs": "^3.13.3",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
"axios": "^1.7.9", "axios": "^1.7.9",
"bootstrap": "^5.3.3",
"postcss-loader": "^7.1.0", "postcss-loader": "^7.1.0",
"puppeteer": "^22.10.0", "puppeteer": "^22.10.0",
"react-router-dom": "^7.1.5",
"sortablejs": "^1.15.2",
"tailwindcss": "^3.2.7", "tailwindcss": "^3.2.7",
"vue-i18n": "^11.3.0" "vue-i18n": "^11.3.0",
"vuedraggable": "^2.24.3"
} }
} }

View File

@@ -11,7 +11,7 @@ use Arkitect\Expression\ForClasses\ResideInOneOfTheseNamespaces;
use Arkitect\Rules\Rule; use Arkitect\Rules\Rule;
return static function (Config $config): void { return static function (Config $config): void {
$domainClassSet = ClassSet::fromDir(__DIR__.'/src/Domain'); $domainClassSet = ClassSet::fromDir(__DIR__ . '/src/Domain');
$businessDomains = ['Manga', 'Reader', 'Scraping', 'Conversion']; $businessDomains = ['Manga', 'Reader', 'Scraping', 'Conversion'];
// Classes PHP standards et utilitaires // Classes PHP standards et utilitaires
@@ -29,7 +29,7 @@ return static function (Config $config): void {
// Dépendances externes autorisées // Dépendances externes autorisées
$externalDependencies = [ $externalDependencies = [
'Symfony\Component\Messenger', 'Symfony\Component\Messenger',
'Ramsey\Uuid', 'Ramsey\Uuid'
]; ];
// Règle pour le namespace cohérent // Règle pour le namespace cohérent
@@ -72,7 +72,7 @@ return static function (Config $config): void {
// Interdiction explicite pour l'Application d'accéder à l'Infrastructure // Interdiction explicite pour l'Application d'accéder à l'Infrastructure
$rules[] = Rule::allClasses() $rules[] = Rule::allClasses()
->that(new ResideInOneOfTheseNamespaces("App\Domain\\$domain\Application")) ->that(new ResideInOneOfTheseNamespaces("App\Domain\\$domain\Application"))
->should(new NotDependsOnTheseNamespaces(["App\Domain\\$domain\Infrastructure"])) ->should(new NotDependsOnTheseNamespaces("App\Domain\\$domain\Infrastructure"))
->because("la couche Application de $domain ne doit jamais dépendre de l'Infrastructure, même au sein de son propre domaine"); ->because("la couche Application de $domain ne doit jamais dépendre de l'Infrastructure, même au sein de son propre domaine");
} }

View File

@@ -18,7 +18,7 @@ use Symfony\Component\Console\Output\OutputInterface;
class SendTestNotificationCommand extends Command class SendTestNotificationCommand extends Command
{ {
public function __construct( public function __construct(
private readonly NotificationInterface $notification, private readonly NotificationInterface $notification
) { ) {
parent::__construct(); parent::__construct();
} }
@@ -38,7 +38,6 @@ class SendTestNotificationCommand extends Command
$allowed = ['info', 'success', 'error', 'warning']; $allowed = ['info', 'success', 'error', 'warning'];
if (!in_array($type, $allowed, true)) { if (!in_array($type, $allowed, true)) {
$output->writeln(sprintf('<error>Type invalide "%s". Valeurs acceptées : %s</error>', $type, implode(', ', $allowed))); $output->writeln(sprintf('<error>Type invalide "%s". Valeurs acceptées : %s</error>', $type, implode(', ', $allowed)));
return Command::FAILURE; return Command::FAILURE;
} }

View File

@@ -4,6 +4,7 @@ namespace App\Controller;
use ApiPlatform\Api\IriConverterInterface; use ApiPlatform\Api\IriConverterInterface;
use App\Entity\User; use App\Entity\User;
use Exception;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
@@ -12,11 +13,11 @@ use Symfony\Component\Security\Http\Attribute\CurrentUser;
class SecurityController extends AbstractController class SecurityController extends AbstractController
{ {
#[Route('/login', name: 'app_login', methods: ['GET', 'POST'])] #[Route('/login', name: 'app_login', methods: ['GET', 'POST'])]
public function login(IriConverterInterface $iriConverter, #[CurrentUser] ?User $user = null): Response public function login(IriConverterInterface $iriConverter, #[CurrentUser] User $user = null): Response
{ {
if (!$user) { if (!$user) {
return $this->json([ return $this->json([
'error' => 'Invalid credentials', 'error' => 'Invalid credentials'
], 401); ], 401);
} }
@@ -26,11 +27,11 @@ class SecurityController extends AbstractController
} }
/** /**
* @throws \Exception * @throws Exception
*/ */
#[Route('/logout', name: 'app_logout', methods: ['GET'])] #[Route('/logout', name: 'app_logout', methods: ['GET'])]
public function logout(): void public function logout(): void
{ {
throw new \Exception('This method can be blank.'); throw new Exception('This method can be blank.');
} }
} }

View File

@@ -7,7 +7,7 @@ final readonly class ConvertFileCommand
public function __construct( public function __construct(
public string $filePath, public string $filePath,
public string $originalFilename, public string $originalFilename,
public int $fileSize, public int $fileSize
) { ) {
} }
} }

View File

@@ -13,7 +13,7 @@ final readonly class ConvertFileCommandHandler
private const MAX_FILE_SIZE = 150 * 1024 * 1024; // 150MB private const MAX_FILE_SIZE = 150 * 1024 * 1024; // 150MB
public function __construct( public function __construct(
private ConversionServiceInterface $conversionService, private ConversionServiceInterface $conversionService
) { ) {
} }

View File

@@ -10,7 +10,7 @@ final readonly class ConversionResponse
public string $convertedFilePath, public string $convertedFilePath,
public string $outputFilename, public string $outputFilename,
public int $originalFileSize, public int $originalFileSize,
public int $convertedFileSize, public int $convertedFileSize
) { ) {
} }

View File

@@ -2,7 +2,9 @@
namespace App\Domain\Conversion\Domain\Exception; namespace App\Domain\Conversion\Domain\Exception;
class ConversionException extends \RuntimeException use RuntimeException;
class ConversionException extends RuntimeException
{ {
public static function fileNotFound(string $filePath): self public static function fileNotFound(string $filePath): self
{ {

View File

@@ -7,7 +7,7 @@ final readonly class ConversionRequest
public function __construct( public function __construct(
private string $filePath, private string $filePath,
private string $originalFilename, private string $originalFilename,
private int $fileSize, private int $fileSize
) { ) {
} }
@@ -29,7 +29,6 @@ final readonly class ConversionRequest
public function getOutputFilename(): string public function getOutputFilename(): string
{ {
$pathInfo = pathinfo($this->originalFilename, PATHINFO_FILENAME); $pathInfo = pathinfo($this->originalFilename, PATHINFO_FILENAME);
return $pathInfo . '.cbz';
return $pathInfo.'.cbz';
} }
} }

View File

@@ -8,7 +8,7 @@ final readonly class ConversionResult
private string $convertedFilePath, private string $convertedFilePath,
private string $outputFilename, private string $outputFilename,
private int $originalFileSize, private int $originalFileSize,
private int $convertedFileSize, private int $convertedFileSize
) { ) {
} }

View File

@@ -5,16 +5,18 @@ namespace App\Domain\Conversion\Infrastructure\ApiPlatform\Controller;
use App\Domain\Conversion\Application\Command\ConvertFileCommand; use App\Domain\Conversion\Application\Command\ConvertFileCommand;
use App\Domain\Conversion\Application\CommandHandler\ConvertFileCommandHandler; use App\Domain\Conversion\Application\CommandHandler\ConvertFileCommandHandler;
use App\Domain\Conversion\Domain\Exception\ConversionException; use App\Domain\Conversion\Domain\Exception\ConversionException;
use App\Domain\Conversion\Infrastructure\ApiPlatform\Resource\ConvertFileResource;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\AsController; use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
#[AsController] #[AsController]
final class ConvertFileController extends AbstractController final class ConvertFileController extends AbstractController
{ {
public function __construct( public function __construct(
private readonly ConvertFileCommandHandler $commandHandler, private readonly ConvertFileCommandHandler $commandHandler
) { ) {
} }
@@ -23,7 +25,7 @@ final class ConvertFileController extends AbstractController
$uploadedFile = $request->files->get('file'); $uploadedFile = $request->files->get('file');
if (!$uploadedFile) { if (!$uploadedFile) {
return $this->json([ return $this->json([
['propertyPath' => 'file', 'message' => 'Please upload a file'], ['propertyPath' => 'file', 'message' => 'Please upload a file']
], 422); ], 422);
} }
@@ -56,6 +58,7 @@ final class ConvertFileController extends AbstractController
'Content-Disposition' => sprintf('attachment; filename=%s', $response->outputFilename), 'Content-Disposition' => sprintf('attachment; filename=%s', $response->outputFilename),
] ]
); );
} catch (ConversionException $e) { } catch (ConversionException $e) {
return $this->json(['error' => $e->getMessage()], 400); return $this->json(['error' => $e->getMessage()], 400);
} }
@@ -69,9 +72,8 @@ final class ConvertFileController extends AbstractController
if (!$uploadedFile->isValid()) { if (!$uploadedFile->isValid()) {
$errors[] = [ $errors[] = [
'propertyPath' => 'file', 'propertyPath' => 'file',
'message' => 'The uploaded file is not valid: '.$uploadedFile->getErrorMessage(), 'message' => 'The uploaded file is not valid: ' . $uploadedFile->getErrorMessage()
]; ];
return $errors; return $errors;
} }
@@ -80,7 +82,7 @@ final class ConvertFileController extends AbstractController
if ($uploadedFile->getSize() > $maxSize) { if ($uploadedFile->getSize() > $maxSize) {
$errors[] = [ $errors[] = [
'propertyPath' => 'file', 'propertyPath' => 'file',
'message' => 'The uploaded file is too large. Allowed size is 150MB.', 'message' => 'The uploaded file is too large. Allowed size is 150MB.'
]; ];
} }
@@ -91,7 +93,7 @@ final class ConvertFileController extends AbstractController
if (!in_array($extension, $allowedExtensions)) { if (!in_array($extension, $allowedExtensions)) {
$errors[] = [ $errors[] = [
'propertyPath' => 'file', 'propertyPath' => 'file',
'message' => 'Please upload a valid CBR or CBZ file', 'message' => 'Please upload a valid CBR or CBZ file'
]; ];
} }

View File

@@ -4,10 +4,10 @@ namespace App\Domain\Conversion\Infrastructure\ApiPlatform\Resource;
use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Post; use ApiPlatform\Metadata\Post;
use ApiPlatform\OpenApi\Model\Operation; use ApiPlatform\OpenApi\Model;
use ApiPlatform\OpenApi\Model\RequestBody;
use App\Domain\Conversion\Infrastructure\ApiPlatform\Controller\ConvertFileController; use App\Domain\Conversion\Infrastructure\ApiPlatform\Controller\ConvertFileController;
use Symfony\Component\HttpFoundation\File\File; use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\Validator\Constraints as Assert;
#[ApiResource( #[ApiResource(
shortName: 'Conversion', shortName: 'Conversion',
@@ -16,11 +16,11 @@ use Symfony\Component\HttpFoundation\File\File;
uriTemplate: '/conversions/convert', uriTemplate: '/conversions/convert',
controller: ConvertFileController::class, controller: ConvertFileController::class,
deserialize: false, deserialize: false,
openapi: new Operation( openapiContext: [
summary: 'Convert comic book file to CBZ', 'summary' => 'Convert comic book file to CBZ',
description: 'Converts a CBR or CBZ file to CBZ format and returns the converted file for download', 'description' => 'Converts a CBR or CBZ file to CBZ format and returns the converted file for download',
requestBody: new RequestBody( 'requestBody' => [
content: new \ArrayObject([ 'content' => [
'multipart/form-data' => [ 'multipart/form-data' => [
'schema' => [ 'schema' => [
'type' => 'object', 'type' => 'object',
@@ -29,28 +29,28 @@ use Symfony\Component\HttpFoundation\File\File;
'file' => [ 'file' => [
'type' => 'string', 'type' => 'string',
'format' => 'binary', 'format' => 'binary',
'description' => 'Comic book file to convert (CBR, CBZ, max 150MB)', 'description' => 'Comic book file to convert (CBR, CBZ, max 150MB)'
]
]
]
]
]
], ],
], 'responses' => [
],
],
])
),
responses: [
'200' => [ '200' => [
'description' => 'File converted successfully', 'description' => 'File converted successfully',
'content' => [ 'content' => [
'application/x-cbz' => [ 'application/x-cbz' => [
'schema' => [ 'schema' => [
'type' => 'string', 'type' => 'string',
'format' => 'binary', 'format' => 'binary'
], ]
], ]
], ]
], ]
]
] ]
) )
),
] ]
)] )]
class ConvertFileResource class ConvertFileResource

View File

@@ -16,7 +16,7 @@ final class ConversionService implements ConversionServiceInterface
public function __construct(string $projectDir) public function __construct(string $projectDir)
{ {
$this->tempDir = $projectDir.'/public/tmp'; $this->tempDir = $projectDir . '/public/tmp';
$this->filesystem = new Filesystem(); $this->filesystem = new Filesystem();
} }
@@ -40,10 +40,10 @@ final class ConversionService implements ConversionServiceInterface
private function convertCbrToCbz(string $cbrPath): string private function convertCbrToCbz(string $cbrPath): string
{ {
$tempDir = $this->tempDir.'/'.uniqid('cbr_conversion_'); $tempDir = $this->tempDir . '/' . uniqid('cbr_conversion_');
$this->filesystem->mkdir($tempDir); $this->filesystem->mkdir($tempDir);
$extractDir = $tempDir.'/extract'; $extractDir = $tempDir . '/extract';
$this->filesystem->mkdir($extractDir); $this->filesystem->mkdir($extractDir);
// Essayer d'extraire avec unrar-free // Essayer d'extraire avec unrar-free
@@ -56,16 +56,16 @@ final class ConversionService implements ConversionServiceInterface
$process->run(); $process->run();
if (!$process->isSuccessful()) { if (!$process->isSuccessful()) {
throw new \RuntimeException('Extraction failed: '.$process->getErrorOutput()); throw new \RuntimeException("Extraction failed: " . $process->getErrorOutput());
} }
} }
// Créer le CBZ // Créer le CBZ
$cbzFileName = pathinfo($cbrPath, PATHINFO_FILENAME).'.cbz'; $cbzFileName = pathinfo($cbrPath, PATHINFO_FILENAME) . '.cbz';
$cbzPath = $this->tempDir.'/'.$cbzFileName; $cbzPath = $this->tempDir . '/' . $cbzFileName;
$zip = new \ZipArchive(); $zip = new \ZipArchive();
if (true !== $zip->open($cbzPath, \ZipArchive::CREATE)) { if ($zip->open($cbzPath, \ZipArchive::CREATE) !== true) {
throw new \RuntimeException('Cannot create ZIP file'); throw new \RuntimeException("Cannot create ZIP file");
} }
$files = new \RecursiveIteratorIterator( $files = new \RecursiveIteratorIterator(

View File

@@ -7,7 +7,7 @@ readonly class ChapterEditData
public function __construct( public function __construct(
public string $id, public string $id,
public ?string $title = null, public ?string $title = null,
public ?int $volume = null, public ?int $volume = null
) { ) {
} }
} }

View File

@@ -2,10 +2,12 @@
namespace App\Domain\Manga\Application\Command; namespace App\Domain\Manga\Application\Command;
use DateTimeImmutable;
readonly class CheckMonitoredMangas readonly class CheckMonitoredMangas
{ {
public function __construct( public function __construct(
public ?\DateTimeImmutable $since = null, public ?DateTimeImmutable $since = null
) { ) {
} }
} }

View File

@@ -14,7 +14,7 @@ readonly class CreateManga
public string $status, public string $status,
public ?string $externalId, public ?string $externalId,
public ?string $imageUrl, public ?string $imageUrl,
public ?float $rating, public ?float $rating
) { ) {
} }
} }

View File

@@ -5,7 +5,7 @@ namespace App\Domain\Manga\Application\Command;
readonly class CreateMangaFromMangadex readonly class CreateMangaFromMangadex
{ {
public function __construct( public function __construct(
public string $externalId, public string $externalId
) { ) {
} }
} }

View File

@@ -7,7 +7,7 @@ use App\Domain\Shared\Domain\Contract\CommandInterface;
readonly class DeleteCbz implements CommandInterface readonly class DeleteCbz implements CommandInterface
{ {
public function __construct( public function __construct(
public string $chapterId, public string $chapterId
) { ) {
} }
} }

View File

@@ -7,7 +7,7 @@ use App\Domain\Shared\Domain\Contract\CommandInterface;
readonly class DeleteChapter implements CommandInterface readonly class DeleteChapter implements CommandInterface
{ {
public function __construct( public function __construct(
public string $chapterId, public string $chapterId
) { ) {
} }
} }

View File

@@ -7,7 +7,7 @@ use App\Domain\Shared\Domain\Contract\CommandInterface;
readonly class DeleteManga implements CommandInterface readonly class DeleteManga implements CommandInterface
{ {
public function __construct( public function __construct(
public string $mangaId, public string $mangaId
) { ) {
} }
} }

View File

@@ -13,7 +13,7 @@ readonly class EditManga
public ?array $genres = null, public ?array $genres = null,
public ?string $status = null, public ?string $status = null,
public ?float $rating = null, public ?float $rating = null,
public ?array $alternativeSlugs = null, public ?array $alternativeSlugs = null
) { ) {
} }
} }

View File

@@ -8,7 +8,7 @@ readonly class EditMultipleChapters
* @param array<ChapterEditData> $chapters * @param array<ChapterEditData> $chapters
*/ */
public function __construct( public function __construct(
public array $chapters, public array $chapters
) { ) {
} }
} }

View File

@@ -7,7 +7,7 @@ use App\Domain\Manga\Domain\Model\ValueObject\MangaId;
readonly class FetchMangaChapters readonly class FetchMangaChapters
{ {
public function __construct( public function __construct(
public MangaId $mangaId, public MangaId $mangaId
) { ) {
} }
} }

View File

@@ -7,7 +7,7 @@ readonly class ImportChapter
public function __construct( public function __construct(
public string $mangaId, public string $mangaId,
public float $chapterNumber, public float $chapterNumber,
public string $fileBinary, public string $fileBinary
) { ) {
} }
} }

View File

@@ -7,7 +7,7 @@ readonly class ImportVolume
public function __construct( public function __construct(
public string $mangaId, public string $mangaId,
public int $volumeNumber, public int $volumeNumber,
public string $fileBinary, public string $fileBinary
) { ) {
} }
} }

View File

@@ -7,7 +7,7 @@ use App\Domain\Manga\Domain\Model\ValueObject\MangaId;
readonly class RefreshMangaChapters readonly class RefreshMangaChapters
{ {
public function __construct( public function __construct(
public MangaId $mangaId, public MangaId $mangaId
) { ) {
} }
} }

View File

@@ -8,7 +8,7 @@ readonly class ToggleMangaMonitoring
{ {
public function __construct( public function __construct(
public MangaId $mangaId, public MangaId $mangaId,
public bool $enabled, public bool $enabled
) { ) {
} }
} }

View File

@@ -6,13 +6,14 @@ use App\Domain\Manga\Application\Command\CheckMonitoredMangas;
use App\Domain\Manga\Application\Command\RefreshMangaChapters; use App\Domain\Manga\Application\Command\RefreshMangaChapters;
use App\Domain\Manga\Application\Query\MonitoringCriteria; use App\Domain\Manga\Application\Query\MonitoringCriteria;
use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface; use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface;
use DateTimeImmutable;
use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Messenger\MessageBusInterface;
readonly class CheckMonitoredMangasHandler readonly class CheckMonitoredMangasHandler
{ {
public function __construct( public function __construct(
private MangaRepositoryInterface $mangaRepository, private MangaRepositoryInterface $mangaRepository,
private MessageBusInterface $commandBus, private MessageBusInterface $commandBus
) { ) {
} }
@@ -20,7 +21,7 @@ readonly class CheckMonitoredMangasHandler
{ {
$criteria = new MonitoringCriteria( $criteria = new MonitoringCriteria(
enabled: true, enabled: true,
lastCheckBefore: $command->since ?? new \DateTimeImmutable('-1 hour') lastCheckBefore: $command->since ?? new DateTimeImmutable('-1 hour')
); );
$monitoredMangas = $this->mangaRepository->findByMonitoringCriteria($criteria); $monitoredMangas = $this->mangaRepository->findByMonitoringCriteria($criteria);

View File

@@ -3,6 +3,7 @@
namespace App\Domain\Manga\Application\CommandHandler; namespace App\Domain\Manga\Application\CommandHandler;
use App\Domain\Manga\Application\Command\CreateMangaFromMangadex; use App\Domain\Manga\Application\Command\CreateMangaFromMangadex;
use App\Domain\Manga\Application\Response\CreateMangaResponse;
use App\Domain\Manga\Domain\Contract\Provider\MangaProviderInterface; use App\Domain\Manga\Domain\Contract\Provider\MangaProviderInterface;
use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface; use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface;
use App\Domain\Manga\Domain\Contract\Service\ImageProcessorInterface; use App\Domain\Manga\Domain\Contract\Service\ImageProcessorInterface;
@@ -18,7 +19,7 @@ readonly class CreateMangaFromMangadexHandler
private MangaProviderInterface $mangaProvider, private MangaProviderInterface $mangaProvider,
private MangaRepositoryInterface $mangaRepository, private MangaRepositoryInterface $mangaRepository,
private ImageProcessorInterface $imageProcessor, private ImageProcessorInterface $imageProcessor,
private EventDispatcherInterface $eventDispatcher, private EventDispatcherInterface $eventDispatcher
) { ) {
} }
@@ -26,7 +27,7 @@ readonly class CreateMangaFromMangadexHandler
{ {
$manga = $this->mangaProvider->findByExternalId(new ExternalId($command->externalId)); $manga = $this->mangaProvider->findByExternalId(new ExternalId($command->externalId));
if (null === $manga) { if ($manga === null) {
throw new MangaNotFoundException('Manga not found on Mangadex'); throw new MangaNotFoundException('Manga not found on Mangadex');
} }
@@ -40,7 +41,7 @@ readonly class CreateMangaFromMangadexHandler
// Met à jour le manga avec les nouveaux chemins d'images // Met à jour le manga avec les nouveaux chemins d'images
$manga->updateImageUrls(new ImageUrls($fullImagePath, $thumbnailPath)); $manga->updateImageUrls(new ImageUrls($fullImagePath, $thumbnailPath));
} catch (\Exception $e) { } catch (\Exception $e) {
throw new \RuntimeException('Erreur lors du traitement de l\'image : '.$e->getMessage()); throw new \RuntimeException('Erreur lors du traitement de l\'image : ' . $e->getMessage());
} }
$this->mangaRepository->save($manga); $this->mangaRepository->save($manga);

View File

@@ -20,7 +20,7 @@ readonly class CreateMangaHandler
public function __construct( public function __construct(
private MangaRepositoryInterface $mangaRepository, private MangaRepositoryInterface $mangaRepository,
private ImageProcessorInterface $imageProcessor, private ImageProcessorInterface $imageProcessor,
private MessageBusInterface $messageBus, private MessageBusInterface $messageBus
) { ) {
} }
@@ -48,7 +48,7 @@ readonly class CreateMangaHandler
$thumbnailPath = $this->imageProcessor->createThumbnail($fullImagePath); $thumbnailPath = $this->imageProcessor->createThumbnail($fullImagePath);
$manga->updateImageUrls(new ImageUrls($fullImagePath, $thumbnailPath)); $manga->updateImageUrls(new ImageUrls($fullImagePath, $thumbnailPath));
} catch (\Exception $e) { } catch (\Exception $e) {
throw new \RuntimeException('Erreur lors du traitement de l\'image : '.$e->getMessage()); throw new \RuntimeException('Erreur lors du traitement de l\'image : ' . $e->getMessage());
} }
} }

View File

@@ -5,8 +5,8 @@ namespace App\Domain\Manga\Application\CommandHandler;
use App\Domain\Manga\Application\Command\DeleteCbz; use App\Domain\Manga\Application\Command\DeleteCbz;
use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface; use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface;
use App\Domain\Manga\Domain\Contract\Service\FileServiceInterface; use App\Domain\Manga\Domain\Contract\Service\FileServiceInterface;
use App\Domain\Manga\Domain\Exception\CbzFileNotFoundException;
use App\Domain\Manga\Domain\Exception\ChapterNotFoundException; use App\Domain\Manga\Domain\Exception\ChapterNotFoundException;
use App\Domain\Manga\Domain\Exception\CbzFileNotFoundException;
use App\Domain\Shared\Domain\Contract\CommandHandlerInterface; use App\Domain\Shared\Domain\Contract\CommandHandlerInterface;
use App\Domain\Shared\Domain\Contract\CommandInterface; use App\Domain\Shared\Domain\Contract\CommandInterface;
@@ -14,7 +14,7 @@ readonly class DeleteCbzHandler implements CommandHandlerInterface
{ {
public function __construct( public function __construct(
private MangaRepositoryInterface $mangaRepository, private MangaRepositoryInterface $mangaRepository,
private FileServiceInterface $fileService, private FileServiceInterface $fileService
) { ) {
} }

View File

@@ -11,7 +11,7 @@ use App\Domain\Shared\Domain\Contract\CommandInterface;
readonly class DeleteChapterHandler implements CommandHandlerInterface readonly class DeleteChapterHandler implements CommandHandlerInterface
{ {
public function __construct( public function __construct(
private MangaRepositoryInterface $mangaRepository, private MangaRepositoryInterface $mangaRepository
) { ) {
} }

View File

@@ -11,7 +11,7 @@ use App\Domain\Shared\Domain\Contract\CommandInterface;
readonly class DeleteMangaHandler implements CommandHandlerInterface readonly class DeleteMangaHandler implements CommandHandlerInterface
{ {
public function __construct( public function __construct(
private MangaRepositoryInterface $mangaRepository, private MangaRepositoryInterface $mangaRepository
) { ) {
} }

View File

@@ -10,7 +10,7 @@ use App\Domain\Manga\Domain\Model\ValueObject\MangaTitle;
readonly class EditMangaHandler readonly class EditMangaHandler
{ {
public function __construct( public function __construct(
private MangaRepositoryInterface $mangaRepository, private MangaRepositoryInterface $mangaRepository
) { ) {
} }
@@ -23,35 +23,35 @@ readonly class EditMangaHandler
} }
// Update only provided fields (partial update) // Update only provided fields (partial update)
if (null !== $command->title) { if ($command->title !== null) {
$manga->updateTitle(new MangaTitle($command->title)); $manga->updateTitle(new MangaTitle($command->title));
} }
if (null !== $command->description) { if ($command->description !== null) {
$manga->updateDescription($command->description); $manga->updateDescription($command->description);
} }
if (null !== $command->author) { if ($command->author !== null) {
$manga->updateAuthor($command->author); $manga->updateAuthor($command->author);
} }
if (null !== $command->publicationYear) { if ($command->publicationYear !== null) {
$manga->updatePublicationYear($command->publicationYear); $manga->updatePublicationYear($command->publicationYear);
} }
if (null !== $command->genres) { if ($command->genres !== null) {
$manga->updateGenres($command->genres); $manga->updateGenres($command->genres);
} }
if (null !== $command->status) { if ($command->status !== null) {
$manga->updateStatus($command->status); $manga->updateStatus($command->status);
} }
if (null !== $command->rating) { if ($command->rating !== null) {
$manga->setRating($command->rating); $manga->setRating($command->rating);
} }
if (null !== $command->alternativeSlugs) { if ($command->alternativeSlugs !== null) {
$manga->updateAlternativeSlugs($command->alternativeSlugs); $manga->updateAlternativeSlugs($command->alternativeSlugs);
} }

View File

@@ -9,7 +9,7 @@ use App\Domain\Manga\Domain\Exception\ChapterNotFoundException;
readonly class EditMultipleChaptersHandler readonly class EditMultipleChaptersHandler
{ {
public function __construct( public function __construct(
private MangaRepositoryInterface $mangaRepository, private MangaRepositoryInterface $mangaRepository
) { ) {
} }
@@ -24,11 +24,11 @@ readonly class EditMultipleChaptersHandler
$manga = $this->mangaRepository->findById($chapter->getMangaId()->getValue()); $manga = $this->mangaRepository->findById($chapter->getMangaId()->getValue());
if (null !== $chapterData->title) { if ($chapterData->title !== null) {
$manga->updateChapterTitle($chapter, $chapterData->title); $manga->updateChapterTitle($chapter, $chapterData->title);
} }
if (null !== $chapterData->volume) { if ($chapterData->volume !== null) {
$manga->updateChapterVolume($chapter, $chapterData->volume); $manga->updateChapterVolume($chapter, $chapterData->volume);
} }

View File

@@ -12,7 +12,7 @@ readonly class FetchMangaChaptersHandler
{ {
public function __construct( public function __construct(
private MangaRepositoryInterface $mangaRepository, private MangaRepositoryInterface $mangaRepository,
private ChapterSynchronizationServiceInterface $chapterSynchronizationService, private ChapterSynchronizationServiceInterface $chapterSynchronizationService
) { ) {
} }
@@ -20,12 +20,12 @@ readonly class FetchMangaChaptersHandler
{ {
$manga = $this->mangaRepository->findById($command->mangaId->getValue()); $manga = $this->mangaRepository->findById($command->mangaId->getValue());
if (null === $manga) { if ($manga === null) {
throw new MangaNotFoundException(); throw new MangaNotFoundException();
} }
if (null === $manga->getExternalId()) { if ($manga->getExternalId() === null) {
throw new MangadexApiException('Manga has no external_id'); throw new MangadexApiException("Manga has no external_id");
} }
// Synchronisation initiale (pas d'événements) // Synchronisation initiale (pas d'événements)

View File

@@ -4,15 +4,15 @@ namespace App\Domain\Manga\Application\CommandHandler;
use App\Domain\Manga\Application\Command\ImportChapter; use App\Domain\Manga\Application\Command\ImportChapter;
use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface; use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface;
use App\Domain\Manga\Domain\Exception\ChapterNotFoundException;
use App\Domain\Manga\Domain\Exception\MangaNotFoundException; use App\Domain\Manga\Domain\Exception\MangaNotFoundException;
use App\Domain\Manga\Domain\Exception\ChapterNotFoundException;
use App\Domain\Shared\Domain\Contract\ImageStorageInterface; use App\Domain\Shared\Domain\Contract\ImageStorageInterface;
readonly class ImportChapterHandler readonly class ImportChapterHandler
{ {
public function __construct( public function __construct(
private MangaRepositoryInterface $mangaRepository, private MangaRepositoryInterface $mangaRepository,
private ImageStorageInterface $imageStorage, private ImageStorageInterface $imageStorage
) { ) {
} }
@@ -55,6 +55,6 @@ readonly class ImportChapterHandler
{ {
$zipMagicNumber = "\x50\x4b\x03\x04"; // PK\x03\x04 $zipMagicNumber = "\x50\x4b\x03\x04"; // PK\x03\x04
return 0 === strpos($fileBinary, $zipMagicNumber); return strpos($fileBinary, $zipMagicNumber) === 0;
} }
} }

View File

@@ -11,7 +11,7 @@ readonly class ImportVolumeHandler
{ {
public function __construct( public function __construct(
private MangaRepositoryInterface $mangaRepository, private MangaRepositoryInterface $mangaRepository,
private ImageStorageInterface $imageStorage, private ImageStorageInterface $imageStorage
) { ) {
} }
@@ -35,7 +35,9 @@ readonly class ImportVolumeHandler
); );
if (empty($chapters)) { if (empty($chapters)) {
throw new \InvalidArgumentException("No chapters found for manga {$command->mangaId} in volume {$command->volumeNumber}"); throw new \InvalidArgumentException(
"No chapters found for manga {$command->mangaId} in volume {$command->volumeNumber}"
);
} }
// 4. Extract CBZ into individual images storage (shared directory for all volume chapters) // 4. Extract CBZ into individual images storage (shared directory for all volume chapters)
@@ -54,6 +56,6 @@ readonly class ImportVolumeHandler
{ {
$zipMagicNumber = "\x50\x4b\x03\x04"; // PK\x03\x04 $zipMagicNumber = "\x50\x4b\x03\x04"; // PK\x03\x04
return 0 === strpos($fileBinary, $zipMagicNumber); return strpos($fileBinary, $zipMagicNumber) === 0;
} }
} }

View File

@@ -7,6 +7,7 @@ use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface;
use App\Domain\Manga\Domain\Contract\Service\ChapterSynchronizationServiceInterface; use App\Domain\Manga\Domain\Contract\Service\ChapterSynchronizationServiceInterface;
use App\Domain\Manga\Domain\Event\ChapterReadyForScraping; use App\Domain\Manga\Domain\Event\ChapterReadyForScraping;
use App\Domain\Manga\Domain\Model\ValueObject\ChapterId; use App\Domain\Manga\Domain\Model\ValueObject\ChapterId;
use DateTimeImmutable;
use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Messenger\MessageBusInterface;
readonly class RefreshMangaChaptersHandler readonly class RefreshMangaChaptersHandler
@@ -14,7 +15,7 @@ readonly class RefreshMangaChaptersHandler
public function __construct( public function __construct(
private MangaRepositoryInterface $mangaRepository, private MangaRepositoryInterface $mangaRepository,
private ChapterSynchronizationServiceInterface $chapterSynchronizationService, private ChapterSynchronizationServiceInterface $chapterSynchronizationService,
private MessageBusInterface $eventBus, private MessageBusInterface $eventBus
) { ) {
} }
@@ -22,7 +23,7 @@ readonly class RefreshMangaChaptersHandler
{ {
$manga = $this->mangaRepository->findById($command->mangaId->getValue()); $manga = $this->mangaRepository->findById($command->mangaId->getValue());
if (null === $manga) { if ($manga === null) {
throw new \RuntimeException('Manga not found'); throw new \RuntimeException('Manga not found');
} }
@@ -30,7 +31,7 @@ readonly class RefreshMangaChaptersHandler
$newChapterIds = $this->chapterSynchronizationService->synchronizeChapters($manga); $newChapterIds = $this->chapterSynchronizationService->synchronizeChapters($manga);
// Mise à jour de la date de monitoring // Mise à jour de la date de monitoring
$manga->updateLastMonitoringCheck(new \DateTimeImmutable()); $manga->updateLastMonitoringCheck(new DateTimeImmutable());
$this->mangaRepository->save($manga); $this->mangaRepository->save($manga);
// Événement de scraping pour chaque nouveau chapitre // Événement de scraping pour chaque nouveau chapitre

View File

@@ -9,7 +9,7 @@ use App\Domain\Manga\Domain\Exception\MangaNotFoundException;
readonly class ToggleMangaMonitoringHandler readonly class ToggleMangaMonitoringHandler
{ {
public function __construct( public function __construct(
private MangaRepositoryInterface $mangaRepository, private MangaRepositoryInterface $mangaRepository
) { ) {
} }

View File

@@ -10,7 +10,7 @@ use App\Domain\Manga\Domain\Model\ValueObject\MangaId;
readonly class MangaCreatedEventListener readonly class MangaCreatedEventListener
{ {
public function __construct( public function __construct(
private FetchMangaChaptersHandler $fetchMangaChaptersHandler, private FetchMangaChaptersHandler $fetchMangaChaptersHandler
) { ) {
} }

View File

@@ -23,7 +23,7 @@ readonly class VolumeImportedEventListener
} }
$chapters = $this->mangaRepository->findChaptersByMangaIdAndVolume($manga->getId()->getValue(), (int) $event->volume); $chapters = $this->mangaRepository->findChaptersByMangaIdAndVolume($manga->getId()->getValue(), (int) $event->volume);
if ([] === $chapters) { if ($chapters === []) {
return; return;
} }

View File

@@ -7,7 +7,7 @@ use App\Domain\Shared\Domain\Contract\QueryInterface;
readonly class DownloadCbz implements QueryInterface readonly class DownloadCbz implements QueryInterface
{ {
public function __construct( public function __construct(
public string $chapterId, public string $chapterId
) { ) {
} }
} }

View File

@@ -8,7 +8,7 @@ readonly class DownloadVolume implements QueryInterface
{ {
public function __construct( public function __construct(
public string $mangaId, public string $mangaId,
public int $volume, public int $volume
) { ) {
} }
} }

View File

@@ -7,7 +7,7 @@ namespace App\Domain\Manga\Application\Query;
readonly class FindMangaMatchByFilename readonly class FindMangaMatchByFilename
{ {
public function __construct( public function __construct(
public string $filename, public string $filename
) { ) {
} }
} }

View File

@@ -5,7 +5,7 @@ namespace App\Domain\Manga\Application\Query;
readonly class GetMangaById readonly class GetMangaById
{ {
public function __construct( public function __construct(
public string $id, public string $id
) { ) {
} }
} }

View File

@@ -5,7 +5,7 @@ namespace App\Domain\Manga\Application\Query;
readonly class GetMangaBySlug readonly class GetMangaBySlug
{ {
public function __construct( public function __construct(
public string $slug, public string $slug
) { ) {
} }
} }

View File

@@ -8,7 +8,7 @@ readonly class GetMangaChapters
public string $mangaId, public string $mangaId,
public ?int $page = 1, public ?int $page = 1,
public ?int $limit = 20, public ?int $limit = 20,
public ?string $sortOrder = 'desc', public ?string $sortOrder = 'desc'
) { ) {
} }
} }

View File

@@ -8,7 +8,7 @@ readonly class GetMangaList
public ?int $page = 1, public ?int $page = 1,
public ?int $limit = 20, public ?int $limit = 20,
public ?string $sortBy = 'title', public ?string $sortBy = 'title',
public ?string $sortOrder = 'asc', public ?string $sortOrder = 'asc'
) { ) {
} }
} }

View File

@@ -2,11 +2,13 @@
namespace App\Domain\Manga\Application\Query; namespace App\Domain\Manga\Application\Query;
use DateTimeImmutable;
readonly class MonitoringCriteria readonly class MonitoringCriteria
{ {
public function __construct( public function __construct(
public bool $enabled, public bool $enabled,
public ?\DateTimeImmutable $lastCheckBefore = null, public ?DateTimeImmutable $lastCheckBefore = null
) { ) {
} }
} }

View File

@@ -7,7 +7,7 @@ readonly class SearchLocalManga
public function __construct( public function __construct(
public string $query, public string $query,
public int $page = 1, public int $page = 1,
public int $limit = 20, public int $limit = 20
) { ) {
} }
} }

View File

@@ -5,7 +5,7 @@ namespace App\Domain\Manga\Application\Query;
readonly class SearchManga readonly class SearchManga
{ {
public function __construct( public function __construct(
public string $title, public string $title
) { ) {
} }
} }

View File

@@ -13,7 +13,7 @@ readonly class DiscoverMangaHandler
{ {
public function __construct( public function __construct(
private MangaRepositoryInterface $mangaRepository, private MangaRepositoryInterface $mangaRepository,
private MangaProviderInterface $mangaProvider, private MangaProviderInterface $mangaProvider
) { ) {
} }
@@ -41,7 +41,7 @@ readonly class DiscoverMangaHandler
$recommendations = array_values(array_filter( $recommendations = array_values(array_filter(
$collection->getItems(), $collection->getItems(),
fn (Manga $m) => null === $m->getExternalId() fn (Manga $m) => $m->getExternalId() === null
|| !in_array($m->getExternalId()->getValue(), $ownedExternalIds, true) || !in_array($m->getExternalId()->getValue(), $ownedExternalIds, true)
)); ));

View File

@@ -7,8 +7,8 @@ use App\Domain\Manga\Application\Response\DownloadResponse;
use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface; use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface;
use App\Domain\Manga\Domain\Contract\Service\FileServiceInterface; use App\Domain\Manga\Domain\Contract\Service\FileServiceInterface;
use App\Domain\Manga\Domain\Exception\CbzFileNotFoundException; use App\Domain\Manga\Domain\Exception\CbzFileNotFoundException;
use App\Domain\Manga\Domain\Exception\ChapterNotAvailableException;
use App\Domain\Manga\Domain\Exception\ChapterNotFoundException; use App\Domain\Manga\Domain\Exception\ChapterNotFoundException;
use App\Domain\Manga\Domain\Exception\ChapterNotAvailableException;
use App\Domain\Shared\Domain\Contract\QueryHandlerInterface; use App\Domain\Shared\Domain\Contract\QueryHandlerInterface;
use App\Domain\Shared\Domain\Contract\QueryInterface; use App\Domain\Shared\Domain\Contract\QueryInterface;
use App\Domain\Shared\Domain\Contract\ResponseInterface; use App\Domain\Shared\Domain\Contract\ResponseInterface;
@@ -17,7 +17,7 @@ readonly class DownloadCbzHandler implements QueryHandlerInterface
{ {
public function __construct( public function __construct(
private MangaRepositoryInterface $mangaRepository, private MangaRepositoryInterface $mangaRepository,
private FileServiceInterface $fileService, private FileServiceInterface $fileService
) { ) {
} }

View File

@@ -16,7 +16,7 @@ readonly class DownloadVolumeHandler implements QueryHandlerInterface
{ {
public function __construct( public function __construct(
private MangaRepositoryInterface $mangaRepository, private MangaRepositoryInterface $mangaRepository,
private FileServiceInterface $fileService, private FileServiceInterface $fileService
) { ) {
} }

View File

@@ -15,7 +15,7 @@ readonly class FindMangaMatchByFilenameHandler
{ {
public function __construct( public function __construct(
private FilenameAnalyzerInterface $filenameAnalyzer, private FilenameAnalyzerInterface $filenameAnalyzer,
private MangaRepositoryInterface $mangaRepository, private MangaRepositoryInterface $mangaRepository
) { ) {
} }
@@ -70,7 +70,7 @@ readonly class FindMangaMatchByFilenameHandler
/** /**
* Calcule un score de correspondance entre le manga et le titre recherché * Calcule un score de correspondance entre le manga et le titre recherché
* Score plus élevé = meilleure correspondance. * Score plus élevé = meilleure correspondance
*/ */
private function calculateMatchScore(Manga $manga, string $searchedTitle): int private function calculateMatchScore(Manga $manga, string $searchedTitle): int
{ {
@@ -97,12 +97,12 @@ readonly class FindMangaMatchByFilenameHandler
} }
// Le titre du manga contient le terme recherché // Le titre du manga contient le terme recherché
if (false !== stripos($mangaTitle, $searchedTitle)) { if (stripos($mangaTitle, $searchedTitle) !== false) {
$score += 50; $score += 50;
} }
// Le terme recherché contient le titre du manga // Le terme recherché contient le titre du manga
if (false !== stripos($searchedTitle, $mangaTitle)) { if (stripos($searchedTitle, $mangaTitle) !== false) {
$score += 40; $score += 40;
} }

View File

@@ -10,7 +10,7 @@ use App\Domain\Manga\Domain\Exception\MangaNotFoundException;
readonly class GetMangaByIdHandler readonly class GetMangaByIdHandler
{ {
public function __construct( public function __construct(
private MangaRepositoryInterface $mangaRepository, private MangaRepositoryInterface $mangaRepository
) { ) {
} }

View File

@@ -11,7 +11,7 @@ use App\Domain\Manga\Domain\Model\ValueObject\MangaSlug;
readonly class GetMangaBySlugHandler readonly class GetMangaBySlugHandler
{ {
public function __construct( public function __construct(
private MangaRepositoryInterface $mangaRepository, private MangaRepositoryInterface $mangaRepository
) { ) {
} }

View File

@@ -12,7 +12,7 @@ use App\Domain\Manga\Domain\Model\Chapter;
readonly class GetMangaChaptersHandler readonly class GetMangaChaptersHandler
{ {
public function __construct( public function __construct(
private MangaRepositoryInterface $mangaRepository, private MangaRepositoryInterface $mangaRepository
) { ) {
} }
@@ -30,7 +30,7 @@ readonly class GetMangaChaptersHandler
$grouped = $this->groupChapters($allChapters); $grouped = $this->groupChapters($allChapters);
if ('desc' === $query->sortOrder) { if ($query->sortOrder === 'desc') {
usort($grouped, fn (ChapterResponse $a, ChapterResponse $b) => $b->number <=> $a->number); usort($grouped, fn (ChapterResponse $a, ChapterResponse $b) => $b->number <=> $a->number);
} }
@@ -58,7 +58,7 @@ readonly class GetMangaChaptersHandler
$pagesDir = $chapter->getPagesDirectory(); $pagesDir = $chapter->getPagesDirectory();
$volume = $chapter->getVolume(); $volume = $chapter->getVolume();
if (null !== $pagesDir && null !== $volume) { if ($pagesDir !== null && $volume !== null) {
if ($pagesDir === $currentPagesDir && $volume === $currentVolume) { if ($pagesDir === $currentPagesDir && $volume === $currentVolume) {
$currentGroup[] = $chapter; $currentGroup[] = $chapter;
} else { } else {
@@ -104,7 +104,7 @@ readonly class GetMangaChaptersHandler
$max = max($numbers); $max = max($numbers);
$fmt = fn (float $n) => $n == (int) $n ? (string) (int) $n : (string) $n; $fmt = fn (float $n) => $n == (int) $n ? (string) (int) $n : (string) $n;
$range = count($group) > 1 ? $fmt($min).'-'.$fmt($max) : $fmt($min); $range = count($group) > 1 ? $fmt($min) . '-' . $fmt($max) : $fmt($min);
return new ChapterResponse( return new ChapterResponse(
id: $first->getId(), id: $first->getId(),

View File

@@ -3,13 +3,13 @@
namespace App\Domain\Manga\Application\QueryHandler; namespace App\Domain\Manga\Application\QueryHandler;
use App\Domain\Manga\Application\Query\GetMangaList; use App\Domain\Manga\Application\Query\GetMangaList;
use App\Domain\Manga\Application\Response\MangaListResponse;
use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface; use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface;
use App\Domain\Manga\Application\Response\MangaListResponse;
readonly class GetMangaListHandler readonly class GetMangaListHandler
{ {
public function __construct( public function __construct(
private MangaRepositoryInterface $mangaRepository, private MangaRepositoryInterface $mangaRepository
) { ) {
} }

View File

@@ -11,7 +11,7 @@ use App\Domain\Manga\Domain\Model\Manga;
readonly class SearchLocalMangaHandler readonly class SearchLocalMangaHandler
{ {
public function __construct( public function __construct(
private MangaRepositoryInterface $repository, private MangaRepositoryInterface $repository
) { ) {
} }

View File

@@ -11,7 +11,7 @@ use App\Domain\Manga\Domain\Model\Manga;
readonly class SearchMangaHandler readonly class SearchMangaHandler
{ {
public function __construct( public function __construct(
private MangaProviderInterface $mangaProvider, private MangaProviderInterface $mangaProvider
) { ) {
} }
@@ -19,6 +19,7 @@ readonly class SearchMangaHandler
{ {
$mangaCollection = $this->mangaProvider->search($query->title); $mangaCollection = $this->mangaProvider->search($query->title);
return new MangaSearchResponse( return new MangaSearchResponse(
array_map( array_map(
fn (Manga $manga, int $index) => new MangaSearchItem( fn (Manga $manga, int $index) => new MangaSearchItem(

View File

@@ -8,7 +8,7 @@ readonly class ChapterListResponse
public array $chapters, public array $chapters,
public int $total, public int $total,
public int $page, public int $page,
public int $limit, public int $limit
) { ) {
} }

View File

@@ -8,7 +8,7 @@ use Symfony\Component\HttpFoundation\Response;
readonly class DownloadResponse implements ResponseInterface readonly class DownloadResponse implements ResponseInterface
{ {
public function __construct( public function __construct(
public Response $httpResponse, public Response $httpResponse
) { ) {
} }
} }

View File

@@ -9,7 +9,7 @@ readonly class MangaListResponse
public int $total, public int $total,
public int $page, public int $page,
public int $limit, public int $limit,
public array $chapterCounts = [], public array $chapterCounts = []
) { ) {
} }

View File

@@ -14,7 +14,7 @@ readonly class MangaMatchItem
public ?string $thumbnailUrl, public ?string $thumbnailUrl,
public int $matchScore, public int $matchScore,
public ?float $chapterNumber = null, public ?float $chapterNumber = null,
public ?float $volumeNumber = null, public ?float $volumeNumber = null
) { ) {
} }
} }

View File

@@ -13,7 +13,7 @@ readonly class MangaMatchResponse
public array $matches, public array $matches,
public ?float $chapterNumber, public ?float $chapterNumber,
public ?float $volumeNumber, public ?float $volumeNumber,
public array $possibleTitles, public array $possibleTitles
) { ) {
} }

View File

@@ -18,7 +18,7 @@ readonly class MangaResponse
public ?string $imageUrl, public ?string $imageUrl,
public ?string $thumbnailUrl, public ?string $thumbnailUrl,
public ?float $rating, public ?float $rating,
public bool $monitored, public bool $monitored
) { ) {
} }
} }

View File

@@ -16,7 +16,7 @@ readonly class MangaSearchItem
public string $status, public string $status,
public ?string $imageUrl, public ?string $imageUrl,
public ?string $thumbnailUrl, public ?string $thumbnailUrl,
public ?float $rating, public ?float $rating
) { ) {
} }
} }

View File

@@ -11,7 +11,7 @@ readonly class SearchLocalMangaResponse
public array $items, public array $items,
public int $total, public int $total,
public int $page, public int $page,
public int $limit, public int $limit
) { ) {
} }

View File

@@ -7,12 +7,12 @@ use App\Domain\Manga\Domain\Exception\MangadexAuthenticationException;
interface MangadexClientInterface interface MangadexClientInterface
{ {
/** /**
* @throws MangadexAuthenticationException * @throws \App\Domain\Manga\Domain\Exception\MangadexAuthenticationException
*/ */
public function authenticate(): void; public function authenticate(): void;
/** /**
* @throws MangadexAuthenticationException * @throws \App\Domain\Manga\Domain\Exception\MangadexAuthenticationException
*/ */
public function refreshToken(): void; public function refreshToken(): void;
@@ -39,7 +39,6 @@ interface MangadexClientInterface
/** /**
* @param string[] $mangaIds * @param string[] $mangaIds
*
* @return array{ * @return array{
* statistics: array<string, array{ * statistics: array<string, array{
* rating: array{average: float} * rating: array{average: float}

View File

@@ -3,8 +3,8 @@
namespace App\Domain\Manga\Domain\Contract\Repository; namespace App\Domain\Manga\Domain\Contract\Repository;
use App\Domain\Manga\Application\Query\MonitoringCriteria; use App\Domain\Manga\Application\Query\MonitoringCriteria;
use App\Domain\Manga\Domain\Model\Chapter;
use App\Domain\Manga\Domain\Model\Manga; use App\Domain\Manga\Domain\Model\Manga;
use App\Domain\Manga\Domain\Model\Chapter;
use App\Domain\Manga\Domain\Model\ValueObject\ExternalId; use App\Domain\Manga\Domain\Model\ValueObject\ExternalId;
use App\Domain\Manga\Domain\Model\ValueObject\MangaSlug; use App\Domain\Manga\Domain\Model\ValueObject\MangaSlug;
@@ -13,21 +13,13 @@ interface MangaRepositoryInterface
// --- Manga --- // --- Manga ---
public function findAll(int $page = 1, int $limit = 20, string $sortBy = 'title', string $sortOrder = 'asc'): array; public function findAll(int $page = 1, int $limit = 20, string $sortBy = 'title', string $sortOrder = 'asc'): array;
public function count(): int; public function count(): int;
public function findById(string $id): ?Manga; public function findById(string $id): ?Manga;
public function findBySlug(MangaSlug $slug): ?Manga; public function findBySlug(MangaSlug $slug): ?Manga;
public function findByExternalId(ExternalId $externalId): ?Manga; public function findByExternalId(ExternalId $externalId): ?Manga;
public function save(Manga $manga): void; public function save(Manga $manga): void;
public function delete(Manga $manga): void; public function delete(Manga $manga): void;
public function search(string $query, int $page = 1, int $limit = 20): array; public function search(string $query, int $page = 1, int $limit = 20): array;
public function countSearch(string $query): int; public function countSearch(string $query): int;
/** /**
@@ -43,20 +35,14 @@ interface MangaRepositoryInterface
* @return Chapter[] * @return Chapter[]
*/ */
public function findAllChapters(string $mangaId, string $sortOrder = 'desc'): array; public function findAllChapters(string $mangaId, string $sortOrder = 'desc'): array;
public function countChapters(string $mangaId): int; public function countChapters(string $mangaId): int;
public function countAvailableChapters(string $mangaId): int; public function countAvailableChapters(string $mangaId): int;
public function findChapterById(string $id): ?Chapter; public function findChapterById(string $id): ?Chapter;
public function findVisibleChapterById(string $id): ?Chapter; public function findVisibleChapterById(string $id): ?Chapter;
public function findChapterByMangaIdAndNumber(string $mangaId, float $chapterNumber): ?Chapter; public function findChapterByMangaIdAndNumber(string $mangaId, float $chapterNumber): ?Chapter;
/** /**
* @param float[] $chapterNumbers * @param float[] $chapterNumbers
*
* @return array<float, Chapter> * @return array<float, Chapter>
*/ */
public function findExistingChaptersByNumbers(string $mangaId, array $chapterNumbers): array; public function findExistingChaptersByNumbers(string $mangaId, array $chapterNumbers): array;
@@ -75,4 +61,5 @@ interface MangaRepositoryInterface
* @return Chapter[] * @return Chapter[]
*/ */
public function findVisibleChaptersWithPagesByMangaIdAndVolume(string $mangaId, int $volume): array; public function findVisibleChaptersWithPagesByMangaIdAndVolume(string $mangaId, int $volume): array;
} }

View File

@@ -7,8 +7,7 @@ use App\Domain\Manga\Domain\Model\Manga;
interface ChapterSynchronizationServiceInterface interface ChapterSynchronizationServiceInterface
{ {
/** /**
* Synchronise les chapitres d'un manga depuis la source externe. * Synchronise les chapitres d'un manga depuis la source externe
*
* @return string[] IDs des nouveaux chapitres ajoutés * @return string[] IDs des nouveaux chapitres ajoutés
*/ */
public function synchronizeChapters(Manga $manga): array; public function synchronizeChapters(Manga $manga): array;

View File

@@ -7,24 +7,23 @@ use Symfony\Component\HttpFoundation\Response;
interface FileServiceInterface interface FileServiceInterface
{ {
/** /**
* Télécharge un fichier CBZ. * Télécharge un fichier CBZ
*/ */
public function downloadCbz(string $filePath, string $filename): Response; public function downloadCbz(string $filePath, string $filename): Response;
/** /**
* Crée un fichier ZIP contenant plusieurs CBZ. * Crée un fichier ZIP contenant plusieurs CBZ
*
* @param array<string> $cbzPaths * @param array<string> $cbzPaths
*/ */
public function createVolumeCbz(array $cbzPaths, string $volumeName): Response; public function createVolumeCbz(array $cbzPaths, string $volumeName): Response;
/** /**
* Supprime un fichier CBZ du système de fichiers. * Supprime un fichier CBZ du système de fichiers
*/ */
public function deleteCbzFile(string $filePath): bool; public function deleteCbzFile(string $filePath): bool;
/** /**
* Vérifie si un fichier CBZ existe. * Vérifie si un fichier CBZ existe
*/ */
public function cbzExists(string $filePath): bool; public function cbzExists(string $filePath): bool;
} }

View File

@@ -7,7 +7,7 @@ use App\Domain\Manga\Domain\Model\ValueObject\ChapterId;
readonly class ChapterReadyForScraping readonly class ChapterReadyForScraping
{ {
public function __construct( public function __construct(
public ChapterId $chapterId, public ChapterId $chapterId
) { ) {
} }
} }

View File

@@ -6,7 +6,7 @@ readonly class MangaCreated
{ {
public function __construct( public function __construct(
public string $mangaId, public string $mangaId,
public string $externalId, public string $externalId
) { ) {
} }
} }

View File

@@ -2,7 +2,9 @@
namespace App\Domain\Manga\Domain\Exception; namespace App\Domain\Manga\Domain\Exception;
class CbzFileNotFoundException extends \DomainException use DomainException;
class CbzFileNotFoundException extends DomainException
{ {
public function __construct(string $filePath) public function __construct(string $filePath)
{ {

View File

@@ -2,7 +2,9 @@
namespace App\Domain\Manga\Domain\Exception; namespace App\Domain\Manga\Domain\Exception;
class ChapterNotAvailableException extends \DomainException use DomainException;
class ChapterNotAvailableException extends DomainException
{ {
public function __construct(int $chapterId) public function __construct(int $chapterId)
{ {

View File

@@ -2,7 +2,9 @@
namespace App\Domain\Manga\Domain\Exception; namespace App\Domain\Manga\Domain\Exception;
class ChapterNotFoundException extends \DomainException use DomainException;
class ChapterNotFoundException extends DomainException
{ {
public function __construct(string $chapterId) public function __construct(string $chapterId)
{ {

View File

@@ -2,7 +2,9 @@
namespace App\Domain\Manga\Domain\Exception; namespace App\Domain\Manga\Domain\Exception;
class VolumeNotFoundException extends \DomainException use DomainException;
class VolumeNotFoundException extends DomainException
{ {
public function __construct(string $mangaId, int $volume) public function __construct(string $mangaId, int $volume)
{ {

View File

@@ -13,7 +13,7 @@ readonly class AnalyzedFilename
public function __construct( public function __construct(
private MangaTitle $title, private MangaTitle $title,
private ?ChapterNumber $chapterNumber = null, private ?ChapterNumber $chapterNumber = null,
private ?VolumeNumber $volumeNumber = null, private ?VolumeNumber $volumeNumber = null
) { ) {
} }
@@ -34,11 +34,11 @@ readonly class AnalyzedFilename
public function hasChapterNumber(): bool public function hasChapterNumber(): bool
{ {
return null !== $this->chapterNumber; return $this->chapterNumber !== null;
} }
public function hasVolumeNumber(): bool public function hasVolumeNumber(): bool
{ {
return null !== $this->volumeNumber; return $this->volumeNumber !== null;
} }
} }

View File

@@ -16,7 +16,7 @@ class Chapter
private bool $isVisible, private bool $isVisible,
private ?string $pagesDirectory = null, private ?string $pagesDirectory = null,
private int $pageCount = 0, private int $pageCount = 0,
private \DateTimeImmutable $createdAt = new \DateTimeImmutable(), private \DateTimeImmutable $createdAt = new \DateTimeImmutable()
) { ) {
} }
@@ -52,7 +52,7 @@ class Chapter
public function isAvailable(): bool public function isAvailable(): bool
{ {
return null !== $this->pagesDirectory; return $this->pagesDirectory !== null;
} }
public function getPagesDirectory(): ?string public function getPagesDirectory(): ?string

View File

@@ -9,6 +9,7 @@ use App\Domain\Manga\Domain\Model\ValueObject\MangaSlug;
use App\Domain\Manga\Domain\Model\ValueObject\MangaTitle; use App\Domain\Manga\Domain\Model\ValueObject\MangaTitle;
use App\Domain\Manga\Domain\Model\ValueObject\MonitoringStatus; use App\Domain\Manga\Domain\Model\ValueObject\MonitoringStatus;
use App\Domain\Shared\Domain\Model\AggregateRoot; use App\Domain\Shared\Domain\Model\AggregateRoot;
use DateTimeImmutable;
final class Manga extends AggregateRoot final class Manga extends AggregateRoot
{ {
@@ -35,9 +36,9 @@ final class Manga extends AggregateRoot
private ?float $rating = null, private ?float $rating = null,
private ?ImageUrls $imageUrls = null, private ?ImageUrls $imageUrls = null,
private array $alternativeSlugs = [], private array $alternativeSlugs = [],
private ?\DateTimeImmutable $createdAt = null, private ?DateTimeImmutable $createdAt = null,
private ?MonitoringStatus $monitoringStatus = null, private ?MonitoringStatus $monitoringStatus = null,
private ?\DateTimeImmutable $lastMonitoringCheck = null, private ?DateTimeImmutable $lastMonitoringCheck = null,
) { ) {
$this->monitoringStatus = $this->monitoringStatus ?? MonitoringStatus::disabled(); $this->monitoringStatus = $this->monitoringStatus ?? MonitoringStatus::disabled();
} }
@@ -157,7 +158,7 @@ final class Manga extends AggregateRoot
$this->alternativeSlugs = $alternativeSlugs; $this->alternativeSlugs = $alternativeSlugs;
} }
public function getCreatedAt(): ?\DateTimeImmutable public function getCreatedAt(): ?DateTimeImmutable
{ {
return $this->createdAt; return $this->createdAt;
} }
@@ -180,7 +181,7 @@ final class Manga extends AggregateRoot
public function enableMonitoring(): void public function enableMonitoring(): void
{ {
$this->monitoringStatus = MonitoringStatus::enabled(); $this->monitoringStatus = MonitoringStatus::enabled();
$this->lastMonitoringCheck = new \DateTimeImmutable(); $this->lastMonitoringCheck = new DateTimeImmutable();
} }
public function disableMonitoring(): void public function disableMonitoring(): void
@@ -189,12 +190,12 @@ final class Manga extends AggregateRoot
$this->lastMonitoringCheck = null; $this->lastMonitoringCheck = null;
} }
public function getLastMonitoringCheck(): ?\DateTimeImmutable public function getLastMonitoringCheck(): ?DateTimeImmutable
{ {
return $this->lastMonitoringCheck; return $this->lastMonitoringCheck;
} }
public function updateLastMonitoringCheck(\DateTimeImmutable $lastMonitoringCheck): void public function updateLastMonitoringCheck(DateTimeImmutable $lastMonitoringCheck): void
{ {
$this->lastMonitoringCheck = $lastMonitoringCheck; $this->lastMonitoringCheck = $lastMonitoringCheck;
} }

Some files were not shown because too many files have changed in this diff Show More