diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3115834 --- /dev/null +++ b/Makefile @@ -0,0 +1,32 @@ +DOCKER_COMPOSE := $(shell command -v docker-compose 2>/dev/null && echo "docker-compose" || echo "docker compose") +APP = $(DOCKER_COMPOSE) exec -t app + +up: + $(DOCKER_COMPOSE) up -d + +start: + $(DOCKER_COMPOSE) up --build -d + +down: + $(DOCKER_COMPOSE) down + +app-bash: + $(DOCKER_COMPOSE) exec -t app bash + +cc: + $(APP) php bin/console cache:clear + +migrate: + $(APP) php bin/console doctrine:migrations:migrate --no-interaction + +parse: + $(APP) bash -c "php bin/console app:parse-invoices data/invoices_*" + +install: + $(APP) composer install + +update: + $(APP) composer update + +test: + $(APP) php vendor/bin/phpunit tests/Unit diff --git a/README.md b/README.md index 84a9961..9ad1773 100644 --- a/README.md +++ b/README.md @@ -1,64 +1,331 @@ - # Invoice Parser Ce projet est une application Symfony qui permet de parser des fichiers de type JSON et CSV contenant des informations sur des factures. Les données extraites sont ensuite mises à jour dans la base de données via des requêtes SQL. -## Prérequis +--- + +## Stack + +- PHP 8.2 + Symfony 6.4 +- Doctrine ORM 3.x + PostgreSQL 15 +- PHPUnit 9.6 +- Docker (PHP-FPM + PostgreSQL) + +--- + +## Architecture (DDD) + +``` +src/ +├── Application/ +│ └── Command/ParseInvoiceFile/ +│ ├── ParseInvoiceFileCommand.php # Commande CQRS — porte le chemin du fichier +│ ├── ParseInvoiceFileHandler.php # Orchestration : parse → valide → upsert +│ └── ParseInvoiceResult.php # Compteurs inserted / updated / skipped + erreurs +│ +├── Domain/ +│ ├── Entity/ +│ │ └── Invoice.php # Entité Doctrine (name, amount, currency, date) +│ ├── Parser/ +│ │ ├── InvoiceParserInterface.php # supports() + parse() +│ │ └── InvoiceParserRegistryInterface.php # getParser(filePath) +│ └── Repository/ +│ └── InvoiceRepositoryInterface.php # create / update / flush / findByNameAndDate +│ +└── Infrastructure/ + ├── Console/ + │ └── ParseInvoicesCommand.php # Commande CLI app:parse-invoices + ├── Logger/ + │ └── InvoiceLogger.php # var/log/invoice.log + ├── Parser/ + │ ├── InvoiceParserRegistry.php # Itère les parsers, retourne le premier qui supporte le fichier + │ ├── Csv/ + │ │ ├── AbstractCsvInvoiceParser.php # Logique CSV commune (supports + parse) + │ │ ├── Cid1CsvInvoiceParser.php # Client 1 — TSV, sans header + │ │ └── Cid2CsvInvoiceParser.php # Client 2 — CSV virgule, avec header + │ ├── Json/ + │ │ ├── AbstractJsonInvoiceParser.php # Logique JSON commune + │ │ ├── Cid1JsonInvoiceParser.php # Client 1 — champs FR + │ │ └── Cid4JsonInvoiceParser.php # Client 4 — champs mixtes + └── Repository/ + └── InvoiceRepository.php # Implémentation Doctrine +``` + +--- + +## Pattern Registry + +`InvoiceParserRegistry` itère tous les parsers et délègue la sélection à chacun via `supports()`. +Chaque parser sait lui-même s'il prend en charge le fichier, en vérifiant l'extension **et** l'identifiant client dans le nom. + +``` +invoices_20260607100102_cid_1.csv + └─────┘└──┘ + client ext + │ + ▼ + Cid1CsvInvoiceParser::supports() ✔ + Cid2CsvInvoiceParser::supports() ✘ + Cid1JsonInvoiceParser::supports() ✘ + ... +``` + +**Un même client peut avoir plusieurs formats.** La clé de sélection est `extension + cid_x`, pas juste le client. +`cid_1` avec un CSV → `Cid1CsvInvoiceParser`. `cid_1` avec un JSON → `Cid1JsonInvoiceParser`. Les deux parsers coexistent sans conflit : + +``` +invoices_xxx_cid_1.csv → Cid1CsvInvoiceParser::supports() ✔ (csv + cid_1) + Cid1JsonInvoiceParser::supports() ✘ (json ≠ csv) + +invoices_xxx_cid_1.json → Cid1CsvInvoiceParser::supports() ✘ (csv ≠ json) + Cid1JsonInvoiceParser::supports() ✔ (json + cid_1) +``` + +**Format de nom de fichier attendu :** -- Docker -- Docker Compose +``` +invoices_{datetime}_cid_{id}.{ext} +``` + +**Upsert** — clé unique `name + date`. Les lignes existantes sont mises à jour (`amount`, `currency`). Les doublons dans le même fichier sont détectés en mémoire via `$invoiceCache` avant le `flush()`. + +--- + +## Format des fichiers + +Exemples : +``` +data/invoices_20260607120101_cid_1.csv +data/invoices_20260607120201_cid_1.json +data/invoices_20260607120301_cid_4.json +data/invoices_20260607120401_cid_2.csv +``` + +--- + +## Commandes Make + +| Commande | Description | +|----------|-------------| +| `make up` | Démarrer les containers | +| `make start` | Rebuilder et démarrer les containers | +| `make down` | Arrêter les containers | +| `make app-bash` | Ouvrir un shell dans le container | +| `make cc` | Vider le cache Symfony | +| `make migrate` | Appliquer les migrations | +| `make parse` | Parser tous les fichiers `data/invoices_*` | +| `make install` | Installer les dépendances Composer | +| `make update` | Mettre à jour les dépendances Composer | +| `make test` | Lancer les tests unitaires | + +--- ## Installation -1. Clone le projet dans ton répertoire local. +```bash +# 1. Clone le projet dans le répertoire local +git clone https://github.com/InnobyteNo8651/invoice-parser.git +cd invoice-parser + +# 2. Aller sur la branche +git switch refactor/code-quality-improvements + +# 3. Démarrer les containers +make start + +# 4. Installer les dépendances +make install + +# 5. Appliquer toutes les migrations +make migrate +``` + +--- + +## Migrations + +### `Version20250203171333` — Table initiale + +Crée la table `invoice` avec les colonnes de base : + +| Colonne | Type | +|------------|--------------------| +| `id` | INT (séquence) | +| `name` | VARCHAR(255) | +| `amount` | DOUBLE PRECISION | +| `currency` | VARCHAR(255) | + +### `Version20260607091233` — Ajout de `date` + correction `currency` + +- Réduit `currency` de VARCHAR(255) à **VARCHAR(3)** (format ISO 4217 : EUR, USD…) +- Ajoute la colonne **`date DATE NOT NULL`** (mappée en `DateTimeImmutable` via Doctrine) + +> **Note :** si la table contient déjà des données au moment d'appliquer cette migration, +> les lignes existantes reçoivent `CURRENT_DATE` comme valeur par défaut temporaire. +> Il faut vider la table et réimporter les fichiers pour avoir les bonnes dates. + +--- + +## Utilisation + +```bash +# Tous les fichiers du dossier data/ +make parse + +# Un ou plusieurs fichiers spécifiques +make app-bash +php bin/console app:parse-invoices data/invoices_20260607120101_cid_1.csv data/invoices_20260607120201_cid_1.json +``` + +Sortie exemple : +``` +Processing: data/invoices_20260607120101_cid_1.csv + Inserted: 8, Updated: 2, Skipped: 0 + + [OK] Done — inserted: 8, updated: 2, skipped: 0. +``` + +Les erreurs sont affichées en console et loggées dans `var/log/invoice.log`. + +--- + +## Ajouter un nouveau client + +Il suffit de créer **une seule classe**. Symfony la détecte automatiquement via `_instanceof` dans `services.yaml`. + +### Client CSV (ex: cid_5) + +```php +// src/Infrastructure/Parser/Csv/Cid5CsvInvoiceParser.php +final class Cid5CsvInvoiceParser extends AbstractCsvInvoiceParser +{ + protected function supportsClient(string $filePath): bool + { + return str_contains(basename($filePath), 'cid_5'); + } + + public function getSeparator(): string { return ';'; } + public function getColumns(): array { return ['name', 'amount', 'currency', 'date']; } + public function getPositions(): array { return [0, 1, 2, 3]; } + public function hasHeader(): bool { return true; } +} +``` + +### Client JSON (ex: cid_6) + +```php +// src/Infrastructure/Parser/Json/Cid6JsonInvoiceParser.php +final class Cid6JsonInvoiceParser extends AbstractJsonInvoiceParser +{ + protected function supportsClient(string $filePath): bool + { + return str_contains(basename($filePath), 'cid_6'); + } + + public function getFieldMap(): array + { + return ['name' => 'full_name', 'amount' => 'total', 'currency' => 'cur', 'date' => 'invoice_date']; + } +} +``` + +--- + +## Ajouter un nouveau format de fichier + +Pour un format inédit (ex: XML), créer **deux fichiers**. Symfony les détecte automatiquement via `_instanceof` sur `InvoiceParserInterface`. + +### 1 — Classe abstraite + +```php +// src/Infrastructure/Parser/Xml/AbstractXmlInvoiceParser.php +abstract class AbstractXmlInvoiceParser implements InvoiceParserInterface +{ + abstract protected function supportsClient(string $filePath): bool; + abstract public function getFieldMap(): array; + + public function supports(string $filePath): bool + { + return pathinfo($filePath, PATHINFO_EXTENSION) === 'xml' + && $this->supportsClient($filePath); + } - ```bash - git clone https://github.com/ton-repository/invoice-parser.git - cd invoice-parser - ``` + public function parse(string $filePath): array + { + // lire le fichier, instancier SimpleXMLElement, + // itérer les nœuds enfants, retourner Invoice[]|Throwable[] + } +} +``` -2. Construis et lance les containers Docker. +### 2 — Parser concret (ex: cid_7) - ```bash - docker-compose up --build -d - ``` +```php +// src/Infrastructure/Parser/Xml/Cid7XmlInvoiceParser.php +final class Cid7XmlInvoiceParser extends AbstractXmlInvoiceParser +{ + protected function supportsClient(string $filePath): bool + { + return str_contains(basename($filePath), 'cid_7'); + } - Cela créera les containers pour l'application Symfony et la base de données PostgreSQL. + public function getFieldMap(): array + { + return ['name' => 'ClientName', 'amount' => 'Total', 'currency' => 'Currency', 'date' => 'InvoiceDate']; + } +} +``` -3. Installe les dépendances PHP via Composer. +--- - ```bash - docker-compose exec app composer install - ``` +## Tests -4. Créer la base de données et exécute les migrations. +```bash +make test +``` - ```bash - docker-compose exec app php bin/console doctrine:migrations:migrate - ``` +``` +OK (3 tests, 8 assertions) +``` -## Lancer l'application +Seule la couche **Domain** est couverte — c'est la priorité : logique métier pure, sans dépendances externes. -### 1. Exécuter la commande de parsing +| Suite | Fichier | Ce qui est testé | +|-------|---------|-----------------| +| `InvoiceTest` | `tests/Unit/Domain/Entity/InvoiceTest.php` | Constructeur, getters, `updateAmountAndCurrency` | -Pour parser les fichiers de factures (`json` ou `csv`), exécute la commande suivante : +--- - ```bash - docker-compose run --rm app php bin/console app:parse - ``` +## Améliorations possibles -Cette commande va charger et parser les fichiers, puis mettre à jour les enregistrements dans la base de données. +### Fonctionnel +- [ ] **Type monétaire** — remplacer `float $amount` par un Value Object `Money` (stockage en centimes `int`) pour éviter les erreurs d'arrondi des flottants +- [ ] **Unicité** — ajouter une contrainte d'unicité en base sur `name + date` pour garantir l'intégrité +- [ ] **Format date** — détecter automatiquement le format de date (FR/EN) et insérer correctement dans la colonne date -### 2. Lancer les tests unitaires +### Fichiers volumineux +- [ ] **Streaming** — lire les fichiers ligne par ligne au lieu de tout charger en mémoire (`SplFileObject` pour CSV, JSONL pour JSON) +- [ ] **Batch flush** — appeler `flush()` + `clear()` toutes les N lignes pour libérer la mémoire Doctrine +- [ ] **Traitement async** — intégrer Symfony Messenger pour traiter les fichiers en arrière-plan -Pour vérifier le bon fonctionnement de l'application, tu peux exécuter les tests unitaires via PHPUnit. Pour cela, utilise la commande suivante : +### Tests +- [ ] **Tests unitaires** — `ParseInvoiceResultTest`, `ParseInvoiceFileHandlerTest`, parsers CSV/JSON +- [ ] **Tests d'intégration** — upsert réel sur base de données, contrainte d'unicité +- [ ] **Tests fonctionnels** — commande CLI end-to-end - ```bash - docker-compose exec app php vendor/bin/phpunit tests/InvoiceParserTest.php - ``` +### Infrastructure +- [ ] **Rotation des logs** — limiter la taille du fichier `invoice.log` +- [ ] **Chemin du log configurable** — extraire en paramètre Symfony +- [ ] **Barre de progression** — afficher une `ProgressBar` Symfony pour les gros fichiers +- [ ] **Rapport d'erreurs** — exporter les lignes en erreur dans un fichier CSV séparé -Pour exécuter tous les tests dans le projet, tu peux utiliser : +### Outillage & CI/CD +- [ ] **Pipeline CI/CD** — GitHub Actions +- [ ] **CS Fixer** — PHP CS Fixer pour enforcer le style de code +- [ ] **Variables d'environnement** — séparer les configs `local` / `dev` / `preprod` / `prod` +- [ ] **Planification** — cron job ou Symfony Scheduler pour automatiser le parsing - ```bash - docker-compose exec app php vendor/bin/phpunit - ``` +### Sécurité +- [ ] **Chiffrement des données sensibles** — chiffrer les champs sensibles en base (`name`…) via DoctrineEncryptBundle ou solution custom +- [ ] **Secrets** — ne jamais committer les credentials (`DATABASE_URL`…) +- [ ] **Permissions fichiers** — restreindre l'accès aux fichiers à parser (dossier dédié, utilisateur dédié) diff --git a/composer.json b/composer.json index cdbf320..f2fe6be 100644 --- a/composer.json +++ b/composer.json @@ -17,6 +17,7 @@ "symfony/flex": "^2", "symfony/framework-bundle": "6.4.*", "symfony/runtime": "6.4.*", + "symfony/validator": "6.4.*", "symfony/yaml": "6.4.*" }, "config": { diff --git a/composer.lock b/composer.lock index 85fb085..0506917 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "e0cd7b1cf6a4a219bcb7f9ea6f987d0b", + "content-hash": "477db8e9d98c679e4f6aeb5bb07b536a", "packages": [ { "name": "doctrine/annotations", @@ -80,126 +80,34 @@ "issues": "https://github.com/doctrine/annotations/issues", "source": "https://github.com/doctrine/annotations/tree/2.0.2" }, + "abandoned": true, "time": "2024-09-05T10:17:24+00:00" }, - { - "name": "doctrine/cache", - "version": "2.2.0", - "source": { - "type": "git", - "url": "https://github.com/doctrine/cache.git", - "reference": "1ca8f21980e770095a31456042471a57bc4c68fb" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/cache/zipball/1ca8f21980e770095a31456042471a57bc4c68fb", - "reference": "1ca8f21980e770095a31456042471a57bc4c68fb", - "shasum": "" - }, - "require": { - "php": "~7.1 || ^8.0" - }, - "conflict": { - "doctrine/common": ">2.2,<2.4" - }, - "require-dev": { - "cache/integration-tests": "dev-master", - "doctrine/coding-standard": "^9", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "psr/cache": "^1.0 || ^2.0 || ^3.0", - "symfony/cache": "^4.4 || ^5.4 || ^6", - "symfony/var-exporter": "^4.4 || ^5.4 || ^6" - }, - "type": "library", - "autoload": { - "psr-4": { - "Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de" - }, - { - "name": "Jonathan Wage", - "email": "jonwage@gmail.com" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com" - } - ], - "description": "PHP Doctrine Cache library is a popular cache implementation that supports many different drivers such as redis, memcache, apc, mongodb and others.", - "homepage": "https://www.doctrine-project.org/projects/cache.html", - "keywords": [ - "abstraction", - "apcu", - "cache", - "caching", - "couchdb", - "memcached", - "php", - "redis", - "xcache" - ], - "support": { - "issues": "https://github.com/doctrine/cache/issues", - "source": "https://github.com/doctrine/cache/tree/2.2.0" - }, - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcache", - "type": "tidelift" - } - ], - "time": "2022-05-20T20:07:39+00:00" - }, { "name": "doctrine/collections", - "version": "2.2.2", + "version": "2.6.0", "source": { "type": "git", "url": "https://github.com/doctrine/collections.git", - "reference": "d8af7f248c74f195f7347424600fd9e17b57af59" + "reference": "7713da39d8e237f28411d6a616a3dce5e20d5de2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/collections/zipball/d8af7f248c74f195f7347424600fd9e17b57af59", - "reference": "d8af7f248c74f195f7347424600fd9e17b57af59", + "url": "https://api.github.com/repos/doctrine/collections/zipball/7713da39d8e237f28411d6a616a3dce5e20d5de2", + "reference": "7713da39d8e237f28411d6a616a3dce5e20d5de2", "shasum": "" }, "require": { "doctrine/deprecations": "^1", - "php": "^8.1" + "php": "^8.1", + "symfony/polyfill-php84": "^1.30" }, "require-dev": { - "doctrine/coding-standard": "^12", + "doctrine/coding-standard": "^14", "ext-json": "*", - "phpstan/phpstan": "^1.8", - "phpstan/phpstan-phpunit": "^1.0", - "phpunit/phpunit": "^10.5", - "vimeo/psalm": "^5.11" + "phpstan/phpstan": "^2.1.30", + "phpstan/phpstan-phpunit": "^2.0.7", + "phpunit/phpunit": "^10.5.58 || ^11.5.42 || ^12.4" }, "type": "library", "autoload": { @@ -243,7 +151,7 @@ ], "support": { "issues": "https://github.com/doctrine/collections/issues", - "source": "https://github.com/doctrine/collections/tree/2.2.2" + "source": "https://github.com/doctrine/collections/tree/2.6.0" }, "funding": [ { @@ -259,42 +167,45 @@ "type": "tidelift" } ], - "time": "2024-04-18T06:56:21+00:00" + "time": "2026-01-15T10:01:58+00:00" }, { "name": "doctrine/dbal", - "version": "3.9.4", + "version": "3.10.5", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "ec16c82f20be1a7224e65ac67144a29199f87959" + "reference": "95d84866bf3c04b2ddca1df7c049714660959aef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/ec16c82f20be1a7224e65ac67144a29199f87959", - "reference": "ec16c82f20be1a7224e65ac67144a29199f87959", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/95d84866bf3c04b2ddca1df7c049714660959aef", + "reference": "95d84866bf3c04b2ddca1df7c049714660959aef", "shasum": "" }, "require": { "composer-runtime-api": "^2", - "doctrine/cache": "^1.11|^2.0", "doctrine/deprecations": "^0.5.3|^1", "doctrine/event-manager": "^1|^2", "php": "^7.4 || ^8.0", "psr/cache": "^1|^2|^3", "psr/log": "^1|^2|^3" }, + "conflict": { + "doctrine/cache": "< 1.11" + }, "require-dev": { - "doctrine/coding-standard": "12.0.0", + "doctrine/cache": "^1.11|^2.0", + "doctrine/coding-standard": "14.0.0", "fig/log-test": "^1", "jetbrains/phpstorm-stubs": "2023.1", - "phpstan/phpstan": "2.1.1", + "phpstan/phpstan": "2.1.30", "phpstan/phpstan-strict-rules": "^2", - "phpunit/phpunit": "9.6.22", - "slevomat/coding-standard": "8.13.1", - "squizlabs/php_codesniffer": "3.10.2", - "symfony/cache": "^5.4|^6.0|^7.0", - "symfony/console": "^4.4|^5.4|^6.0|^7.0" + "phpunit/phpunit": "9.6.34", + "slevomat/coding-standard": "8.27.1", + "squizlabs/php_codesniffer": "4.0.1", + "symfony/cache": "^5.4|^6.0|^7.0|^8.0", + "symfony/console": "^4.4|^5.4|^6.0|^7.0|^8.0" }, "suggest": { "symfony/console": "For helpful console commands such as SQL execution and import of files." @@ -354,7 +265,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/3.9.4" + "source": "https://github.com/doctrine/dbal/tree/3.10.5" }, "funding": [ { @@ -370,30 +281,33 @@ "type": "tidelift" } ], - "time": "2025-01-16T08:28:55+00:00" + "time": "2026-02-24T08:03:57+00:00" }, { "name": "doctrine/deprecations", - "version": "1.1.4", + "version": "1.1.6", "source": { "type": "git", "url": "https://github.com/doctrine/deprecations.git", - "reference": "31610dbb31faa98e6b5447b62340826f54fbc4e9" + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/deprecations/zipball/31610dbb31faa98e6b5447b62340826f54fbc4e9", - "reference": "31610dbb31faa98e6b5447b62340826f54fbc4e9", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", "shasum": "" }, "require": { "php": "^7.1 || ^8.0" }, + "conflict": { + "phpunit/phpunit": "<=7.5 || >=14" + }, "require-dev": { - "doctrine/coding-standard": "^9 || ^12", - "phpstan/phpstan": "1.4.10 || 2.0.3", + "doctrine/coding-standard": "^9 || ^12 || ^14", + "phpstan/phpstan": "1.4.10 || 2.1.30", "phpstan/phpstan-phpunit": "^1.0 || ^2", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0", "psr/log": "^1 || ^2 || ^3" }, "suggest": { @@ -413,68 +327,69 @@ "homepage": "https://www.doctrine-project.org/", "support": { "issues": "https://github.com/doctrine/deprecations/issues", - "source": "https://github.com/doctrine/deprecations/tree/1.1.4" + "source": "https://github.com/doctrine/deprecations/tree/1.1.6" }, - "time": "2024-12-07T21:18:45+00:00" + "time": "2026-02-07T07:09:04+00:00" }, { "name": "doctrine/doctrine-bundle", - "version": "2.13.2", + "version": "2.18.2", "source": { "type": "git", "url": "https://github.com/doctrine/DoctrineBundle.git", - "reference": "2363c43d9815a11657e452625cd64172d5587486" + "reference": "0ff098b29b8b3c68307c8987dcaed7fd829c6546" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/2363c43d9815a11657e452625cd64172d5587486", - "reference": "2363c43d9815a11657e452625cd64172d5587486", + "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/0ff098b29b8b3c68307c8987dcaed7fd829c6546", + "reference": "0ff098b29b8b3c68307c8987dcaed7fd829c6546", "shasum": "" }, "require": { - "doctrine/cache": "^1.11 || ^2.0", "doctrine/dbal": "^3.7.0 || ^4.0", - "doctrine/persistence": "^2.2 || ^3", + "doctrine/deprecations": "^1.0", + "doctrine/persistence": "^3.1 || ^4", "doctrine/sql-formatter": "^1.0.1", - "php": "^7.4 || ^8.0", - "symfony/cache": "^5.4 || ^6.0 || ^7.0", - "symfony/config": "^5.4 || ^6.0 || ^7.0", - "symfony/console": "^5.4 || ^6.0 || ^7.0", - "symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0", - "symfony/deprecation-contracts": "^2.1 || ^3", - "symfony/doctrine-bridge": "^5.4.46 || ~6.3.12 || ^6.4.3 || ^7.0.3", - "symfony/framework-bundle": "^5.4 || ^6.0 || ^7.0", - "symfony/polyfill-php80": "^1.15", - "symfony/service-contracts": "^1.1.1 || ^2.0 || ^3" + "php": "^8.1", + "symfony/cache": "^6.4 || ^7.0", + "symfony/config": "^6.4 || ^7.0", + "symfony/console": "^6.4 || ^7.0", + "symfony/dependency-injection": "^6.4 || ^7.0", + "symfony/doctrine-bridge": "^6.4.3 || ^7.0.3", + "symfony/framework-bundle": "^6.4 || ^7.0", + "symfony/service-contracts": "^2.5 || ^3" }, "conflict": { "doctrine/annotations": ">=3.0", + "doctrine/cache": "< 1.11", "doctrine/orm": "<2.17 || >=4.0", - "twig/twig": "<1.34 || >=2.0 <2.4" + "symfony/var-exporter": "< 6.4.1 || 7.0.0", + "twig/twig": "<2.13 || >=3.0 <3.0.4" }, "require-dev": { "doctrine/annotations": "^1 || ^2", - "doctrine/coding-standard": "^12", - "doctrine/deprecations": "^1.0", - "doctrine/orm": "^2.17 || ^3.0", + "doctrine/cache": "^1.11 || ^2.0", + "doctrine/coding-standard": "^14", + "doctrine/orm": "^2.17 || ^3.1", "friendsofphp/proxy-manager-lts": "^1.0", "phpstan/phpstan": "2.1.1", "phpstan/phpstan-phpunit": "2.0.3", "phpstan/phpstan-strict-rules": "^2", - "phpunit/phpunit": "^9.5.26", + "phpunit/phpunit": "^10.5.53 || ^12.3.10", "psr/log": "^1.1.4 || ^2.0 || ^3.0", - "symfony/phpunit-bridge": "^6.1 || ^7.0", - "symfony/property-info": "^5.4 || ^6.0 || ^7.0", - "symfony/proxy-manager-bridge": "^5.4 || ^6.0", - "symfony/security-bundle": "^5.4 || ^6.0 || ^7.0", - "symfony/stopwatch": "^5.4 || ^6.0 || ^7.0", - "symfony/string": "^5.4 || ^6.0 || ^7.0", - "symfony/twig-bridge": "^5.4 || ^6.0 || ^7.0", - "symfony/validator": "^5.4 || ^6.0 || ^7.0", - "symfony/var-exporter": "^5.4 || ^6.2 || ^7.0", - "symfony/web-profiler-bundle": "^5.4 || ^6.0 || ^7.0", - "symfony/yaml": "^5.4 || ^6.0 || ^7.0", - "twig/twig": "^1.34 || ^2.12 || ^3.0" + "symfony/doctrine-messenger": "^6.4 || ^7.0", + "symfony/expression-language": "^6.4 || ^7.0", + "symfony/messenger": "^6.4 || ^7.0", + "symfony/property-info": "^6.4 || ^7.0", + "symfony/security-bundle": "^6.4 || ^7.0", + "symfony/stopwatch": "^6.4 || ^7.0", + "symfony/string": "^6.4 || ^7.0", + "symfony/twig-bridge": "^6.4 || ^7.0", + "symfony/validator": "^6.4 || ^7.0", + "symfony/var-exporter": "^6.4.1 || ^7.0.1", + "symfony/web-profiler-bundle": "^6.4 || ^7.0", + "symfony/yaml": "^6.4 || ^7.0", + "twig/twig": "^2.14.7 || ^3.0.4" }, "suggest": { "doctrine/orm": "The Doctrine ORM integration is optional in the bundle.", @@ -519,7 +434,7 @@ ], "support": { "issues": "https://github.com/doctrine/DoctrineBundle/issues", - "source": "https://github.com/doctrine/DoctrineBundle/tree/2.13.2" + "source": "https://github.com/doctrine/DoctrineBundle/tree/2.18.2" }, "funding": [ { @@ -535,42 +450,41 @@ "type": "tidelift" } ], - "time": "2025-01-15T11:12:38+00:00" + "time": "2025-12-20T21:35:32+00:00" }, { "name": "doctrine/doctrine-migrations-bundle", - "version": "3.4.1", + "version": "3.7.0", "source": { "type": "git", "url": "https://github.com/doctrine/DoctrineMigrationsBundle.git", - "reference": "e858ce0f5c12b266dce7dce24834448355155da7" + "reference": "1e380c6dd8ac8488217f39cff6b77e367f1a644b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/DoctrineMigrationsBundle/zipball/e858ce0f5c12b266dce7dce24834448355155da7", - "reference": "e858ce0f5c12b266dce7dce24834448355155da7", + "url": "https://api.github.com/repos/doctrine/DoctrineMigrationsBundle/zipball/1e380c6dd8ac8488217f39cff6b77e367f1a644b", + "reference": "1e380c6dd8ac8488217f39cff6b77e367f1a644b", "shasum": "" }, "require": { - "doctrine/doctrine-bundle": "^2.4", + "doctrine/doctrine-bundle": "^2.4 || ^3.0", "doctrine/migrations": "^3.2", "php": "^7.2 || ^8.0", "symfony/deprecation-contracts": "^2.1 || ^3", - "symfony/framework-bundle": "^5.4 || ^6.0 || ^7.0" + "symfony/framework-bundle": "^5.4 || ^6.0 || ^7.0 || ^8.0" }, "require-dev": { "composer/semver": "^3.0", - "doctrine/coding-standard": "^12", + "doctrine/coding-standard": "^12 || ^14", "doctrine/orm": "^2.6 || ^3", - "doctrine/persistence": "^2.0 || ^3", "phpstan/phpstan": "^1.4 || ^2", "phpstan/phpstan-deprecation-rules": "^1 || ^2", "phpstan/phpstan-phpunit": "^1 || ^2", "phpstan/phpstan-strict-rules": "^1.1 || ^2", "phpstan/phpstan-symfony": "^1.3 || ^2", "phpunit/phpunit": "^8.5 || ^9.5", - "symfony/phpunit-bridge": "^6.3 || ^7", - "symfony/var-exporter": "^5.4 || ^6 || ^7" + "symfony/phpunit-bridge": "^6.3 || ^7 || ^8", + "symfony/var-exporter": "^5.4 || ^6 || ^7 || ^8" }, "type": "symfony-bundle", "autoload": { @@ -605,7 +519,7 @@ ], "support": { "issues": "https://github.com/doctrine/DoctrineMigrationsBundle/issues", - "source": "https://github.com/doctrine/DoctrineMigrationsBundle/tree/3.4.1" + "source": "https://github.com/doctrine/DoctrineMigrationsBundle/tree/3.7.0" }, "funding": [ { @@ -621,20 +535,20 @@ "type": "tidelift" } ], - "time": "2025-01-27T22:48:22+00:00" + "time": "2025-11-15T19:02:59+00:00" }, { "name": "doctrine/event-manager", - "version": "2.0.1", + "version": "2.1.1", "source": { "type": "git", "url": "https://github.com/doctrine/event-manager.git", - "reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e" + "reference": "dda33921b198841ca8dbad2eaa5d4d34769d18cf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/event-manager/zipball/b680156fa328f1dfd874fd48c7026c41570b9c6e", - "reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e", + "url": "https://api.github.com/repos/doctrine/event-manager/zipball/dda33921b198841ca8dbad2eaa5d4d34769d18cf", + "reference": "dda33921b198841ca8dbad2eaa5d4d34769d18cf", "shasum": "" }, "require": { @@ -644,10 +558,10 @@ "doctrine/common": "<2.9" }, "require-dev": { - "doctrine/coding-standard": "^12", - "phpstan/phpstan": "^1.8.8", - "phpunit/phpunit": "^10.5", - "vimeo/psalm": "^5.24" + "doctrine/coding-standard": "^14", + "phpdocumentor/guides-cli": "^1.4", + "phpstan/phpstan": "^2.1.32", + "phpunit/phpunit": "^10.5.58" }, "type": "library", "autoload": { @@ -696,7 +610,7 @@ ], "support": { "issues": "https://github.com/doctrine/event-manager/issues", - "source": "https://github.com/doctrine/event-manager/tree/2.0.1" + "source": "https://github.com/doctrine/event-manager/tree/2.1.1" }, "funding": [ { @@ -712,37 +626,36 @@ "type": "tidelift" } ], - "time": "2024-05-22T20:47:39+00:00" + "time": "2026-01-29T07:11:08+00:00" }, { "name": "doctrine/inflector", - "version": "2.0.10", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/doctrine/inflector.git", - "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc" + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/inflector/zipball/5817d0659c5b50c9b950feb9af7b9668e2c436bc", - "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b", "shasum": "" }, "require": { "php": "^7.2 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^11.0", - "phpstan/phpstan": "^1.8", - "phpstan/phpstan-phpunit": "^1.1", - "phpstan/phpstan-strict-rules": "^1.3", - "phpunit/phpunit": "^8.5 || ^9.5", - "vimeo/psalm": "^4.25 || ^5.4" + "doctrine/coding-standard": "^12.0 || ^13.0", + "phpstan/phpstan": "^1.12 || ^2.0", + "phpstan/phpstan-phpunit": "^1.4 || ^2.0", + "phpstan/phpstan-strict-rules": "^1.6 || ^2.0", + "phpunit/phpunit": "^8.5 || ^12.2" }, "type": "library", "autoload": { "psr-4": { - "Doctrine\\Inflector\\": "lib/Doctrine/Inflector" + "Doctrine\\Inflector\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -787,7 +700,7 @@ ], "support": { "issues": "https://github.com/doctrine/inflector/issues", - "source": "https://github.com/doctrine/inflector/tree/2.0.10" + "source": "https://github.com/doctrine/inflector/tree/2.1.0" }, "funding": [ { @@ -803,7 +716,7 @@ "type": "tidelift" } ], - "time": "2024-02-18T20:23:39+00:00" + "time": "2025-08-10T19:31:58+00:00" }, { "name": "doctrine/instantiator", @@ -954,16 +867,16 @@ }, { "name": "doctrine/migrations", - "version": "3.8.2", + "version": "3.9.7", "source": { "type": "git", "url": "https://github.com/doctrine/migrations.git", - "reference": "5007eb1168691225ac305fe16856755c20860842" + "reference": "96cb2a89b56c9efb0bac38e606dc0b0f13e650ec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/migrations/zipball/5007eb1168691225ac305fe16856755c20860842", - "reference": "5007eb1168691225ac305fe16856755c20860842", + "url": "https://api.github.com/repos/doctrine/migrations/zipball/96cb2a89b56c9efb0bac38e606dc0b0f13e650ec", + "reference": "96cb2a89b56c9efb0bac38e606dc0b0f13e650ec", "shasum": "" }, "require": { @@ -973,29 +886,29 @@ "doctrine/event-manager": "^1.2 || ^2.0", "php": "^8.1", "psr/log": "^1.1.3 || ^2 || ^3", - "symfony/console": "^5.4 || ^6.0 || ^7.0", - "symfony/stopwatch": "^5.4 || ^6.0 || ^7.0", - "symfony/var-exporter": "^6.2 || ^7.0" + "symfony/console": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/stopwatch": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/var-exporter": "^6.2 || ^7.0 || ^8.0" }, "conflict": { "doctrine/orm": "<2.12 || >=4" }, "require-dev": { - "doctrine/coding-standard": "^12", + "doctrine/coding-standard": "^14", "doctrine/orm": "^2.13 || ^3", - "doctrine/persistence": "^2 || ^3", + "doctrine/persistence": "^2 || ^3 || ^4", "doctrine/sql-formatter": "^1.0", "ext-pdo_sqlite": "*", "fig/log-test": "^1", - "phpstan/phpstan": "^1.10", - "phpstan/phpstan-deprecation-rules": "^1.1", - "phpstan/phpstan-phpunit": "^1.3", - "phpstan/phpstan-strict-rules": "^1.4", - "phpstan/phpstan-symfony": "^1.3", - "phpunit/phpunit": "^10.3", - "symfony/cache": "^5.4 || ^6.0 || ^7.0", - "symfony/process": "^5.4 || ^6.0 || ^7.0", - "symfony/yaml": "^5.4 || ^6.0 || ^7.0" + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-phpunit": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpstan/phpstan-symfony": "^2", + "phpunit/phpunit": "^10.3 || ^11.0 || ^12.0", + "symfony/cache": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/process": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/yaml": "^5.4 || ^6.0 || ^7.0 || ^8.0" }, "suggest": { "doctrine/sql-formatter": "Allows to generate formatted SQL with the diff command.", @@ -1037,7 +950,7 @@ ], "support": { "issues": "https://github.com/doctrine/migrations/issues", - "source": "https://github.com/doctrine/migrations/tree/3.8.2" + "source": "https://github.com/doctrine/migrations/tree/3.9.7" }, "funding": [ { @@ -1053,20 +966,20 @@ "type": "tidelift" } ], - "time": "2024-10-10T21:35:27+00:00" + "time": "2026-04-23T19:33:20+00:00" }, { "name": "doctrine/orm", - "version": "3.3.1", + "version": "3.6.7", "source": { "type": "git", "url": "https://github.com/doctrine/orm.git", - "reference": "b1f8253105aa5382c495e5f9f8ef34e297775428" + "reference": "bc217c0e19c3a9eadfa67697143b87c9ba01272c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/orm/zipball/b1f8253105aa5382c495e5f9f8ef34e297775428", - "reference": "b1f8253105aa5382c495e5f9f8ef34e297775428", + "url": "https://api.github.com/repos/doctrine/orm/zipball/bc217c0e19c3a9eadfa67697143b87c9ba01272c", + "reference": "bc217c0e19c3a9eadfa67697143b87c9ba01272c", "shasum": "" }, "require": { @@ -1082,20 +995,18 @@ "ext-ctype": "*", "php": "^8.1", "psr/cache": "^1 || ^2 || ^3", - "symfony/console": "^5.4 || ^6.0 || ^7.0", - "symfony/var-exporter": "^6.3.9 || ^7.0" + "symfony/console": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/var-exporter": "^6.3.9 || ^7.0 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^12.0", + "doctrine/coding-standard": "^14.0", "phpbench/phpbench": "^1.0", - "phpdocumentor/guides-cli": "^1.4", "phpstan/extension-installer": "^1.4", - "phpstan/phpstan": "2.0.3", + "phpstan/phpstan": "2.1.23", "phpstan/phpstan-deprecation-rules": "^2", - "phpunit/phpunit": "^10.4.0", + "phpunit/phpunit": "^10.5.0 || ^11.5", "psr/log": "^1 || ^2 || ^3", - "squizlabs/php_codesniffer": "3.7.2", - "symfony/cache": "^5.4 || ^6.2 || ^7.0" + "symfony/cache": "^5.4 || ^6.2 || ^7.0 || ^8.0" }, "suggest": { "ext-dom": "Provides support for XSD validation for XML mapping files", @@ -1141,45 +1052,43 @@ ], "support": { "issues": "https://github.com/doctrine/orm/issues", - "source": "https://github.com/doctrine/orm/tree/3.3.1" + "source": "https://github.com/doctrine/orm/tree/3.6.7" }, - "time": "2024-12-19T07:08:14+00:00" + "time": "2026-05-25T16:45:47+00:00" }, { "name": "doctrine/persistence", - "version": "3.4.0", + "version": "4.2.0", "source": { "type": "git", "url": "https://github.com/doctrine/persistence.git", - "reference": "0ea965320cec355dba75031c1b23d4c78362e3ff" + "reference": "49ab73e0d3e2ac8d1f5ecda3dd8acd5503781e8b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/persistence/zipball/0ea965320cec355dba75031c1b23d4c78362e3ff", - "reference": "0ea965320cec355dba75031c1b23d4c78362e3ff", + "url": "https://api.github.com/repos/doctrine/persistence/zipball/49ab73e0d3e2ac8d1f5ecda3dd8acd5503781e8b", + "reference": "49ab73e0d3e2ac8d1f5ecda3dd8acd5503781e8b", "shasum": "" }, "require": { + "doctrine/deprecations": "^1", "doctrine/event-manager": "^1 || ^2", - "php": "^7.2 || ^8.0", + "php": "^8.1", "psr/cache": "^1.0 || ^2.0 || ^3.0" }, - "conflict": { - "doctrine/common": "<2.10" - }, "require-dev": { - "doctrine/coding-standard": "^12", - "doctrine/common": "^3.0", - "phpstan/phpstan": "1.12.7", - "phpstan/phpstan-phpunit": "^1", - "phpstan/phpstan-strict-rules": "^1.1", - "phpunit/phpunit": "^8.5.38 || ^9.5", - "symfony/cache": "^4.4 || ^5.4 || ^6.0 || ^7.0" + "doctrine/coding-standard": "^14", + "phpstan/phpstan": "2.1.30", + "phpstan/phpstan-phpunit": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^10.5.58 || ^12", + "symfony/cache": "^4.4 || ^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/finder": "^4.4 || ^5.4 || ^6.0 || ^7.0 || ^8.0" }, "type": "library", "autoload": { "psr-4": { - "Doctrine\\Persistence\\": "src/Persistence" + "Doctrine\\Persistence\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1223,7 +1132,7 @@ ], "support": { "issues": "https://github.com/doctrine/persistence/issues", - "source": "https://github.com/doctrine/persistence/tree/3.4.0" + "source": "https://github.com/doctrine/persistence/tree/4.2.0" }, "funding": [ { @@ -1239,30 +1148,30 @@ "type": "tidelift" } ], - "time": "2024-10-30T19:48:12+00:00" + "time": "2026-04-26T12:12:52+00:00" }, { "name": "doctrine/sql-formatter", - "version": "1.5.2", + "version": "1.5.4", "source": { "type": "git", "url": "https://github.com/doctrine/sql-formatter.git", - "reference": "d6d00aba6fd2957fe5216fe2b7673e9985db20c8" + "reference": "9563949f5cd3bd12a17d12fb980528bc141c5806" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/sql-formatter/zipball/d6d00aba6fd2957fe5216fe2b7673e9985db20c8", - "reference": "d6d00aba6fd2957fe5216fe2b7673e9985db20c8", + "url": "https://api.github.com/repos/doctrine/sql-formatter/zipball/9563949f5cd3bd12a17d12fb980528bc141c5806", + "reference": "9563949f5cd3bd12a17d12fb980528bc141c5806", "shasum": "" }, "require": { "php": "^8.1" }, "require-dev": { - "doctrine/coding-standard": "^12", - "ergebnis/phpunit-slow-test-detector": "^2.14", - "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^10.5" + "doctrine/coding-standard": "^14", + "ergebnis/phpunit-slow-test-detector": "^2.20", + "phpstan/phpstan": "^2.1.31", + "phpunit/phpunit": "^10.5.58" }, "bin": [ "bin/sql-formatter" @@ -1292,9 +1201,9 @@ ], "support": { "issues": "https://github.com/doctrine/sql-formatter/issues", - "source": "https://github.com/doctrine/sql-formatter/tree/1.5.2" + "source": "https://github.com/doctrine/sql-formatter/tree/1.5.4" }, - "time": "2025-01-24T11:45:48+00:00" + "time": "2026-02-08T16:21:46+00:00" }, { "name": "psr/cache", @@ -1500,16 +1409,16 @@ }, { "name": "symfony/cache", - "version": "v6.4.18", + "version": "v6.4.41", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "b209751ed25f735ea90ca4c9c969d9413a17dfee" + "reference": "5490a577195422c3c9cda09c64823580858af854" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/b209751ed25f735ea90ca4c9c969d9413a17dfee", - "reference": "b209751ed25f735ea90ca4c9c969d9413a17dfee", + "url": "https://api.github.com/repos/symfony/cache/zipball/5490a577195422c3c9cda09c64823580858af854", + "reference": "5490a577195422c3c9cda09c64823580858af854", "shasum": "" }, "require": { @@ -1576,7 +1485,7 @@ "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/v6.4.18" + "source": "https://github.com/symfony/cache/tree/v6.4.41" }, "funding": [ { @@ -1587,25 +1496,29 @@ "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-01-22T14:13:52+00:00" + "time": "2026-05-24T08:42:40+00:00" }, { "name": "symfony/cache-contracts", - "version": "v3.5.1", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/cache-contracts.git", - "reference": "15a4f8e5cd3bce9aeafc882b1acab39ec8de2c1b" + "reference": "225e8a254166bd3442e370c6f50145465db63831" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/15a4f8e5cd3bce9aeafc882b1acab39ec8de2c1b", - "reference": "15a4f8e5cd3bce9aeafc882b1acab39ec8de2c1b", + "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/225e8a254166bd3442e370c6f50145465db63831", + "reference": "225e8a254166bd3442e370c6f50145465db63831", "shasum": "" }, "require": { @@ -1619,7 +1532,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -1652,7 +1565,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/cache-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/cache-contracts/tree/v3.7.0" }, "funding": [ { @@ -1663,25 +1576,29 @@ "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": "2024-09-25T14:20:29+00:00" + "time": "2026-05-05T15:33:14+00:00" }, { "name": "symfony/config", - "version": "v6.4.14", + "version": "v6.4.37", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "4e55e7e4ffddd343671ea972216d4509f46c22ef" + "reference": "ee615e8352db9c5f0b7b149154a3f654dc72042b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/4e55e7e4ffddd343671ea972216d4509f46c22ef", - "reference": "4e55e7e4ffddd343671ea972216d4509f46c22ef", + "url": "https://api.github.com/repos/symfony/config/zipball/ee615e8352db9c5f0b7b149154a3f654dc72042b", + "reference": "ee615e8352db9c5f0b7b149154a3f654dc72042b", "shasum": "" }, "require": { @@ -1727,7 +1644,7 @@ "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/config/tree/v6.4.14" + "source": "https://github.com/symfony/config/tree/v6.4.37" }, "funding": [ { @@ -1738,25 +1655,29 @@ "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": "2024-11-04T11:33:53+00:00" + "time": "2026-04-29T10:19:30+00:00" }, { "name": "symfony/console", - "version": "v6.4.17", + "version": "v6.4.41", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "799445db3f15768ecc382ac5699e6da0520a0a04" + "reference": "d21b17ed158e79180fac3895ff751707970eeb57" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/799445db3f15768ecc382ac5699e6da0520a0a04", - "reference": "799445db3f15768ecc382ac5699e6da0520a0a04", + "url": "https://api.github.com/repos/symfony/console/zipball/d21b17ed158e79180fac3895ff751707970eeb57", + "reference": "d21b17ed158e79180fac3895ff751707970eeb57", "shasum": "" }, "require": { @@ -1821,7 +1742,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.4.17" + "source": "https://github.com/symfony/console/tree/v6.4.41" }, "funding": [ { @@ -1832,25 +1753,29 @@ "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": "2024-12-07T12:07:30+00:00" + "time": "2026-05-24T08:48:41+00:00" }, { "name": "symfony/dependency-injection", - "version": "v6.4.16", + "version": "v6.4.38", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "7a379d8871f6a36f01559c14e11141cc02eb8dc8" + "reference": "f0990df92ee67721886a2a8b6e19a1bafbf3d7a4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/7a379d8871f6a36f01559c14e11141cc02eb8dc8", - "reference": "7a379d8871f6a36f01559c14e11141cc02eb8dc8", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/f0990df92ee67721886a2a8b6e19a1bafbf3d7a4", + "reference": "f0990df92ee67721886a2a8b6e19a1bafbf3d7a4", "shasum": "" }, "require": { @@ -1858,7 +1783,7 @@ "psr/container": "^1.1|^2.0", "symfony/deprecation-contracts": "^2.5|^3", "symfony/service-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^6.2.10|^7.0" + "symfony/var-exporter": "^6.4.20|^7.2.5" }, "conflict": { "ext-psr": "<1.1|>=2", @@ -1902,7 +1827,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v6.4.16" + "source": "https://github.com/symfony/dependency-injection/tree/v6.4.38" }, "funding": [ { @@ -1913,25 +1838,29 @@ "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": "2024-11-25T14:52:46+00:00" + "time": "2026-05-04T13:00:01+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.5.1", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b", "shasum": "" }, "require": { @@ -1944,7 +1873,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -1969,7 +1898,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.0" }, "funding": [ { @@ -1980,25 +1909,29 @@ "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": "2024-09-25T14:20:29+00:00" + "time": "2026-04-13T15:52:40+00:00" }, { "name": "symfony/doctrine-bridge", - "version": "v6.4.18", + "version": "v6.4.34", "source": { "type": "git", "url": "https://github.com/symfony/doctrine-bridge.git", - "reference": "fd0094d4648bf6bbdafcd1f0c8aafb6e93d735e6" + "reference": "9e82991eda36e85b640644e1d8d34d89eff498a6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/fd0094d4648bf6bbdafcd1f0c8aafb6e93d735e6", - "reference": "fd0094d4648bf6bbdafcd1f0c8aafb6e93d735e6", + "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/9e82991eda36e85b640644e1d8d34d89eff498a6", + "reference": "9e82991eda36e85b640644e1d8d34d89eff498a6", "shasum": "" }, "require": { @@ -2077,7 +2010,7 @@ "description": "Provides integration for Doctrine with various Symfony components", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/doctrine-bridge/tree/v6.4.18" + "source": "https://github.com/symfony/doctrine-bridge/tree/v6.4.34" }, "funding": [ { @@ -2088,25 +2021,29 @@ "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-01-25T08:04:58+00:00" + "time": "2026-02-06T08:53:22+00:00" }, { "name": "symfony/dotenv", - "version": "v6.4.16", + "version": "v6.4.39", "source": { "type": "git", "url": "https://github.com/symfony/dotenv.git", - "reference": "1ac5e7e7e862d4d574258daf08bd569ba926e4a5" + "reference": "e25b00497fe75e318c01a72ea24c0a9c2837cd62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dotenv/zipball/1ac5e7e7e862d4d574258daf08bd569ba926e4a5", - "reference": "1ac5e7e7e862d4d574258daf08bd569ba926e4a5", + "url": "https://api.github.com/repos/symfony/dotenv/zipball/e25b00497fe75e318c01a72ea24c0a9c2837cd62", + "reference": "e25b00497fe75e318c01a72ea24c0a9c2837cd62", "shasum": "" }, "require": { @@ -2151,7 +2088,7 @@ "environment" ], "support": { - "source": "https://github.com/symfony/dotenv/tree/v6.4.16" + "source": "https://github.com/symfony/dotenv/tree/v6.4.39" }, "funding": [ { @@ -2162,25 +2099,29 @@ "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": "2024-11-27T11:08:19+00:00" + "time": "2026-05-06T14:07:03+00:00" }, { "name": "symfony/error-handler", - "version": "v6.4.18", + "version": "v6.4.36", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "e8d3b5b1975e67812a54388b1ba8e9ec28eb770e" + "reference": "2ea68f0e1835ad6a126f93bbc14cd236c10ab361" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/e8d3b5b1975e67812a54388b1ba8e9ec28eb770e", - "reference": "e8d3b5b1975e67812a54388b1ba8e9ec28eb770e", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/2ea68f0e1835ad6a126f93bbc14cd236c10ab361", + "reference": "2ea68f0e1835ad6a126f93bbc14cd236c10ab361", "shasum": "" }, "require": { @@ -2226,7 +2167,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v6.4.18" + "source": "https://github.com/symfony/error-handler/tree/v6.4.36" }, "funding": [ { @@ -2237,25 +2178,29 @@ "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-01-06T09:38:16+00:00" + "time": "2026-03-10T15:56:14+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v6.4.13", + "version": "v6.4.37", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "0ffc48080ab3e9132ea74ef4e09d8dcf26bf897e" + "reference": "2e3bf817ba9347341ab15926700fb6320367c0e1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/0ffc48080ab3e9132ea74ef4e09d8dcf26bf897e", - "reference": "0ffc48080ab3e9132ea74ef4e09d8dcf26bf897e", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/2e3bf817ba9347341ab15926700fb6320367c0e1", + "reference": "2e3bf817ba9347341ab15926700fb6320367c0e1", "shasum": "" }, "require": { @@ -2306,7 +2251,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v6.4.13" + "source": "https://github.com/symfony/event-dispatcher/tree/v6.4.37" }, "funding": [ { @@ -2317,25 +2262,29 @@ "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": "2024-09-25T14:18:03+00:00" + "time": "2026-04-13T14:11:12+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v3.5.1", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f" + "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/7642f5e970b672283b7823222ae8ef8bbc160b9f", - "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/ccba7060602b7fed0b03c85bf025257f76d9ef32", + "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32", "shasum": "" }, "require": { @@ -2349,7 +2298,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -2382,7 +2331,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.7.0" }, "funding": [ { @@ -2393,25 +2342,29 @@ "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": "2024-09-25T14:20:29+00:00" + "time": "2026-01-05T13:30:16+00:00" }, { "name": "symfony/filesystem", - "version": "v6.4.13", + "version": "v6.4.39", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "4856c9cf585d5a0313d8d35afd681a526f038dd3" + "reference": "c507b077756b4e3e09adbbe7975fac81cd3722ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/4856c9cf585d5a0313d8d35afd681a526f038dd3", - "reference": "4856c9cf585d5a0313d8d35afd681a526f038dd3", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/c507b077756b4e3e09adbbe7975fac81cd3722ca", + "reference": "c507b077756b4e3e09adbbe7975fac81cd3722ca", "shasum": "" }, "require": { @@ -2448,7 +2401,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v6.4.13" + "source": "https://github.com/symfony/filesystem/tree/v6.4.39" }, "funding": [ { @@ -2459,25 +2412,29 @@ "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": "2024-10-25T15:07:50+00:00" + "time": "2026-05-07T13:11:42+00:00" }, { "name": "symfony/finder", - "version": "v6.4.17", + "version": "v6.4.34", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "1d0e8266248c5d9ab6a87e3789e6dc482af3c9c7" + "reference": "9590e86be1d1c57bfbb16d0dd040345378c20896" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/1d0e8266248c5d9ab6a87e3789e6dc482af3c9c7", - "reference": "1d0e8266248c5d9ab6a87e3789e6dc482af3c9c7", + "url": "https://api.github.com/repos/symfony/finder/zipball/9590e86be1d1c57bfbb16d0dd040345378c20896", + "reference": "9590e86be1d1c57bfbb16d0dd040345378c20896", "shasum": "" }, "require": { @@ -2512,7 +2469,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v6.4.17" + "source": "https://github.com/symfony/finder/tree/v6.4.34" }, "funding": [ { @@ -2523,40 +2480,45 @@ "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": "2024-12-29T13:51:37+00:00" + "time": "2026-01-28T15:16:37+00:00" }, { "name": "symfony/flex", - "version": "v2.4.7", + "version": "v2.11.0", "source": { "type": "git", "url": "https://github.com/symfony/flex.git", - "reference": "92f4fba342161ff36072bd3b8e0b3c6c23160402" + "reference": "4a6d98eea3ebc7f68d82810cb682eedca2649e99" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/flex/zipball/92f4fba342161ff36072bd3b8e0b3c6c23160402", - "reference": "92f4fba342161ff36072bd3b8e0b3c6c23160402", + "url": "https://api.github.com/repos/symfony/flex/zipball/4a6d98eea3ebc7f68d82810cb682eedca2649e99", + "reference": "4a6d98eea3ebc7f68d82810cb682eedca2649e99", "shasum": "" }, "require": { "composer-plugin-api": "^2.1", - "php": ">=8.0" + "php": ">=8.1" }, "conflict": { - "composer/semver": "<1.7.2" + "composer/semver": "<1.7.2", + "symfony/dotenv": "<5.4" }, "require-dev": { "composer/composer": "^2.1", - "symfony/dotenv": "^5.4|^6.0", - "symfony/filesystem": "^5.4|^6.0", - "symfony/phpunit-bridge": "^5.4|^6.0", - "symfony/process": "^5.4|^6.0" + "phpunit/phpunit": "^12.4", + "symfony/dotenv": "^6.4.41|^7.4.13|^8.0.13", + "symfony/filesystem": "^6.4|^7.4|^8.0", + "symfony/process": "^6.4|^7.4|^8.0" }, "type": "composer-plugin", "extra": { @@ -2580,7 +2542,7 @@ "description": "Composer plugin for Symfony", "support": { "issues": "https://github.com/symfony/flex/issues", - "source": "https://github.com/symfony/flex/tree/v2.4.7" + "source": "https://github.com/symfony/flex/tree/v2.11.0" }, "funding": [ { @@ -2591,25 +2553,29 @@ "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": "2024-10-07T08:51:54+00:00" + "time": "2026-05-29T17:25:22+00:00" }, { "name": "symfony/framework-bundle", - "version": "v6.4.18", + "version": "v6.4.41", "source": { "type": "git", "url": "https://github.com/symfony/framework-bundle.git", - "reference": "91df8ee37543ebc01756c9e5eaf94d1878ff1ccd" + "reference": "53dd12518c5c8d1f9b92f077fed4f521bccfbc0a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/91df8ee37543ebc01756c9e5eaf94d1878ff1ccd", - "reference": "91df8ee37543ebc01756c9e5eaf94d1878ff1ccd", + "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/53dd12518c5c8d1f9b92f077fed4f521bccfbc0a", + "reference": "53dd12518c5c8d1f9b92f077fed4f521bccfbc0a", "shasum": "" }, "require": { @@ -2645,7 +2611,7 @@ "symfony/lock": "<5.4", "symfony/mailer": "<5.4", "symfony/messenger": "<6.3", - "symfony/mime": "<6.4", + "symfony/mime": "<6.4.37|>=7.0,<7.4.9", "symfony/property-access": "<5.4", "symfony/property-info": "<5.4", "symfony/runtime": "<5.4.45|>=6.0,<6.4.13|>=7.0,<7.1.6", @@ -2682,7 +2648,7 @@ "symfony/lock": "^5.4|^6.0|^7.0", "symfony/mailer": "^5.4|^6.0|^7.0", "symfony/messenger": "^6.3|^7.0", - "symfony/mime": "^6.4|^7.0", + "symfony/mime": "^6.4.37|^7.4.9", "symfony/notifier": "^5.4|^6.0|^7.0", "symfony/polyfill-intl-icu": "~1.0", "symfony/process": "^5.4|^6.0|^7.0", @@ -2729,7 +2695,7 @@ "description": "Provides a tight integration between Symfony components and the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/framework-bundle/tree/v6.4.18" + "source": "https://github.com/symfony/framework-bundle/tree/v6.4.41" }, "funding": [ { @@ -2740,25 +2706,29 @@ "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-01-28T18:47:02+00:00" + "time": "2026-05-23T15:52:25+00:00" }, { "name": "symfony/http-foundation", - "version": "v6.4.18", + "version": "v6.4.41", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "d0492d6217e5ab48f51fca76f64cf8e78919d0db" + "reference": "48d76c29a67a301e0f7779a512bf76417395ffef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/d0492d6217e5ab48f51fca76f64cf8e78919d0db", - "reference": "d0492d6217e5ab48f51fca76f64cf8e78919d0db", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/48d76c29a67a301e0f7779a512bf76417395ffef", + "reference": "48d76c29a67a301e0f7779a512bf76417395ffef", "shasum": "" }, "require": { @@ -2806,7 +2776,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v6.4.18" + "source": "https://github.com/symfony/http-foundation/tree/v6.4.41" }, "funding": [ { @@ -2817,25 +2787,29 @@ "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-01-09T15:48:56+00:00" + "time": "2026-05-24T10:54:17+00:00" }, { "name": "symfony/http-kernel", - "version": "v6.4.18", + "version": "v6.4.41", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "fca7197bfe9e99dfae7fb1ad3f7f5bd9ef80e1b7" + "reference": "155f6c1fa29720e8c947591da3be4014951fba6f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/fca7197bfe9e99dfae7fb1ad3f7f5bd9ef80e1b7", - "reference": "fca7197bfe9e99dfae7fb1ad3f7f5bd9ef80e1b7", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/155f6c1fa29720e8c947591da3be4014951fba6f", + "reference": "155f6c1fa29720e8c947591da3be4014951fba6f", "shasum": "" }, "require": { @@ -2876,7 +2850,7 @@ "symfony/config": "^6.1|^7.0", "symfony/console": "^5.4|^6.0|^7.0", "symfony/css-selector": "^5.4|^6.0|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4.1|^7.0.1", "symfony/dom-crawler": "^5.4|^6.0|^7.0", "symfony/expression-language": "^5.4|^6.0|^7.0", "symfony/finder": "^5.4|^6.0|^7.0", @@ -2920,7 +2894,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v6.4.18" + "source": "https://github.com/symfony/http-kernel/tree/v6.4.41" }, "funding": [ { @@ -2931,25 +2905,29 @@ "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-01-29T07:25:58+00:00" + "time": "2026-05-27T08:22:22+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.31.0", + "version": "v1.38.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" + "reference": "e9247d281d694a5120554d9afaf54e070e88a603" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", - "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/e9247d281d694a5120554d9afaf54e070e88a603", + "reference": "e9247d281d694a5120554d9afaf54e070e88a603", "shasum": "" }, "require": { @@ -2998,7 +2976,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.38.1" }, "funding": [ { @@ -3009,25 +2987,29 @@ "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": "2024-09-09T11:45:10+00:00" + "time": "2026-05-26T05:58:03+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.31.0", + "version": "v1.38.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "3833d7255cc303546435cb650316bff708a1c75c" + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", - "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/2d446c214bdbe5b71bde5011b060a05fece3ae6b", + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b", "shasum": "" }, "require": { @@ -3079,7 +3061,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.38.0" }, "funding": [ { @@ -3090,28 +3072,33 @@ "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": "2024-09-09T11:45:10+00:00" + "time": "2026-05-25T13:48:31+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.31.0", + "version": "v1.38.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" + "reference": "14c5439eec4ccff081ac14eca2dc57feb2a66d92" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/14c5439eec4ccff081ac14eca2dc57feb2a66d92", + "reference": "14c5439eec4ccff081ac14eca2dc57feb2a66d92", "shasum": "" }, "require": { + "ext-iconv": "*", "php": ">=7.2" }, "provide": { @@ -3159,7 +3146,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.1" }, "funding": [ { @@ -3170,25 +3157,29 @@ "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": "2024-09-09T11:45:10+00:00" + "time": "2026-05-26T12:51:13+00:00" }, { "name": "symfony/polyfill-php83", - "version": "v1.31.0", + "version": "v1.38.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php83.git", - "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491" + "reference": "8339098cae28673c15cce00d80734af0453054e2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491", - "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/8339098cae28673c15cce00d80734af0453054e2", + "reference": "8339098cae28673c15cce00d80734af0453054e2", "shasum": "" }, "require": { @@ -3235,7 +3226,87 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.38.1" + }, + "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-05-26T12:51:13+00:00" + }, + { + "name": "symfony/polyfill-php84", + "version": "v1.38.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa", + "reference": "f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "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 backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.38.1" }, "funding": [ { @@ -3246,25 +3317,29 @@ "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": "2024-09-09T11:45:10+00:00" + "time": "2026-05-26T12:51:13+00:00" }, { "name": "symfony/routing", - "version": "v6.4.18", + "version": "v6.4.41", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "e9bfc94953019089acdfb9be51c1b9142c4afa68" + "reference": "af04c79671fd8df0805a44c83fa2b0ba56c8329e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/e9bfc94953019089acdfb9be51c1b9142c4afa68", - "reference": "e9bfc94953019089acdfb9be51c1b9142c4afa68", + "url": "https://api.github.com/repos/symfony/routing/zipball/af04c79671fd8df0805a44c83fa2b0ba56c8329e", + "reference": "af04c79671fd8df0805a44c83fa2b0ba56c8329e", "shasum": "" }, "require": { @@ -3318,7 +3393,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v6.4.18" + "source": "https://github.com/symfony/routing/tree/v6.4.41" }, "funding": [ { @@ -3329,25 +3404,29 @@ "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-01-09T08:51:02+00:00" + "time": "2026-05-24T11:18:16+00:00" }, { "name": "symfony/runtime", - "version": "v6.4.14", + "version": "v6.4.41", "source": { "type": "git", "url": "https://github.com/symfony/runtime.git", - "reference": "4facd4174f45cd37c65860403412b67c7381136a" + "reference": "4a354e15532ed4f99c6d0fdc5618bfbd3ea05a8a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/runtime/zipball/4facd4174f45cd37c65860403412b67c7381136a", - "reference": "4facd4174f45cd37c65860403412b67c7381136a", + "url": "https://api.github.com/repos/symfony/runtime/zipball/4a354e15532ed4f99c6d0fdc5618bfbd3ea05a8a", + "reference": "4a354e15532ed4f99c6d0fdc5618bfbd3ea05a8a", "shasum": "" }, "require": { @@ -3397,7 +3476,7 @@ "runtime" ], "support": { - "source": "https://github.com/symfony/runtime/tree/v6.4.14" + "source": "https://github.com/symfony/runtime/tree/v6.4.41" }, "funding": [ { @@ -3408,25 +3487,29 @@ "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": "2024-11-05T16:39:55+00:00" + "time": "2026-05-23T14:01:00+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.5.1", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0" + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0", - "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d25d82433a80eba6aa0e6c24b61d7370d99e444a", + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a", "shasum": "" }, "require": { @@ -3444,7 +3527,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -3480,7 +3563,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.7.0" }, "funding": [ { @@ -3491,25 +3574,29 @@ "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": "2024-09-25T14:20:29+00:00" + "time": "2026-03-28T09:44:51+00:00" }, { "name": "symfony/stopwatch", - "version": "v6.4.13", + "version": "v6.4.24", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "2cae0a6f8d04937d02f6d19806251e2104d54f92" + "reference": "b67e94e06a05d9572c2fa354483b3e13e3cb1898" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/2cae0a6f8d04937d02f6d19806251e2104d54f92", - "reference": "2cae0a6f8d04937d02f6d19806251e2104d54f92", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/b67e94e06a05d9572c2fa354483b3e13e3cb1898", + "reference": "b67e94e06a05d9572c2fa354483b3e13e3cb1898", "shasum": "" }, "require": { @@ -3542,7 +3629,7 @@ "description": "Provides a way to profile code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/stopwatch/tree/v6.4.13" + "source": "https://github.com/symfony/stopwatch/tree/v6.4.24" }, "funding": [ { @@ -3553,25 +3640,29 @@ "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": "2024-09-25T14:18:03+00:00" + "time": "2025-07-10T08:14:14+00:00" }, { "name": "symfony/string", - "version": "v6.4.15", + "version": "v6.4.39", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "73a5e66ea2e1677c98d4449177c5a9cf9d8b4c6f" + "reference": "62e3c927de664edadb5bef260987eb047a17a113" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/73a5e66ea2e1677c98d4449177c5a9cf9d8b4c6f", - "reference": "73a5e66ea2e1677c98d4449177c5a9cf9d8b4c6f", + "url": "https://api.github.com/repos/symfony/string/zipball/62e3c927de664edadb5bef260987eb047a17a113", + "reference": "62e3c927de664edadb5bef260987eb047a17a113", "shasum": "" }, "require": { @@ -3585,7 +3676,6 @@ "symfony/translation-contracts": "<2.5" }, "require-dev": { - "symfony/error-handler": "^5.4|^6.0|^7.0", "symfony/http-client": "^5.4|^6.0|^7.0", "symfony/intl": "^6.2|^7.0", "symfony/translation-contracts": "^2.5|^3.0", @@ -3628,7 +3718,190 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v6.4.15" + "source": "https://github.com/symfony/string/tree/v6.4.39" + }, + "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-05-12T11:44:19+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v3.7.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "0ab302977a952b42fd51475c4ebac81f8da0a95d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/0ab302977a952b42fd51475c4ebac81f8da0a95d", + "reference": "0ab302977a952b42fd51475c4ebac81f8da0a95d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "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": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/translation-contracts/tree/v3.7.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-01-05T13:30:16+00:00" + }, + { + "name": "symfony/validator", + "version": "v6.4.37", + "source": { + "type": "git", + "url": "https://github.com/symfony/validator.git", + "reference": "72cfcf7925746d9950bbdab1362f6bda3b4046cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/validator/zipball/72cfcf7925746d9950bbdab1362f6bda3b4046cf", + "reference": "72cfcf7925746d9950bbdab1362f6bda3b4046cf", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php83": "^1.27", + "symfony/translation-contracts": "^2.5|^3" + }, + "conflict": { + "doctrine/annotations": "<1.13", + "doctrine/lexer": "<1.1", + "symfony/dependency-injection": "<5.4", + "symfony/expression-language": "<5.4", + "symfony/http-kernel": "<5.4", + "symfony/intl": "<5.4", + "symfony/property-info": "<5.4", + "symfony/translation": "<5.4.35|>=6.0,<6.3.12|>=6.4,<6.4.3|>=7.0,<7.0.3", + "symfony/yaml": "<5.4" + }, + "require-dev": { + "doctrine/annotations": "^1.13|^2", + "egulias/email-validator": "^2.1.10|^3|^4", + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/finder": "^5.4|^6.0|^7.0", + "symfony/http-client": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/intl": "^5.4|^6.0|^7.0", + "symfony/mime": "^5.4|^6.0|^7.0", + "symfony/property-access": "^5.4|^6.0|^7.0", + "symfony/property-info": "^5.4|^6.0|^7.0", + "symfony/translation": "^5.4.35|~6.3.12|^6.4.3|^7.0.3", + "symfony/yaml": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Validator\\": "" + }, + "exclude-from-classmap": [ + "/Tests/", + "/Resources/bin/" + ] + }, + "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 tools to validate values", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/validator/tree/v6.4.37" }, "funding": [ { @@ -3639,25 +3912,29 @@ "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": "2024-11-13T13:31:12+00:00" + "time": "2026-04-29T06:33:37+00:00" }, { "name": "symfony/var-dumper", - "version": "v6.4.18", + "version": "v6.4.36", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "4ad10cf8b020e77ba665305bb7804389884b4837" + "reference": "7c8ad9ce4faf6c8a99948e70ce02b601a0439782" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/4ad10cf8b020e77ba665305bb7804389884b4837", - "reference": "4ad10cf8b020e77ba665305bb7804389884b4837", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/7c8ad9ce4faf6c8a99948e70ce02b601a0439782", + "reference": "7c8ad9ce4faf6c8a99948e70ce02b601a0439782", "shasum": "" }, "require": { @@ -3669,7 +3946,6 @@ "symfony/console": "<5.4" }, "require-dev": { - "ext-iconv": "*", "symfony/console": "^5.4|^6.0|^7.0", "symfony/error-handler": "^6.3|^7.0", "symfony/http-kernel": "^5.4|^6.0|^7.0", @@ -3713,7 +3989,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v6.4.18" + "source": "https://github.com/symfony/var-dumper/tree/v6.4.36" }, "funding": [ { @@ -3724,25 +4000,29 @@ "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-01-17T11:26:11+00:00" + "time": "2026-03-30T15:36:00+00:00" }, { "name": "symfony/var-exporter", - "version": "v6.4.13", + "version": "v6.4.37", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "0f605f72a363f8743001038a176eeb2a11223b51" + "reference": "34f6957deffacabd1b1c579a312daa481e3e99ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/0f605f72a363f8743001038a176eeb2a11223b51", - "reference": "0f605f72a363f8743001038a176eeb2a11223b51", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/34f6957deffacabd1b1c579a312daa481e3e99ca", + "reference": "34f6957deffacabd1b1c579a312daa481e3e99ca", "shasum": "" }, "require": { @@ -3790,7 +4070,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v6.4.13" + "source": "https://github.com/symfony/var-exporter/tree/v6.4.37" }, "funding": [ { @@ -3801,25 +4081,29 @@ "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": "2024-09-25T14:18:03+00:00" + "time": "2026-04-14T12:12:40+00:00" }, { "name": "symfony/yaml", - "version": "v6.4.18", + "version": "v6.4.41", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "bf598c9d9bb4a22f495a4e26e4c4fce2f8ecefc5" + "reference": "e8fdf3408c85806198d5826e604ffc6830d33152" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/bf598c9d9bb4a22f495a4e26e4c4fce2f8ecefc5", - "reference": "bf598c9d9bb4a22f495a4e26e4c4fce2f8ecefc5", + "url": "https://api.github.com/repos/symfony/yaml/zipball/e8fdf3408c85806198d5826e604ffc6830d33152", + "reference": "e8fdf3408c85806198d5826e604ffc6830d33152", "shasum": "" }, "require": { @@ -3862,7 +4146,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v6.4.18" + "source": "https://github.com/symfony/yaml/tree/v6.4.41" }, "funding": [ { @@ -3873,27 +4157,31 @@ "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-01-07T09:44:41+00:00" + "time": "2026-05-25T06:03:23+00:00" } ], "packages-dev": [ { "name": "myclabs/deep-copy", - "version": "1.12.1", + "version": "1.13.4", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845" + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/123267b2c49fbf30d78a7b2d333f6be754b94845", - "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", "shasum": "" }, "require": { @@ -3932,7 +4220,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.12.1" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" }, "funding": [ { @@ -3940,20 +4228,20 @@ "type": "tidelift" } ], - "time": "2024-11-08T17:47:46+00:00" + "time": "2025-08-01T08:46:24+00:00" }, { "name": "nikic/php-parser", - "version": "v5.4.0", + "version": "v5.7.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "447a020a1f875a434d62f2a401f53b82a396e494" + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494", - "reference": "447a020a1f875a434d62f2a401f53b82a396e494", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", "shasum": "" }, "require": { @@ -3972,7 +4260,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.x-dev" } }, "autoload": { @@ -3996,9 +4284,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" }, - "time": "2024-12-30T11:07:19+00:00" + "time": "2025-12-06T11:56:16+00:00" }, { "name": "phar-io/manifest", @@ -4439,16 +4727,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.22", + "version": "9.6.34", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "f80235cb4d3caa59ae09be3adf1ded27521d1a9c" + "reference": "b36f02317466907a230d3aa1d34467041271ef4a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f80235cb4d3caa59ae09be3adf1ded27521d1a9c", - "reference": "f80235cb4d3caa59ae09be3adf1ded27521d1a9c", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b36f02317466907a230d3aa1d34467041271ef4a", + "reference": "b36f02317466907a230d3aa1d34467041271ef4a", "shasum": "" }, "require": { @@ -4459,7 +4747,7 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.12.1", + "myclabs/deep-copy": "^1.13.4", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=7.3", @@ -4470,11 +4758,11 @@ "phpunit/php-timer": "^5.0.3", "sebastian/cli-parser": "^1.0.2", "sebastian/code-unit": "^1.0.8", - "sebastian/comparator": "^4.0.8", + "sebastian/comparator": "^4.0.10", "sebastian/diff": "^4.0.6", "sebastian/environment": "^5.1.5", - "sebastian/exporter": "^4.0.6", - "sebastian/global-state": "^5.0.7", + "sebastian/exporter": "^4.0.8", + "sebastian/global-state": "^5.0.8", "sebastian/object-enumerator": "^4.0.4", "sebastian/resource-operations": "^3.0.4", "sebastian/type": "^3.2.1", @@ -4522,7 +4810,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.22" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.34" }, "funding": [ { @@ -4533,12 +4821,20 @@ "url": "https://github.com/sebastianbergmann", "type": "github" }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, { "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", "type": "tidelift" } ], - "time": "2024-12-05T13:48:26+00:00" + "time": "2026-01-27T05:45:00+00:00" }, { "name": "sebastian/cli-parser", @@ -4709,16 +5005,16 @@ }, { "name": "sebastian/comparator", - "version": "4.0.8", + "version": "4.0.10", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "fa0f136dd2334583309d32b62544682ee972b51a" + "reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", - "reference": "fa0f136dd2334583309d32b62544682ee972b51a", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/e4df00b9b3571187db2831ae9aada2c6efbd715d", + "reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d", "shasum": "" }, "require": { @@ -4771,15 +5067,27 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.10" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" } ], - "time": "2022-09-14T12:41:17+00:00" + "time": "2026-01-24T09:22:56+00:00" }, { "name": "sebastian/complexity", @@ -4969,16 +5277,16 @@ }, { "name": "sebastian/exporter", - "version": "4.0.6", + "version": "4.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72" + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72", - "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/14c6ba52f95a36c3d27c835d65efc7123c446e8c", + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c", "shasum": "" }, "require": { @@ -5034,28 +5342,40 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6" + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.8" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" } ], - "time": "2024-03-02T06:33:00+00:00" + "time": "2025-09-24T06:03:27+00:00" }, { "name": "sebastian/global-state", - "version": "5.0.7", + "version": "5.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9" + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", - "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b6781316bdcd28260904e7cc18ec983d0d2ef4f6", + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6", "shasum": "" }, "require": { @@ -5098,15 +5418,27 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7" + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.8" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", + "type": "tidelift" } ], - "time": "2024-03-02T06:35:11+00:00" + "time": "2025-08-10T07:10:35+00:00" }, { "name": "sebastian/lines-of-code", @@ -5279,16 +5611,16 @@ }, { "name": "sebastian/recursion-context", - "version": "4.0.5", + "version": "4.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" + "reference": "539c6691e0623af6dc6f9c20384c120f963465a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", - "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/539c6691e0623af6dc6f9c20384c120f963465a0", + "reference": "539c6691e0623af6dc6f9c20384c120f963465a0", "shasum": "" }, "require": { @@ -5330,15 +5662,27 @@ "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.6" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" } ], - "time": "2023-02-03T06:07:39+00:00" + "time": "2025-08-10T06:57:39+00:00" }, { "name": "sebastian/resource-operations", @@ -5505,16 +5849,16 @@ }, { "name": "theseer/tokenizer", - "version": "1.2.3", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", "shasum": "" }, "require": { @@ -5543,7 +5887,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" }, "funding": [ { @@ -5551,7 +5895,7 @@ "type": "github" } ], - "time": "2024-03-03T12:36:25+00:00" + "time": "2025-11-17T20:03:58+00:00" } ], "aliases": [], @@ -5565,5 +5909,5 @@ "ext-iconv": "*" }, "platform-dev": {}, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/config/packages/doctrine.yaml b/config/packages/doctrine.yaml index 315e82c..859bccf 100644 --- a/config/packages/doctrine.yaml +++ b/config/packages/doctrine.yaml @@ -10,7 +10,7 @@ doctrine: naming_strategy: doctrine.orm.naming_strategy.underscore auto_mapping: true mappings: - App\Entity: - dir: '%kernel.project_dir%/src/Entity' - prefix: 'App\Entity' + App\Domain\Entity: + dir: '%kernel.project_dir%/src/Domain/Entity' + prefix: 'App\Domain\Entity' is_bundle: false diff --git a/config/packages/validator.yaml b/config/packages/validator.yaml new file mode 100644 index 0000000..0201281 --- /dev/null +++ b/config/packages/validator.yaml @@ -0,0 +1,13 @@ +framework: + validation: + email_validation_mode: html5 + + # Enables validator auto-mapping support. + # For instance, basic validation constraints will be inferred from Doctrine's metadata. + #auto_mapping: + # App\Entity\: [] + +when@test: + framework: + validation: + not_compromised_password: false diff --git a/config/services.yaml b/config/services.yaml index 31ed25d..9de87a0 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -17,9 +17,9 @@ services: resource: '../src/' exclude: - '../src/DependencyInjection/' - - '../src/Entity/' + - '../src/Domain/Entity/' - '../src/Kernel.php' - App\Command\: - resource: '../src/Command' - tags: ['console.command'] + _instanceof: + App\Domain\Parser\InvoiceParserInterface: + tags: ['app.invoice_parser'] diff --git a/data/invoices.csv b/data/invoices_20260607120101_cid_1.csv similarity index 100% rename from data/invoices.csv rename to data/invoices_20260607120101_cid_1.csv diff --git a/data/invoices.json b/data/invoices_20260607120201_cid_1.json similarity index 100% rename from data/invoices.json rename to data/invoices_20260607120201_cid_1.json diff --git a/data/invoices_20260607120301_cid_4.json b/data/invoices_20260607120301_cid_4.json new file mode 100644 index 0000000..0d7c70a --- /dev/null +++ b/data/invoices_20260607120301_cid_4.json @@ -0,0 +1,20 @@ +[ + { + "nom_prenom": "Gaël Dupont", + "price": "not-a-number", + "devise": "EUR", + "date": "not-a-date" + }, + { + "nom_prenom": "John Doe", + "price": 999.38, + "devise": "EUR", + "date": "2025-02-06" + }, + { + "nom_prenom": "Raphaël Guérin", + "price": 135.87, + "devise": "USD", + "date": "2025-02-06" + } +] diff --git a/data/invoices_20260607120401_cid_2.csv b/data/invoices_20260607120401_cid_2.csv new file mode 100644 index 0000000..687d294 --- /dev/null +++ b/data/invoices_20260607120401_cid_2.csv @@ -0,0 +1,3 @@ +name,amount,currency,civility,date +John Doe,670.43,EUR,Mr,2025-02-06 +Raphaël Guérin,852.38,EURR,Mr,2025-02-06 \ No newline at end of file diff --git a/migrations/.gitignore b/migrations/.gitignore deleted file mode 100644 index e69de29..0000000 diff --git a/migrations/Version20260607091233.php b/migrations/Version20260607091233.php new file mode 100644 index 0000000..329a781 --- /dev/null +++ b/migrations/Version20260607091233.php @@ -0,0 +1,30 @@ +addSql('ALTER TABLE invoice ALTER COLUMN currency TYPE VARCHAR(3)'); + $this->addSql('ALTER TABLE invoice ADD date DATE NOT NULL DEFAULT CURRENT_DATE'); + $this->addSql('ALTER TABLE invoice ALTER COLUMN date DROP DEFAULT'); + $this->addSql("COMMENT ON COLUMN invoice.date IS '(DC2Type:date_immutable)'"); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE invoice DROP COLUMN date'); + $this->addSql('ALTER TABLE invoice ALTER COLUMN currency TYPE VARCHAR(255)'); + } +} diff --git a/src/Application/Command/ParseInvoiceFile/ParseInvoiceFileCommand.php b/src/Application/Command/ParseInvoiceFile/ParseInvoiceFileCommand.php new file mode 100644 index 0000000..c5757f7 --- /dev/null +++ b/src/Application/Command/ParseInvoiceFile/ParseInvoiceFileCommand.php @@ -0,0 +1,26 @@ +filePath; + $parser = $this->parserRegistry->getParser($file); + + if ($parser === null) { + $message = sprintf('No parser found for file: "%s".', basename($file)); + $result->addError(0, 'parser', $message, true); + $this->logger->error($file, 0, 'parser', $message); + return $result; + } + + try { + $rows = $parser->parse($file); + } catch (Throwable $e) { + $result->addError(0, 'parse', $e->getMessage(), true); + $this->logger->error($file, 0, 'parse', $e->getMessage()); + return $result; + } + + /** @var array */ + $invoiceCache = []; + + foreach ($rows as $index => $row) { + $line = $index + 1; + + if ($row instanceof Throwable) { + $result->addError($line, 'parse', $row->getMessage()); + $result->incrementSkipped(); + $this->logger->warning($file, $line, 'parse', $row->getMessage()); + continue; + } + + $violations = $this->validator->validate($row); + if (count($violations) > 0) { + foreach ($violations as $violation) { + $field = $violation->getPropertyPath(); + $message = (string) $violation->getMessage(); + $result->addError($line, $field, $message); + $this->logger->warning($file, $line, $field, $message); + } + $result->incrementSkipped(); + continue; + } + + try { + $key = $row->getName() . '|' . $row->getDate()->format('Y-m-d'); + $existing = $invoiceCache[$key] ?? $this->invoiceRepository->findByNameAndDate($row->getName(), $row->getDate()); + + if ($existing !== null) { + $existing->updateAmountAndCurrency($row->getAmount(), $row->getCurrency()); + $this->invoiceRepository->update($existing); + $result->incrementUpdated(); + } else { + $this->invoiceRepository->create($row); + $result->incrementInserted(); + $invoiceCache[$key] = $row; + } + } catch (Throwable $e) { + $result->addError($line, 'database', $e->getMessage()); + $result->incrementSkipped(); + $this->logger->error($file, $line, 'database', $e->getMessage()); + } + } + + try { + $this->invoiceRepository->flush(); + } catch (Throwable $e) { + $this->logger->error($file, 0, 'flush', $e->getMessage()); + throw $e; + } + + return $result; + } +} diff --git a/src/Application/Command/ParseInvoiceFile/ParseInvoiceResult.php b/src/Application/Command/ParseInvoiceFile/ParseInvoiceResult.php new file mode 100644 index 0000000..e8812ec --- /dev/null +++ b/src/Application/Command/ParseInvoiceFile/ParseInvoiceResult.php @@ -0,0 +1,69 @@ + */ + private array $errors = []; + + public function incrementInserted(): void + { + $this->inserted++; + } + + public function incrementUpdated(): void + { + $this->updated++; + } + + public function addError(int $line, string $field, string $message, bool $critical = false): void + { + $this->errors[] = ['line' => $line, 'field' => $field, 'message' => $message, 'critical' => $critical]; + } + + public function incrementSkipped(): void + { + $this->skipped++; + } + + public function merge(self $other): void + { + $this->inserted += $other->getInserted(); + $this->updated += $other->getUpdated(); + $this->skipped += $other->getSkipped(); + $this->errors = array_merge($this->errors, $other->getErrors()); + } + + public function hasErrors(): bool + { + return count($this->errors) > 0; + } + + public function getInserted(): int + { + return $this->inserted; + } + + public function getUpdated(): int + { + return $this->updated; + } + + public function getSkipped(): int + { + return $this->skipped; + } + + /** @return array */ + public function getErrors(): array + { + return $this->errors; + } +} diff --git a/src/Command/ParseInvoicesCommand.php b/src/Command/ParseInvoicesCommand.php deleted file mode 100644 index 5dd24eb..0000000 --- a/src/Command/ParseInvoicesCommand.php +++ /dev/null @@ -1,31 +0,0 @@ -parser = $parser; - } - - protected function execute(InputInterface $input, OutputInterface $output): int - { - $this->parser->parse('data/invoices.json'); - $this->parser->parse('data/invoices.csv'); - return Command::SUCCESS; - } -} diff --git a/src/Domain/Entity/Invoice.php b/src/Domain/Entity/Invoice.php new file mode 100644 index 0000000..d35a098 --- /dev/null +++ b/src/Domain/Entity/Invoice.php @@ -0,0 +1,78 @@ +name = $name; + $this->amount = $amount; + $this->currency = $currency; + $this->date = $date; + } + + public function getId(): int + { + return $this->id; + } + + public function getName(): string + { + return $this->name; + } + + public function getAmount(): float + { + return $this->amount; + } + + public function getCurrency(): string + { + return $this->currency; + } + + public function getDate(): DateTimeImmutable + { + return $this->date; + } + + public function updateAmountAndCurrency(float $amount, string $currency): void + { + $this->amount = $amount; + $this->currency = $currency; + } +} diff --git a/src/Domain/Parser/InvoiceParserInterface.php b/src/Domain/Parser/InvoiceParserInterface.php new file mode 100644 index 0000000..1d3e8eb --- /dev/null +++ b/src/Domain/Parser/InvoiceParserInterface.php @@ -0,0 +1,16 @@ + */ + public function parse(string $filePath): array; +} diff --git a/src/Domain/Parser/InvoiceParserRegistryInterface.php b/src/Domain/Parser/InvoiceParserRegistryInterface.php new file mode 100644 index 0000000..df6dd6c --- /dev/null +++ b/src/Domain/Parser/InvoiceParserRegistryInterface.php @@ -0,0 +1,10 @@ +addArgument( + 'files', + InputArgument::IS_ARRAY | InputArgument::REQUIRED, + 'One or more invoice files to parse (CSV or JSON), named with a client ID: invoices_{datetime}_cid_{id}.{ext}', + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $files = $input->getArgument('files'); + $total = new ParseInvoiceResult(); + + foreach ($files as $file) { + $io->section(sprintf('Processing: %s', $file)); + + try { + $result = $this->handler->handle(new ParseInvoiceFileCommand($file)); + } catch (InvalidArgumentException $e) { + $io->error($e->getMessage()); + continue; + } + + $io->text(sprintf( + 'Inserted: %d, Updated: %d, Skipped: %d', + $result->getInserted(), + $result->getUpdated(), + $result->getSkipped(), + )); + + if ($result->hasErrors()) { + foreach ($result->getErrors() as $error) { + $message = sprintf( + '[%s] Line %d | field: %s | %s', + basename($file), + $error['line'], + $error['field'], + $error['message'], + ); + + if ($error['critical']) { + $io->error($message); + } else { + $io->warning($message); + } + } + $io->note('See var/log/invoice.log for details.'); + } + + $total->merge($result); + } + + $io->success(sprintf( + 'Done — inserted: %d, updated: %d, skipped: %d.', + $total->getInserted(), + $total->getUpdated(), + $total->getSkipped(), + )); + + return Command::SUCCESS; + } +} diff --git a/src/Infrastructure/Logger/InvoiceLogger.php b/src/Infrastructure/Logger/InvoiceLogger.php new file mode 100644 index 0000000..c7fa059 --- /dev/null +++ b/src/Infrastructure/Logger/InvoiceLogger.php @@ -0,0 +1,47 @@ +path = $logDir . '/invoice.log'; + } + + public function warning(string $file, int $line, string $field, string $message): void + { + $this->write('WARNING', $file, $line, $field, $message); + } + + public function error(string $file, int $line, string $field, string $message): void + { + $this->write('ERROR', $file, $line, $field, $message); + } + + private function write(string $level, string $file, int $line, string $field, string $message): void + { + file_put_contents( + $this->path, + sprintf( + "[%s] [%-7s] [%s] Line %d | field: %s | %s\n", + (new DateTimeImmutable())->format('Y-m-d H:i:s'), + $level, + basename($file), + $line, + $field, + $message, + ), + FILE_APPEND, + ); + } +} diff --git a/src/Infrastructure/Parser/Csv/AbstractCsvInvoiceParser.php b/src/Infrastructure/Parser/Csv/AbstractCsvInvoiceParser.php new file mode 100644 index 0000000..d941f0a --- /dev/null +++ b/src/Infrastructure/Parser/Csv/AbstractCsvInvoiceParser.php @@ -0,0 +1,66 @@ +supportsClient($filePath); + } + + public function parse(string $filePath): array + { + $lines = @file($filePath, FILE_SKIP_EMPTY_LINES | FILE_IGNORE_NEW_LINES); + + if ($lines === false) { + throw new RuntimeException(sprintf('Cannot read file "%s".', $filePath)); + } + + if ($this->hasHeader()) { + array_shift($lines); + } + + $map = array_combine($this->getColumns(), $this->getPositions()); + $result = []; + + foreach ($lines as $line) { + try { + $row = str_getcsv($line, $this->getSeparator()); + $result[] = new Invoice( + name: $row[$map['name']], + amount: (float) $row[$map['amount']], + currency: $row[$map['currency']], + date: new DateTimeImmutable($row[$map['date']]), + ); + } catch (Throwable $e) { + $result[] = $e; + } + } + + return $result; + } +} diff --git a/src/Infrastructure/Parser/Csv/Cid1CsvInvoiceParser.php b/src/Infrastructure/Parser/Csv/Cid1CsvInvoiceParser.php new file mode 100644 index 0000000..98e7a16 --- /dev/null +++ b/src/Infrastructure/Parser/Csv/Cid1CsvInvoiceParser.php @@ -0,0 +1,33 @@ + */ + private iterable $parsers; + + public function __construct( + #[AutowireIterator('app.invoice_parser')] + iterable $parsers, + ) { + $this->parsers = $parsers; + } + + public function getParser(string $filePath): ?InvoiceParserInterface + { + foreach ($this->parsers as $parser) { + if ($parser->supports($filePath)) { + return $parser; + } + } + + return null; + } +} diff --git a/src/Infrastructure/Parser/Json/AbstractJsonInvoiceParser.php b/src/Infrastructure/Parser/Json/AbstractJsonInvoiceParser.php new file mode 100644 index 0000000..5e14ab4 --- /dev/null +++ b/src/Infrastructure/Parser/Json/AbstractJsonInvoiceParser.php @@ -0,0 +1,66 @@ + 'nom', 'amount' => 'montant', 'currency' => 'devise', 'date' => 'date'] + * + * @return array + */ + abstract public function getFieldMap(): array; + + public function supports(string $filePath): bool + { + return pathinfo($filePath, PATHINFO_EXTENSION) === self::EXTENSION + && $this->supportsClient($filePath); + } + + public function parse(string $filePath): array + { + $contents = @file_get_contents($filePath); + + if ($contents === false) { + throw new RuntimeException(sprintf('Cannot read file "%s".', $filePath)); + } + + $data = json_decode( + $contents, + associative: true, + depth: 512, + flags: JSON_THROW_ON_ERROR, + ); + + $map = $this->getFieldMap(); + $result = []; + + foreach ($data as $row) { + try { + $result[] = new Invoice( + name: $row[$map['name']], + amount: (float) $row[$map['amount']], + currency: $row[$map['currency']], + date: new DateTimeImmutable($row[$map['date']]), + ); + } catch (Throwable $e) { + $result[] = $e; + } + } + + return $result; + } +} diff --git a/src/Infrastructure/Parser/Json/Cid1JsonInvoiceParser.php b/src/Infrastructure/Parser/Json/Cid1JsonInvoiceParser.php new file mode 100644 index 0000000..34ada50 --- /dev/null +++ b/src/Infrastructure/Parser/Json/Cid1JsonInvoiceParser.php @@ -0,0 +1,23 @@ + 'nom', + 'amount' => 'montant', + 'currency' => 'devise', + 'date' => 'date', + ]; + } +} diff --git a/src/Infrastructure/Parser/Json/Cid4JsonInvoiceParser.php b/src/Infrastructure/Parser/Json/Cid4JsonInvoiceParser.php new file mode 100644 index 0000000..bed6c22 --- /dev/null +++ b/src/Infrastructure/Parser/Json/Cid4JsonInvoiceParser.php @@ -0,0 +1,23 @@ + 'nom_prenom', + 'amount' => 'price', + 'currency' => 'devise', + 'date' => 'date', + ]; + } +} diff --git a/src/Infrastructure/Repository/InvoiceRepository.php b/src/Infrastructure/Repository/InvoiceRepository.php new file mode 100644 index 0000000..0eff9c7 --- /dev/null +++ b/src/Infrastructure/Repository/InvoiceRepository.php @@ -0,0 +1,40 @@ +getEntityManager()->persist($invoice); + } + + public function update(Invoice $invoice): void + { + // Entity is already tracked by Doctrine UnitOfWork. + // Changes will be flushed on flush() call. + } + + public function flush(): void + { + $this->getEntityManager()->flush(); + } + + public function findByNameAndDate(string $name, DateTimeImmutable $date): ?Invoice + { + return $this->findOneBy(['name' => $name, 'date' => $date]); + } +} diff --git a/src/Repository/.gitignore b/src/Repository/.gitignore deleted file mode 100644 index e69de29..0000000 diff --git a/src/Repository/InvoiceRepository.php b/src/Repository/InvoiceRepository.php deleted file mode 100644 index 92d7dc2..0000000 --- a/src/Repository/InvoiceRepository.php +++ /dev/null @@ -1,18 +0,0 @@ -em = $em; - } - - public function parse(string $fp): void - { - if (str_contains($fp, 'json')) { //Pour les json - $f = file_get_contents($fp); - $d = preg_split("/\r\n|\n|\r/", $f); - $c = 0; - $m = ""; - $n = ""; - /** Tant qu'il y a une ligne */ - while(true){ - if(isset($d[$c])){ - if(str_contains($d[$c], "montant")){ - $m = explode(": ", $d[$c])[1]; - $m = substr($m, 0, strlen($m) - 1); - } - if(str_contains($d[$c], "nom")){ - $n = explode(": ", $d[$c])[1]; - $n = substr($n, 0, strlen($n) - 1); - } - if(str_contains($d[$c], "}")){ - $this->em->getConnection()->executeStatement( - "UPDATE invoice SET amount = {$m} WHERE name = '{$n}'" - ); - } - $c++; - }else{ - break; - } - } - } elseif (str_contains($fp, 'csv')) { //Pour les json - - - $d = array_map(function($r) { - return str_getcsv($r, "\t"); - }, file($fp)); - $c = 0; - while(true){ - if(isset($d[$c])){ - $this->em->getConnection()->executeStatement( - "UPDATE invoice SET amount = {$d[$c][0]} WHERE name = '{$d[$c][2]}'" - ); - $c++; - }else{ - break; - } - } - } - } -} diff --git a/symfony.lock b/symfony.lock index ec68ba0..e5d49a7 100644 --- a/symfony.lock +++ b/symfony.lock @@ -8,6 +8,15 @@ "ref": "64d8583af5ea57b7afa4aba4b159907f3a148b05" } }, + "doctrine/deprecations": { + "version": "1.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.0", + "ref": "fdd756167454623e21f1d769c5b814b243782a67" + } + }, "doctrine/doctrine-bundle": { "version": "2.13", "recipe": { @@ -105,5 +114,17 @@ "config/packages/routing.yaml", "config/routes.yaml" ] + }, + "symfony/validator": { + "version": "6.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "5.3", + "ref": "c32cfd98f714894c4f128bb99aa2530c1227603c" + }, + "files": [ + "config/packages/validator.yaml" + ] } } diff --git a/tests/InvoiceParserTest.php b/tests/InvoiceParserTest.php deleted file mode 100644 index 0058eeb..0000000 --- a/tests/InvoiceParserTest.php +++ /dev/null @@ -1,45 +0,0 @@ -entityManager = $this->createMock(EntityManagerInterface::class); - - $connection = $this->createMock(Connection::class); - $this->entityManager->method('getConnection')->willReturn($connection); - - $connection->expects($this->exactly(10))->method('executeStatement'); - - $invoiceParser = new InvoiceParser($this->entityManager); - - $invoiceParser->parse('data/invoices.json'); - } - - public function testParseCsv(): void - { - $this->entityManager = $this->createMock(EntityManagerInterface::class); - - $connection = $this->createMock(Connection::class); - $this->entityManager->method('getConnection')->willReturn($connection); - - $connection->expects($this->exactly(10))->method('executeStatement'); - - $invoiceParser = new InvoiceParser($this->entityManager); - - $invoiceParser->parse('data/invoices.csv'); - } - -} - diff --git a/tests/Unit/Domain/Entity/InvoiceTest.php b/tests/Unit/Domain/Entity/InvoiceTest.php new file mode 100644 index 0000000..9b727f8 --- /dev/null +++ b/tests/Unit/Domain/Entity/InvoiceTest.php @@ -0,0 +1,44 @@ +getName()); + self::assertSame(100.50, $invoice->getAmount()); + self::assertSame('EUR', $invoice->getCurrency()); + self::assertSame($date, $invoice->getDate()); + } + + public function testUpdateAmountAndCurrencyDoesNotChangeName(): void + { + $invoice = new Invoice('John Doe', 100.50, 'EUR', new DateTimeImmutable('2025-02-03')); + + $invoice->updateAmountAndCurrency(200.00, 'USD'); + + self::assertSame(200.00, $invoice->getAmount()); + self::assertSame('USD', $invoice->getCurrency()); + self::assertSame('John Doe', $invoice->getName()); + } + + public function testUpdateAmountAndCurrencyDoesNotChangeDate(): void + { + $date = new DateTimeImmutable('2025-02-03'); + $invoice = new Invoice('John Doe', 100.50, 'EUR', $date); + + $invoice->updateAmountAndCurrency(200.00, 'USD'); + + self::assertSame($date, $invoice->getDate()); + } +}