20 Commits

Author SHA1 Message Date
7204ea7754 Merge branch 'main' into feat/monitoring-run-command 2026-03-27 14:23:21 +01:00
ext.jeremy.guillot@maxicoffee.domains
f42b5a9cf5 feat(monitoring): ajouter une commande console pour déclencher le monitoring manuellement
Permet de tester le scheduler en prod sans attendre le cycle de 2h :
  make sf c="app:monitoring:run"
2026-03-27 14:21:05 +01:00
5edd28309f Merge pull request 'fix(monitoring): corriger le scheduler qui ne détectait plus les nouveaux chapitres' (#41) from fix/monitoring-scheduler-since-frozen into main
All checks were successful
Deploy / deploy (push) Successful in 1m8s
Reviewed-on: #41
2026-03-27 12:08:33 +01:00
ext.jeremy.guillot@maxicoffee.domains
3f08e1c899 fix(monitoring): corriger le scheduler qui ne détectait plus les nouveaux chapitres
- MonitoringSchedule : supprimer la date passée au message (était évaluée une
  seule fois au démarrage du container, rendant la requête caduque après le
  premier cycle)
- CheckMonitoredMangasHandler : calculer `since` dynamiquement à l'exécution
  (`new \DateTimeImmutable('-2 hours')`) plutôt que de dépendre du message
- AutoScrapingListener : corriger le TypeError silencieux — créer un ScrapingJob
  avant d'appeler ScrapeChapterHandler (paramètre jobId manquant)

Ajoute les tests unitaires CheckMonitoredMangasHandlerTest et AutoScrapingListenerTest.
2026-03-27 12:08:06 +01:00
214f470e77 Merge pull request 'fix(manga): afficher le titre du chapitre téléchargé individuellement' (#40) from fix/chapter-title-downloaded into main
All checks were successful
Deploy / deploy (push) Successful in 1m9s
Reviewed-on: #40
2026-03-27 11:30:16 +01:00
ext.jeremy.guillot@maxicoffee.domains
345434c25d fix(manga): afficher le titre du chapitre téléchargé individuellement
Quand un chapitre téléchargé est seul dans son groupe (volumeChapterCount === 1),
on affichait "Chapitre 42" au lieu du titre réel. La condition isVolumeGroup
s'appliquait même pour les groupes à un seul élément.

Fix : la mise en forme "Chapitres X-Y" n'est désormais appliquée que lorsque
volumeChapterCount > 1, sinon on affiche chapter.title comme pour les chapitres
non téléchargés.
2026-03-27 11:29:13 +01:00
2868772f5c Merge pull request 'fix(deploy): vider le cache prod au démarrage du conteneur' (#39) from fix/entrypoint-clear-cache into main
All checks were successful
Deploy / deploy (push) Successful in 1m9s
Reviewed-on: #39
2026-03-26 18:50:15 +01:00
a2469b6c07 Merge branch 'main' into fix/entrypoint-clear-cache 2026-03-26 18:50:08 +01:00
ext.jeremy.guillot@maxicoffee.domains
926f938c45 fix(deploy): vider le cache prod au démarrage du conteneur
Sans ce fix, les workers FrankenPHP démarrent avec l'ancien cache persisté
dans le volume Docker. Si les classes référencées (ex. EntityManagerGhost,
LazyGhostTrait) ne correspondent plus à la version déployée, les workers
crashent en boucle, rendant le conteneur instable et faisant échouer le
cache:clear lancé ensuite par Deployer (exit 137).

La suppression de var/cache/prod à l'entrypoint garantit que les workers
démarrent toujours sur un cache vierge, généré à chaud à la première requête.
2026-03-26 18:49:30 +01:00
5551d73962 Merge pull request 'fix: limiter les workers FrankenPHP et nettoyer le Dockerfile' (#38) from fix/cache-clear-oom into main
Some checks failed
Deploy / deploy (push) Failing after 35s
Reviewed-on: #38
2026-03-26 18:44:26 +01:00
395a0a16cb Merge branch 'main' into fix/cache-clear-oom 2026-03-26 18:44:17 +01:00
ext.jeremy.guillot@maxicoffee.domains
8e2e608ad9 fix: limiter les workers FrankenPHP et nettoyer le Dockerfile
- worker.Caddyfile : limiter à 2 workers FrankenPHP pour éviter l'OOM
  lors du cache:clear en prod (chaque worker charge le kernel Symfony
  complet, la valeur par défaut = nb de CPUs était trop élevée)
- Dockerfile : supprimer les COPY des assets UX (ux-live-component,
  ux-react, ux-turbo) supprimés de composer.json
2026-03-26 18:43:51 +01:00
0f80cb9fec Merge pull request 'fix(doctrine): supprimer auto_generate_proxy_classes et proxy_dir' (#37) from fix/doctrine-orm3-config into main
Some checks failed
Deploy / deploy (push) Failing after 36s
Reviewed-on: #37
2026-03-26 18:39:56 +01:00
a3477629fb Merge branch 'main' into fix/doctrine-orm3-config 2026-03-26 18:39:51 +01:00
ext.jeremy.guillot@maxicoffee.domains
cde701986e fix(doctrine): supprimer auto_generate_proxy_classes et proxy_dir
Ces options ont été supprimées de Doctrine Bundle 3.x / ORM 3.x.
Elles causaient une erreur "Unrecognized options" au cache:clear en prod.
2026-03-26 18:39:22 +01:00
b921768aef Merge pull request 'chore: supprimer les dépendances Twig/Stimulus/React/Turbo inutilisées' (#36) from chore/cleanup-unused-dependencies into main
Some checks failed
Deploy / deploy (push) Failing after 1m10s
Reviewed-on: #36
2026-03-26 18:36:11 +01:00
ext.jeremy.guillot@maxicoffee.domains
5f0178f784 chore: supprimer les dépendances Twig/Stimulus/React/Turbo inutilisées
PHP : suppression de symfony/stimulus-bundle, ux-live-component, ux-react,
ux-turbo, twig/extra-bundle et symfony/form (plus utilisés depuis la
migration vers Vue.js SPA).

npm : suppression de @hotwired/stimulus, @hotwired/turbo, react, react-dom,
alpinejs, bootstrap, daisyui, sortablejs, vuedraggable et leurs types.
Corrige l'erreur de déploiement causée par vitest@^4.1.0 (introuvable)
requis par les anciens packages @symfony/ux-react et @symfony/ux-turbo v2.33.0.
2026-03-26 18:35:40 +01:00
c610d22bd2 Merge pull request 'feature/upgrade-symfony-8' (#35) from feature/upgrade-symfony-8 into main
Some checks failed
Deploy / deploy (push) Failing after 21s
Reviewed-on: #35
2026-03-26 18:23:13 +01:00
ab2cf319ac Merge branch 'main' into feature/upgrade-symfony-8 2026-03-26 18:23:07 +01:00
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
21 changed files with 361 additions and 4509 deletions

View File

@@ -108,9 +108,6 @@ RUN composer install --no-cache --prefer-dist --no-dev --no-autoloader --no-scri
FROM node:22-alpine AS node_build
WORKDIR /app
COPY --link package.json package-lock.json ./
COPY --from=composer_deps /app/vendor/symfony/ux-live-component/assets ./vendor/symfony/ux-live-component/assets
COPY --from=composer_deps /app/vendor/symfony/ux-react/assets ./vendor/symfony/ux-react/assets
COPY --from=composer_deps /app/vendor/symfony/ux-turbo/assets ./vendor/symfony/ux-turbo/assets
RUN npm install
COPY --link assets ./assets
COPY --link webpack.config.js ./

View File

@@ -14,14 +14,14 @@
chapterId: chapter.id
}
}">
<template v-if="chapter.isVolumeGroup">
{{ chapter.volumeChapterCount > 1 ? 'Chapitres ' : 'Chapitre ' }}{{ chapter.volumeChaptersRange }}
<template v-if="chapter.isVolumeGroup && chapter.volumeChapterCount > 1">
Chapitres {{ chapter.volumeChaptersRange }}
</template>
<template v-else>{{ chapter.title || 'Sans titre' }}</template>
</router-link>
<span v-else class="text-gray-500 dark:text-gray-400">
<template v-if="chapter.isVolumeGroup">
{{ chapter.volumeChapterCount > 1 ? 'Chapitres ' : 'Chapitre ' }}{{ chapter.volumeChaptersRange }}
<template v-if="chapter.isVolumeGroup && chapter.volumeChapterCount > 1">
Chapitres {{ chapter.volumeChaptersRange }}
</template>
<template v-else>{{ chapter.title || 'Sans titre' }}</template>
</span>

View File

@@ -30,7 +30,6 @@
"symfony/dotenv": "8.0.*",
"symfony/expression-language": "8.0.*",
"symfony/flex": "^2",
"symfony/form": "8.0.*",
"symfony/framework-bundle": "8.0.*",
"symfony/http-client": "8.0.*",
"symfony/mercure-bundle": "^0.4",
@@ -44,15 +43,10 @@
"symfony/scheduler": "8.0.*",
"symfony/security-bundle": "8.0.*",
"symfony/serializer": "8.0.*",
"symfony/stimulus-bundle": "^2.17",
"symfony/twig-bundle": "8.0.*",
"symfony/ux-live-component": "^2.17",
"symfony/ux-react": "^2.23",
"symfony/ux-turbo": "^2.18",
"symfony/validator": "8.0.*",
"symfony/webpack-encore-bundle": "^2.1",
"symfony/yaml": "8.0.*",
"twig/extra-bundle": "^2.12|^3.0",
"twig/twig": "^2.12|^3.0",
"vich/uploader-bundle": "^2.7"
},

849
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "281edff65ffa4e019c69d0ffbef5f223",
"content-hash": "6c61952b2d792d9e9204594abd228d6f",
"packages": [
{
"name": "api-platform/core",
@@ -4822,101 +4822,6 @@
],
"time": "2025-11-16T09:38:19+00:00"
},
{
"name": "symfony/form",
"version": "v8.0.7",
"source": {
"type": "git",
"url": "https://github.com/symfony/form.git",
"reference": "954e17b053dad9fb227ebd90260752e3a46bb06a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/form/zipball/954e17b053dad9fb227ebd90260752e3a46bb06a",
"reference": "954e17b053dad9fb227ebd90260752e3a46bb06a",
"shasum": ""
},
"require": {
"php": ">=8.4",
"symfony/event-dispatcher": "^7.4|^8.0",
"symfony/options-resolver": "^7.4|^8.0",
"symfony/polyfill-ctype": "^1.8",
"symfony/polyfill-intl-icu": "^1.21",
"symfony/polyfill-mbstring": "^1.0",
"symfony/property-access": "^7.4|^8.0",
"symfony/service-contracts": "^2.5|^3"
},
"conflict": {
"symfony/intl": "<7.4",
"symfony/translation-contracts": "<2.5",
"symfony/validator": "<7.4"
},
"require-dev": {
"doctrine/collections": "^1.0|^2.0",
"symfony/clock": "^7.4|^8.0",
"symfony/config": "^7.4|^8.0",
"symfony/console": "^7.4|^8.0",
"symfony/dependency-injection": "^7.4|^8.0",
"symfony/expression-language": "^7.4|^8.0",
"symfony/html-sanitizer": "^7.4|^8.0",
"symfony/http-foundation": "^7.4|^8.0",
"symfony/http-kernel": "^7.4|^8.0",
"symfony/intl": "^7.4|^8.0",
"symfony/security-core": "^7.4|^8.0",
"symfony/security-csrf": "^7.4|^8.0",
"symfony/translation": "^7.4|^8.0",
"symfony/uid": "^7.4|^8.0",
"symfony/validator": "^7.4|^8.0",
"symfony/var-dumper": "^7.4|^8.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Form\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Allows to easily create, process and reuse HTML forms",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/form/tree/v8.0.7"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-03-06T13:17:40+00:00"
},
{
"name": "symfony/framework-bundle",
"version": "v8.0.7",
@@ -5913,77 +5818,6 @@
],
"time": "2025-12-08T08:00:13+00:00"
},
{
"name": "symfony/options-resolver",
"version": "v8.0.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/options-resolver.git",
"reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/options-resolver/zipball/d2b592535ffa6600c265a3893a7f7fd2bad82dd7",
"reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7",
"shasum": ""
},
"require": {
"php": ">=8.4",
"symfony/deprecation-contracts": "^2.5|^3"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\OptionsResolver\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides an improved replacement for the array_replace PHP function",
"homepage": "https://symfony.com",
"keywords": [
"config",
"configuration",
"options"
],
"support": {
"source": "https://github.com/symfony/options-resolver/tree/v8.0.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2025-11-12T15:55:31+00:00"
},
{
"name": "symfony/panther",
"version": "v2.4.0",
@@ -6228,94 +6062,6 @@
],
"time": "2025-06-27T09:58:17+00:00"
},
{
"name": "symfony/polyfill-intl-icu",
"version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-icu.git",
"reference": "bfc8fa13dbaf21d69114b0efcd72ab700fb04d0c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-icu/zipball/bfc8fa13dbaf21d69114b0efcd72ab700fb04d0c",
"reference": "bfc8fa13dbaf21d69114b0efcd72ab700fb04d0c",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"suggest": {
"ext-intl": "For best performance and support of other locales than \"en\""
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Intl\\Icu\\": ""
},
"classmap": [
"Resources/stubs"
],
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for intl's ICU-related data and classes",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"icu",
"intl",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-icu/tree/v1.33.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2025-06-20T22:24:30+00:00"
},
{
"name": "symfony/polyfill-intl-idn",
"version": "v1.33.0",
@@ -7737,79 +7483,6 @@
],
"time": "2025-07-15T11:30:57+00:00"
},
{
"name": "symfony/stimulus-bundle",
"version": "v2.34.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/stimulus-bundle.git",
"reference": "d610a2e021cf63f955838b4bfe40da7e4cafe850"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/stimulus-bundle/zipball/d610a2e021cf63f955838b4bfe40da7e4cafe850",
"reference": "d610a2e021cf63f955838b4bfe40da7e4cafe850",
"shasum": ""
},
"require": {
"php": ">=8.1",
"symfony/config": "^5.4|^6.0|^7.0|^8.0",
"symfony/dependency-injection": "^5.4|^6.0|^7.0|^8.0",
"symfony/deprecation-contracts": "^2.0|^3.0",
"symfony/finder": "^5.4|^6.0|^7.0|^8.0",
"symfony/http-kernel": "^5.4|^6.0|^7.0|^8.0",
"twig/twig": "^2.15.3|^3.8"
},
"require-dev": {
"symfony/asset-mapper": "^6.3|^7.0|^8.0",
"symfony/framework-bundle": "^5.4|^6.0|^7.0|^8.0",
"symfony/phpunit-bridge": "^5.4|^6.0|^7.0|^8.0",
"symfony/twig-bundle": "^5.4|^6.0|^7.0|^8.0",
"zenstruck/browser": "^1.4"
},
"type": "symfony-bundle",
"autoload": {
"psr-4": {
"Symfony\\UX\\StimulusBundle\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Integration with your Symfony app & Stimulus!",
"keywords": [
"symfony-ux"
],
"support": {
"source": "https://github.com/symfony/stimulus-bundle/tree/v2.34.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-03-21T22:29:11+00:00"
},
{
"name": "symfony/stopwatch",
"version": "v8.0.0",
@@ -8321,379 +7994,6 @@
],
"time": "2026-03-04T13:55:34+00:00"
},
{
"name": "symfony/ux-live-component",
"version": "v2.34.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/ux-live-component.git",
"reference": "f246c189192121781c267e26a64ff6942ef61ab6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/ux-live-component/zipball/f246c189192121781c267e26a64ff6942ef61ab6",
"reference": "f246c189192121781c267e26a64ff6942ef61ab6",
"shasum": ""
},
"require": {
"php": ">=8.1",
"symfony/deprecation-contracts": "^2.5|^3.0",
"symfony/property-access": "^5.4.5|^6.0|^7.0|^8.0",
"symfony/property-info": "^5.4|^6.0|^7.0|^8.0",
"symfony/stimulus-bundle": "^2.9",
"symfony/ux-twig-component": "^2.33.0",
"twig/twig": "^3.10.3"
},
"conflict": {
"symfony/config": "<5.4.0",
"symfony/property-info": "~7.0.0",
"symfony/type-info": "<7.2"
},
"require-dev": {
"doctrine/annotations": "^1.0|^2.0",
"doctrine/collections": "^1.6.8|^2.0",
"doctrine/doctrine-bundle": "^2.4.3|^3.0|^4.0",
"doctrine/orm": "^2.9.4|^3.0",
"doctrine/persistence": "^2.5.2|^3.0|^4.0",
"phpdocumentor/reflection-docblock": "^5.6.2",
"symfony/config": "^6.3|^7.0|^8.0",
"symfony/dependency-injection": "^5.4|^6.0|^7.0|^8.0",
"symfony/expression-language": "^5.4|^6.0|^7.0|^8.0",
"symfony/form": "^5.4|^6.0|^7.0|^8.0",
"symfony/framework-bundle": "^5.4|^6.1|^7.0|^8.0",
"symfony/http-kernel": "^6.1|^7.0|^8.0",
"symfony/options-resolver": "^5.4|^6.0|^7.0|^8.0",
"symfony/phpunit-bridge": "^6.1|^7.0|^8.0",
"symfony/security-bundle": "^5.4|^6.0|^7.0|^8.0",
"symfony/serializer": "^5.4|^6.0|^7.0|^8.0",
"symfony/twig-bundle": "^5.4|^6.0|^7.0|^8.0",
"symfony/uid": "^5.4|^6.0|^7.0|^8.0",
"symfony/validator": "^5.4|^6.0|^7.0|^8.0",
"zenstruck/browser": "^1.2.0",
"zenstruck/foundry": "^2.0"
},
"type": "symfony-bundle",
"extra": {
"thanks": {
"url": "https://github.com/symfony/ux",
"name": "symfony/ux"
}
},
"autoload": {
"psr-4": {
"Symfony\\UX\\LiveComponent\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Live components for Symfony",
"homepage": "https://symfony.com",
"keywords": [
"components",
"symfony-ux",
"twig"
],
"support": {
"source": "https://github.com/symfony/ux-live-component/tree/v2.34.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-03-21T22:29:11+00:00"
},
{
"name": "symfony/ux-react",
"version": "v2.34.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/ux-react.git",
"reference": "42ee2b86e3af8493e4a008ebe2af166c2c3d4d05"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/ux-react/zipball/42ee2b86e3af8493e4a008ebe2af166c2c3d4d05",
"reference": "42ee2b86e3af8493e4a008ebe2af166c2c3d4d05",
"shasum": ""
},
"require": {
"php": ">=8.1",
"symfony/stimulus-bundle": "^2.9.1"
},
"require-dev": {
"symfony/asset-mapper": "^6.3|^7.0|^8.0",
"symfony/finder": "^5.4|^6.0|^7.0|^8.0",
"symfony/framework-bundle": "^5.4|^6.0|^7.0|^8.0",
"symfony/phpunit-bridge": "^5.4|^6.0|^7.0|^8.0",
"symfony/twig-bundle": "^5.4|^6.0|^7.0|^8.0",
"symfony/var-dumper": "^5.4|^6.0|^7.0|^8.0"
},
"type": "symfony-bundle",
"extra": {
"thanks": {
"url": "https://github.com/symfony/ux",
"name": "symfony/ux"
}
},
"autoload": {
"psr-4": {
"Symfony\\UX\\React\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Titouan Galopin",
"email": "galopintitouan@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Integration of React in Symfony",
"homepage": "https://symfony.com",
"keywords": [
"symfony-ux"
],
"support": {
"source": "https://github.com/symfony/ux-react/tree/v2.34.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-03-21T22:29:11+00:00"
},
{
"name": "symfony/ux-turbo",
"version": "v2.34.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/ux-turbo.git",
"reference": "87511f621db238302a3bb819958a72feda27fc45"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/ux-turbo/zipball/87511f621db238302a3bb819958a72feda27fc45",
"reference": "87511f621db238302a3bb819958a72feda27fc45",
"shasum": ""
},
"require": {
"php": ">=8.1",
"symfony/stimulus-bundle": "^2.9.1"
},
"conflict": {
"symfony/flex": "<1.13"
},
"require-dev": {
"dbrekelmans/bdi": "dev-main",
"doctrine/doctrine-bundle": "^2.4.3|^3.0|^4.0",
"doctrine/orm": "^2.8|^3.0",
"php-webdriver/webdriver": "^1.15",
"phpstan/phpstan": "^2.1.17",
"symfony/asset-mapper": "^6.4|^7.0|^8.0",
"symfony/debug-bundle": "^5.4|^6.0|^7.0|^8.0",
"symfony/expression-language": "^5.4|^6.0|^7.0|^8.0",
"symfony/form": "^5.4|^6.0|^7.0|^8.0",
"symfony/framework-bundle": "^6.4|^7.0|^8.0",
"symfony/mercure-bundle": "^0.3.7|^0.4.1",
"symfony/messenger": "^5.4|^6.0|^7.0|^8.0",
"symfony/panther": "^2.2",
"symfony/phpunit-bridge": "^5.4|^6.0|^7.0|^8.0",
"symfony/process": "^5.4|6.3.*|^7.0|^8.0",
"symfony/property-access": "^5.4|^6.0|^7.0|^8.0",
"symfony/security-core": "^5.4|^6.0|^7.0|^8.0",
"symfony/stopwatch": "^5.4|^6.0|^7.0|^8.0",
"symfony/twig-bundle": "^6.4|^7.0|^8.0",
"symfony/ux-twig-component": "^2.21",
"symfony/web-profiler-bundle": "^5.4|^6.0|^7.0|^8.0"
},
"type": "symfony-bundle",
"extra": {
"thanks": {
"url": "https://github.com/symfony/ux",
"name": "symfony/ux"
}
},
"autoload": {
"psr-4": {
"Symfony\\UX\\Turbo\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Kévin Dunglas",
"email": "kevin@dunglas.fr"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Hotwire Turbo integration for Symfony",
"homepage": "https://symfony.com",
"keywords": [
"hotwire",
"javascript",
"mercure",
"symfony-ux",
"turbo",
"turbo-stream"
],
"support": {
"source": "https://github.com/symfony/ux-turbo/tree/v2.34.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-03-21T22:29:11+00:00"
},
{
"name": "symfony/ux-twig-component",
"version": "v2.34.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/ux-twig-component.git",
"reference": "f9942e32246fe3fa9d31f60cffc1ada4d274830a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/ux-twig-component/zipball/f9942e32246fe3fa9d31f60cffc1ada4d274830a",
"reference": "f9942e32246fe3fa9d31f60cffc1ada4d274830a",
"shasum": ""
},
"require": {
"php": ">=8.1",
"symfony/dependency-injection": "^5.4|^6.0|^7.0|^8.0",
"symfony/deprecation-contracts": "^2.2|^3.0",
"symfony/event-dispatcher": "^5.4|^6.0|^7.0|^8.0",
"symfony/property-access": "^5.4|^6.0|^7.0|^8.0",
"twig/twig": "^3.10.3"
},
"conflict": {
"symfony/config": "<5.4.0"
},
"require-dev": {
"symfony/console": "^5.4|^6.0|^7.0|^8.0",
"symfony/css-selector": "^5.4|^6.0|^7.0|^8.0",
"symfony/dom-crawler": "^5.4|^6.0|^7.0|^8.0",
"symfony/framework-bundle": "^5.4|^6.0|^7.0|^8.0",
"symfony/phpunit-bridge": "^6.0|^7.0|^8.0",
"symfony/stimulus-bundle": "^2.9.1",
"symfony/twig-bundle": "^5.4|^6.0|^7.0|^8.0",
"symfony/webpack-encore-bundle": "^1.15|^2.3.0"
},
"type": "symfony-bundle",
"extra": {
"thanks": {
"url": "https://github.com/symfony/ux",
"name": "symfony/ux"
}
},
"autoload": {
"psr-4": {
"Symfony\\UX\\TwigComponent\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Twig components for Symfony",
"homepage": "https://symfony.com",
"keywords": [
"components",
"symfony-ux",
"twig"
],
"support": {
"source": "https://github.com/symfony/ux-twig-component/tree/v2.34.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-03-15T18:48:53+00:00"
},
{
"name": "symfony/validator",
"version": "v8.0.7",
@@ -9191,80 +8491,6 @@
],
"time": "2026-02-09T10:14:57+00:00"
},
{
"name": "twig/extra-bundle",
"version": "v3.24.0",
"source": {
"type": "git",
"url": "https://github.com/twigphp/twig-extra-bundle.git",
"reference": "6a621fcb1f28aa9ea7b34a99047ae0cdf5b834c9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/twig-extra-bundle/zipball/6a621fcb1f28aa9ea7b34a99047ae0cdf5b834c9",
"reference": "6a621fcb1f28aa9ea7b34a99047ae0cdf5b834c9",
"shasum": ""
},
"require": {
"php": ">=8.1.0",
"symfony/framework-bundle": "^5.4|^6.4|^7.0|^8.0",
"symfony/twig-bundle": "^5.4|^6.4|^7.0|^8.0",
"twig/twig": "^3.2|^4.0"
},
"require-dev": {
"league/commonmark": "^2.7",
"symfony/phpunit-bridge": "^6.4|^7.0",
"twig/cache-extra": "^3.0",
"twig/cssinliner-extra": "^3.0",
"twig/html-extra": "^3.0",
"twig/inky-extra": "^3.0",
"twig/intl-extra": "^3.0",
"twig/markdown-extra": "^3.0",
"twig/string-extra": "^3.0"
},
"type": "symfony-bundle",
"autoload": {
"psr-4": {
"Twig\\Extra\\TwigExtraBundle\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com",
"homepage": "http://fabien.potencier.org",
"role": "Lead Developer"
}
],
"description": "A Symfony bundle for extra Twig extensions",
"homepage": "https://twig.symfony.com",
"keywords": [
"bundle",
"extra",
"twig"
],
"support": {
"source": "https://github.com/twigphp/twig-extra-bundle/tree/v3.24.0"
},
"funding": [
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/twig/twig",
"type": "tidelift"
}
],
"time": "2026-02-07T08:07:38+00:00"
},
{
"name": "twig/twig",
"version": "v3.24.0",
@@ -13096,6 +12322,77 @@
],
"time": "2026-03-18T13:39:06+00:00"
},
{
"name": "symfony/options-resolver",
"version": "v8.0.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/options-resolver.git",
"reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/options-resolver/zipball/d2b592535ffa6600c265a3893a7f7fd2bad82dd7",
"reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7",
"shasum": ""
},
"require": {
"php": ">=8.4",
"symfony/deprecation-contracts": "^2.5|^3"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\OptionsResolver\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides an improved replacement for the array_replace PHP function",
"homepage": "https://symfony.com",
"keywords": [
"config",
"configuration",
"options"
],
"support": {
"source": "https://github.com/symfony/options-resolver/tree/v8.0.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2025-11-12T15:55:31+00:00"
},
{
"name": "symfony/phpunit-bridge",
"version": "v8.0.7",
@@ -13655,5 +12952,5 @@
"ext-zip": "*"
},
"platform-dev": {},
"plugin-api-version": "2.6.0"
"plugin-api-version": "2.9.0"
}

View File

@@ -14,13 +14,7 @@ return [
Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true],
Symfony\Bundle\MonologBundle\MonologBundle::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\UX\Turbo\TurboBundle::class => ['all' => true],
DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true],
Symfony\UX\React\ReactBundle::class => ['all' => true],
Vich\UploaderBundle\VichUploaderBundle::class => ['all' => true],
];

View File

@@ -1,11 +0,0 @@
# Enable stateless CSRF protection for forms and logins/logouts
framework:
form:
csrf_protection:
token_id: submit
csrf_protection:
stateless_token_ids:
- submit
- authenticate
- logout

View File

@@ -42,8 +42,6 @@ when@test:
when@prod:
doctrine:
orm:
auto_generate_proxy_classes: false
proxy_dir: '%kernel.build_dir%/doctrine/orm/Proxies'
query_cache_driver:
type: pool
pool: doctrine.system_cache_pool

View File

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

View File

@@ -148,7 +148,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* cookie_name?: scalar|Param|null, // The name of the cookie to use when using stateless protection. // Default: "csrf-token"
* },
* form?: bool|array{ // Form configuration
* enabled?: bool|Param, // Default: true
* enabled?: bool|Param, // Default: false
* csrf_protection?: bool|array{
* enabled?: scalar|Param|null, // Default: null
* token_id?: scalar|Param|null, // Default: null
@@ -1765,73 +1765,6 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* script_attributes?: array<string, scalar|Param|null>,
* link_attributes?: array<string, scalar|Param|null>,
* }
* @psalm-type TwigComponentConfig = array{
* defaults?: array<string, string|array{ // Default: ["__deprecated__use_old_naming_behavior"]
* template_directory?: scalar|Param|null, // Default: "components"
* name_prefix?: scalar|Param|null, // Default: ""
* }>,
* anonymous_template_directory?: scalar|Param|null, // Defaults to `components`
* profiler?: bool|array{ // Enables the profiler for Twig Component
* enabled?: bool|Param, // Default: "%kernel.debug%"
* collect_components?: bool|Param, // Collect components instances // Default: true
* },
* controllers_json?: scalar|Param|null, // Deprecated: The "twig_component.controllers_json" config option is deprecated, and will be removed in 3.0. // Default: null
* }
* @psalm-type LiveComponentConfig = array{
* secret?: scalar|Param|null, // The secret used to compute fingerprints and checksums // Default: "%kernel.secret%"
* fetch_credentials?: "same-origin"|"include"|"omit"|Param, // The default fetch credentials mode for all Live Components ('same-origin', 'include', 'omit') // Default: "same-origin"
* }
* @psalm-type StimulusConfig = array{
* controller_paths?: list<scalar|Param|null>,
* controllers_json?: scalar|Param|null, // Default: "%kernel.project_dir%/assets/controllers.json"
* }
* @psalm-type TwigExtraConfig = array{
* cache?: bool|array{
* enabled?: bool|Param, // Default: false
* },
* html?: bool|array{
* enabled?: bool|Param, // Default: false
* },
* markdown?: bool|array{
* enabled?: bool|Param, // Default: false
* },
* intl?: bool|array{
* enabled?: bool|Param, // Default: false
* },
* cssinliner?: bool|array{
* enabled?: bool|Param, // Default: false
* },
* inky?: bool|array{
* enabled?: bool|Param, // Default: false
* },
* string?: bool|array{
* enabled?: bool|Param, // Default: false
* },
* commonmark?: array{
* renderer?: array{ // Array of options for rendering HTML.
* block_separator?: scalar|Param|null,
* inner_separator?: scalar|Param|null,
* soft_break?: scalar|Param|null,
* },
* html_input?: "strip"|"allow"|"escape"|Param, // How to handle HTML input.
* allow_unsafe_links?: bool|Param, // Remove risky link and image URLs by setting this to false. // Default: true
* max_nesting_level?: int|Param, // The maximum nesting level for blocks. // Default: 9223372036854775807
* max_delimiters_per_line?: int|Param, // The maximum number of strong/emphasis delimiters per line. // Default: 9223372036854775807
* slug_normalizer?: array{ // Array of options for configuring how URL-safe slugs are created.
* instance?: mixed,
* max_length?: int|Param, // Default: 255
* unique?: mixed,
* },
* commonmark?: array{ // Array of options for configuring the CommonMark core extension.
* enable_em?: bool|Param, // Default: true
* enable_strong?: bool|Param, // Default: true
* use_asterisk?: bool|Param, // Default: true
* use_underscore?: bool|Param, // Default: true
* unordered_list_markers?: list<scalar|Param|null>,
* },
* ...<mixed>
* },
* }
* @psalm-type MercureConfig = array{
* hubs?: array<string, array{ // Default: []
* url?: scalar|Param|null, // URL of the hub's publish endpoint // Default: null
@@ -1853,26 +1786,12 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* default_cookie_lifetime?: int|Param, // Default lifetime of the cookie containing the JWT, in seconds. Defaults to the value of "framework.session.cookie_lifetime". // Default: null
* enable_profiler?: bool|Param, // Deprecated: The child node "enable_profiler" at path "mercure.enable_profiler" is deprecated. // Enable Symfony Web Profiler integration.
* }
* @psalm-type TurboConfig = array{
* broadcast?: bool|array{
* enabled?: bool|Param, // Default: true
* entity_template_prefixes?: list<scalar|Param|null>,
* doctrine_orm?: bool|array{ // Enable the Doctrine ORM integration
* enabled?: bool|Param, // Default: true
* },
* },
* default_transport?: scalar|Param|null, // Default: "default"
* }
* @psalm-type DamaDoctrineTestConfig = array{
* enable_static_connection?: mixed, // Default: true
* enable_static_meta_data_cache?: bool|Param, // Default: true
* enable_static_query_cache?: bool|Param, // Default: true
* connection_keys?: list<mixed>,
* }
* @psalm-type ReactConfig = array{
* controllers_path?: scalar|Param|null, // The path to the directory where React controller components are stored - relevant only when using symfony/asset-mapper. // Default: "%kernel.project_dir%/assets/react/controllers"
* name_glob?: list<scalar|Param|null>,
* }
* @psalm-type VichUploaderConfig = array{
* default_filename_attribute_suffix?: scalar|Param|null, // Default: "_name"
* db_driver?: scalar|Param|null,
@@ -1924,13 +1843,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* api_platform?: ApiPlatformConfig,
* monolog?: MonologConfig,
* webpack_encore?: WebpackEncoreConfig,
* twig_component?: TwigComponentConfig,
* live_component?: LiveComponentConfig,
* stimulus?: StimulusConfig,
* twig_extra?: TwigExtraConfig,
* mercure?: MercureConfig,
* turbo?: TurboConfig,
* react?: ReactConfig,
* vich_uploader?: VichUploaderConfig,
* "when@dev"?: array{
* imports?: ImportsConfig,
@@ -1948,13 +1861,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* zenstruck_foundry?: ZenstruckFoundryConfig,
* monolog?: MonologConfig,
* webpack_encore?: WebpackEncoreConfig,
* twig_component?: TwigComponentConfig,
* live_component?: LiveComponentConfig,
* stimulus?: StimulusConfig,
* twig_extra?: TwigExtraConfig,
* mercure?: MercureConfig,
* turbo?: TurboConfig,
* react?: ReactConfig,
* vich_uploader?: VichUploaderConfig,
* },
* "when@prod"?: array{
@@ -1970,13 +1877,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* api_platform?: ApiPlatformConfig,
* monolog?: MonologConfig,
* webpack_encore?: WebpackEncoreConfig,
* twig_component?: TwigComponentConfig,
* live_component?: LiveComponentConfig,
* stimulus?: StimulusConfig,
* twig_extra?: TwigExtraConfig,
* mercure?: MercureConfig,
* turbo?: TurboConfig,
* react?: ReactConfig,
* vich_uploader?: VichUploaderConfig,
* },
* "when@test"?: array{
@@ -1994,14 +1895,8 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* zenstruck_foundry?: ZenstruckFoundryConfig,
* monolog?: MonologConfig,
* webpack_encore?: WebpackEncoreConfig,
* twig_component?: TwigComponentConfig,
* live_component?: LiveComponentConfig,
* stimulus?: StimulusConfig,
* twig_extra?: TwigExtraConfig,
* mercure?: MercureConfig,
* turbo?: TurboConfig,
* dama_doctrine_test?: DamaDoctrineTestConfig,
* react?: ReactConfig,
* vich_uploader?: VichUploaderConfig,
* },
* ...<string, ExtensionType|array{ // extra keys must follow the when@%env% pattern or match an extension alias

View File

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

View File

@@ -53,6 +53,13 @@ if [ "$1" = 'frankenphp' ] || [ "$1" = 'php' ] || [ "$1" = 'bin/console' ]; then
fi
fi
# Vider le cache prod stale avant le démarrage des workers FrankenPHP.
# Sans ça, les workers chargent l'ancien cache du volume Docker et crashent
# en boucle si les classes du cache ne correspondent plus à la version déployée.
if [ "$APP_ENV" = "prod" ]; then
rm -rf var/cache/prod
fi
setfacl -R -m u:www-data:rwX -m u:"$(whoami)":rwX var
setfacl -dR -m u:www-data:rwX -m u:"$(whoami)":rwX var
fi

View File

@@ -1,3 +1,4 @@
worker {
file ./public/index.php
num 2
}

3506
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,26 +2,15 @@
"devDependencies": {
"@babel/core": "^7.17.0",
"@babel/preset-env": "^7.16.0",
"@babel/preset-react": "^7.26.3",
"@headlessui/vue": "^1.7.23",
"@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",
"@vue/compiler-sfc": "^3.5.13",
"core-js": "^3.23.0",
"daisyui": "^4.4.2",
"pinia": "^3.0.1",
"react": "^18.0",
"react-dom": "^18.0",
"regenerator-runtime": "^0.13.9",
"sass": "^1.59.3",
"sass-loader": "^13.2.0",
"stimulus-use": "^0.52.2",
"vue": "^3.5.13",
"vue-loader": "^17.4.2",
"vue-router": "^4.5.0",
@@ -41,18 +30,12 @@
"@fortawesome/fontawesome-free": "^6.5.2",
"@fortawesome/fontawesome-svg-core": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"@tanstack/vue-query": "^5.71.0",
"alpinejs": "^3.13.3",
"autoprefixer": "^10.4.14",
"axios": "^1.7.9",
"bootstrap": "^5.3.3",
"postcss-loader": "^7.1.0",
"puppeteer": "^22.10.0",
"react-router-dom": "^7.1.5",
"sortablejs": "^1.15.2",
"tailwindcss": "^3.2.7",
"vue-i18n": "^11.3.0",
"vuedraggable": "^2.24.3"
"vue-i18n": "^11.3.0"
}
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace App\Command;
use App\Domain\Manga\Application\Command\CheckMonitoredMangas;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Messenger\MessageBusInterface;
#[AsCommand(
name: 'app:monitoring:run',
description: 'Déclenche immédiatement la vérification des mangas monitorés (sans attendre le scheduler)',
)]
class RunMonitoringCommand extends Command
{
public function __construct(
private readonly MessageBusInterface $commandBus,
) {
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$output->writeln('Déclenchement du monitoring des mangas...');
$this->commandBus->dispatch(new CheckMonitoredMangas());
$output->writeln('<info>Vérification lancée. Les nouveaux chapitres détectés seront scrappés via le worker commands.</info>');
return Command::SUCCESS;
}
}

View File

@@ -20,7 +20,7 @@ readonly class CheckMonitoredMangasHandler
{
$criteria = new MonitoringCriteria(
enabled: true,
lastCheckBefore: $command->since ?? new \DateTimeImmutable('-1 hour')
lastCheckBefore: new \DateTimeImmutable('-2 hours')
);
$monitoredMangas = $this->mangaRepository->findByMonitoringCriteria($criteria);

View File

@@ -21,9 +21,7 @@ class MonitoringSchedule implements ScheduleProviderInterface
{
return (new Schedule())->add(
// Toutes les 2 heures, vérifie les mangas qui n'ont pas été vérifiés depuis 2 heures
RecurringMessage::every('2 hours', new CheckMonitoredMangas(
new \DateTimeImmutable('-2 hours')
))
RecurringMessage::every('2 hours', new CheckMonitoredMangas())
)->stateful($this->cache);
}
}

View File

@@ -7,6 +7,9 @@ use App\Domain\Scraping\Application\Command\ScrapeChapter;
use App\Domain\Scraping\Application\CommandHandler\ScrapeChapterHandler;
use App\Domain\Scraping\Domain\Contract\Repository\ChapterRepositoryInterface;
use App\Domain\Scraping\Domain\Contract\Repository\MangaRepositoryInterface;
use App\Domain\Scraping\Domain\Model\ScrapingJob;
use App\Domain\Shared\Domain\Contract\JobRepositoryInterface;
use Ramsey\Uuid\Uuid;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
class AutoScrapingListener
@@ -15,6 +18,7 @@ class AutoScrapingListener
private readonly ScrapeChapterHandler $scrapeChapterHandler,
private readonly ChapterRepositoryInterface $chapterRepository,
private readonly MangaRepositoryInterface $mangaRepository,
private readonly JobRepositoryInterface $jobRepository,
) {
}
@@ -25,7 +29,12 @@ class AutoScrapingListener
$manga = $this->mangaRepository->getById($chapter->mangaId);
if ($manga->isMonitored()) {
$this->scrapeChapterHandler->handle(new ScrapeChapter($event->chapterId->getValue()));
$jobId = Uuid::uuid4()->toString();
$job = new ScrapingJob($jobId);
$job->context['chapterId'] = $event->chapterId->getValue();
$this->jobRepository->save($job);
$this->scrapeChapterHandler->handle(new ScrapeChapter($event->chapterId->getValue(), $jobId));
}
}
}

View File

@@ -135,18 +135,6 @@
".env"
]
},
"symfony/form": {
"version": "8.0",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "7.2",
"ref": "7d86a6723f4a623f59e2bf966b6aad2fc461d36b"
},
"files": [
"config/packages/csrf.yaml"
]
},
"symfony/framework-bundle": {
"version": "7.0",
"recipe": {
@@ -285,20 +273,6 @@
"config/routes/security.yaml"
]
},
"symfony/stimulus-bundle": {
"version": "2.17",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "2.13",
"ref": "6acd9ff4f7fd5626d2962109bd4ebab351d43c43"
},
"files": [
"assets/bootstrap.js",
"assets/controllers.json",
"assets/controllers/hello_controller.js"
]
},
"symfony/twig-bundle": {
"version": "7.0",
"recipe": {
@@ -312,45 +286,6 @@
"templates/base.html.twig"
]
},
"symfony/ux-live-component": {
"version": "2.17",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "2.6",
"ref": "73e69baf18f47740d6f58688c5464b10cdacae06"
},
"files": [
"config/routes/ux_live_component.yaml"
]
},
"symfony/ux-react": {
"version": "2.23",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "2.9",
"ref": "e970076b31d602ae6e2106cf91a82c7e1f7ddff2"
},
"files": [
"assets/react/controllers/Hello.jsx"
]
},
"symfony/ux-turbo": {
"version": "v2.18.0"
},
"symfony/ux-twig-component": {
"version": "2.17",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "2.13",
"ref": "67814b5f9794798b885cec9d3f48631424449a01"
},
"files": [
"config/packages/twig_component.yaml"
]
},
"symfony/validator": {
"version": "7.0",
"recipe": {
@@ -392,9 +327,6 @@
"webpack.config.js"
]
},
"twig/extra-bundle": {
"version": "v3.10.0"
},
"vich/uploader-bundle": {
"version": "2.9",
"recipe": {

View File

@@ -0,0 +1,110 @@
<?php
namespace App\Tests\Domain\Manga\Application\CommandHandler;
use App\Domain\Manga\Application\Command\CheckMonitoredMangas;
use App\Domain\Manga\Application\Command\RefreshMangaChapters;
use App\Domain\Manga\Application\CommandHandler\CheckMonitoredMangasHandler;
use App\Domain\Manga\Domain\Model\Manga;
use App\Domain\Manga\Domain\Model\ValueObject\ExternalId;
use App\Domain\Manga\Domain\Model\ValueObject\MangaId;
use App\Domain\Manga\Domain\Model\ValueObject\MangaSlug;
use App\Domain\Manga\Domain\Model\ValueObject\MangaTitle;
use App\Tests\Domain\Manga\Adapter\InMemoryMangaRepository;
use App\Tests\Shared\Adapter\InMemoryMessageBus;
use PHPUnit\Framework\TestCase;
class CheckMonitoredMangasHandlerTest extends TestCase
{
private InMemoryMangaRepository $mangaRepository;
private InMemoryMessageBus $commandBus;
private CheckMonitoredMangasHandler $handler;
protected function setUp(): void
{
$this->mangaRepository = new InMemoryMangaRepository();
$this->commandBus = new InMemoryMessageBus();
$this->commandBus->clear();
$this->handler = new CheckMonitoredMangasHandler($this->mangaRepository, $this->commandBus);
}
private function createManga(string $id): Manga
{
return new Manga(
new MangaId($id),
new MangaTitle('Manga ' . $id),
new MangaSlug('manga-' . $id),
'Description',
'Author',
2024,
[],
'ongoing',
new ExternalId('ext-' . $id)
);
}
public function testDispatchesRefreshForMonitoredMangaWithOldCheck(): void
{
$manga = $this->createManga('manga-1');
$manga->enableMonitoring();
$manga->updateLastMonitoringCheck(new \DateTimeImmutable('-3 hours'));
$this->mangaRepository->save($manga);
$this->handler->handle(new CheckMonitoredMangas());
$this->assertTrue($this->commandBus->hasMessageOfType(RefreshMangaChapters::class));
$dispatched = array_filter(
$this->commandBus->getDispatchedMessages(),
fn ($m) => $m instanceof RefreshMangaChapters
);
$this->assertCount(1, $dispatched);
$this->assertSame('manga-1', array_values($dispatched)[0]->mangaId->getValue());
}
public function testDoesNotDispatchForNonMonitoredManga(): void
{
$manga = $this->createManga('manga-2');
$this->mangaRepository->save($manga);
$this->handler->handle(new CheckMonitoredMangas());
$this->assertFalse($this->commandBus->hasMessageOfType(RefreshMangaChapters::class));
}
public function testDoesNotDispatchForMangaWithRecentCheck(): void
{
$manga = $this->createManga('manga-3');
$manga->enableMonitoring();
$manga->updateLastMonitoringCheck(new \DateTimeImmutable('-30 minutes'));
$this->mangaRepository->save($manga);
$this->handler->handle(new CheckMonitoredMangas());
$this->assertFalse($this->commandBus->hasMessageOfType(RefreshMangaChapters::class));
}
public function testDispatchesOnlyMangasWithOldCheck(): void
{
$mangaOld = $this->createManga('manga-old');
$mangaOld->enableMonitoring();
$mangaOld->updateLastMonitoringCheck(new \DateTimeImmutable('-3 hours'));
$this->mangaRepository->save($mangaOld);
$mangaRecent = $this->createManga('manga-recent');
$mangaRecent->enableMonitoring();
$mangaRecent->updateLastMonitoringCheck(new \DateTimeImmutable('-30 minutes'));
$this->mangaRepository->save($mangaRecent);
$mangaDisabled = $this->createManga('manga-disabled');
$this->mangaRepository->save($mangaDisabled);
$this->handler->handle(new CheckMonitoredMangas());
$dispatched = array_filter(
$this->commandBus->getDispatchedMessages(),
fn ($m) => $m instanceof RefreshMangaChapters
);
$this->assertCount(1, $dispatched);
$this->assertSame('manga-old', array_values($dispatched)[0]->mangaId->getValue());
}
}

View File

@@ -0,0 +1,104 @@
<?php
namespace App\Tests\Domain\Scraping\Infrastructure\EventListener;
use App\Domain\Manga\Domain\Event\ChapterReadyForScraping;
use App\Domain\Manga\Domain\Model\ValueObject\ChapterId;
use App\Domain\Scraping\Application\CommandHandler\ScrapeChapterHandler;
use App\Domain\Scraping\Domain\Model\Chapter;
use App\Domain\Scraping\Domain\Model\Manga;
use App\Domain\Scraping\Domain\Model\ScrapingJob;
use App\Domain\Scraping\Infrastructure\EventListener\AutoScrapingListener;
use App\Domain\Shared\Domain\Event\ChapterScraped;
use App\Domain\Shared\Domain\Model\JobStatus;
use App\Tests\Domain\Scraping\Adapter\InMemoryChapterRepository;
use App\Tests\Domain\Scraping\Adapter\InMemoryEventBus;
use App\Tests\Domain\Scraping\Adapter\InMemoryImageDownloader;
use App\Tests\Domain\Scraping\Adapter\InMemoryImageStorage;
use App\Tests\Domain\Scraping\Adapter\InMemoryMangaRepository;
use App\Tests\Domain\Scraping\Adapter\InMemoryScraperFactory;
use App\Tests\Domain\Scraping\Adapter\InMemorySourceRepository;
use App\Tests\Domain\Shared\Adapter\InMemoryJobRepository;
use PHPUnit\Framework\TestCase;
class AutoScrapingListenerTest extends TestCase
{
private InMemoryChapterRepository $chapterRepository;
private InMemoryMangaRepository $mangaRepository;
private InMemoryJobRepository $jobRepository;
private InMemoryEventBus $eventBus;
private AutoScrapingListener $listener;
protected function setUp(): void
{
$this->chapterRepository = new InMemoryChapterRepository();
$this->mangaRepository = new InMemoryMangaRepository();
$this->mangaRepository->clear();
$this->jobRepository = new InMemoryJobRepository();
$this->eventBus = new InMemoryEventBus();
$handler = new ScrapeChapterHandler(
new InMemoryScraperFactory(),
new InMemoryImageDownloader(),
new InMemoryImageStorage(),
$this->jobRepository,
$this->chapterRepository,
$this->mangaRepository,
new InMemorySourceRepository(),
$this->eventBus,
);
$this->listener = new AutoScrapingListener(
$handler,
$this->chapterRepository,
$this->mangaRepository,
$this->jobRepository,
);
}
public function testCreatesJobAndScrapesWhenMangaIsMonitored(): void
{
$chapterId = 'chapter-uuid-1';
$mangaId = 'manga-monitored';
$this->chapterRepository->save(new Chapter($chapterId, $mangaId, 1177.0, null));
$this->mangaRepository->save(new Manga(
$mangaId, 'One Piece', 'one-piece', 'Desc', 'Oda', '1997', true
));
$this->listener->onChapterReadyForScraping(
new ChapterReadyForScraping(new ChapterId($chapterId))
);
$jobs = $this->jobRepository->findByType('scraping_job');
$this->assertCount(1, $jobs);
$job = array_values($jobs)[0];
$this->assertSame($chapterId, $job->context['chapterId']);
$this->assertInstanceOf(ScrapingJob::class, $job);
$hasChapterScraped = count(array_filter(
$this->eventBus->getDispatchedMessages(),
fn ($m) => $m instanceof ChapterScraped
)) > 0;
$this->assertTrue($hasChapterScraped);
}
public function testDoesNothingWhenMangaIsNotMonitored(): void
{
$chapterId = 'chapter-uuid-2';
$mangaId = 'manga-not-monitored';
$this->chapterRepository->save(new Chapter($chapterId, $mangaId, 1176.0, null));
$this->mangaRepository->save(new Manga(
$mangaId, 'One Piece', 'one-piece', 'Desc', 'Oda', '1997', false
));
$this->listener->onChapterReadyForScraping(
new ChapterReadyForScraping(new ChapterId($chapterId))
);
$this->assertEmpty($this->jobRepository->findByType('scraping_job'));
$this->assertEmpty($this->eventBus->getDispatchedMessages());
}
}