From 0bb0c0b42f0406a02ede340d8ff864e8f42ea6b5 Mon Sep 17 00:00:00 2001 From: team 1 Date: Thu, 12 Feb 2026 10:03:52 +0100 Subject: [PATCH] stash light --- .env | 10 + composer.json | 11 +- composer.lock | 2805 ++++++++++++++++- config/bundles.php | 5 + config/packages/doctrine.yaml | 54 + config/packages/doctrine_migrations.yaml | 6 + config/packages/property_info.yaml | 3 + config/packages/security.yaml | 47 + config/packages/twig.yaml | 6 + config/reference.php | 625 +++- config/routes/security.yaml | 3 + config/services.yaml | 26 +- migrations/Version20260211134954.php | 31 + migrations/Version20260211142601.php | 41 + migrations/Version20260211145114.php | 33 + migrations/Version20260211145121.php | 33 + migrations/Version20260211153201.php | 31 + src/Command/CreateUserCommand.php | 108 + src/Controller/Admin/DashboardController.php | 17 + src/Controller/Admin/DocumentController.php | 217 ++ src/Controller/Admin/IngestJobController.php | 57 + src/Controller/Admin/SecurityController.php | 33 + src/Entity/Document.php | 110 + src/Entity/DocumentVersion.php | 184 ++ src/Entity/IngestJob.php | 101 + src/Entity/User.php | 161 + src/Index/IndexConfiguration.php | 95 + src/Index/IndexMetaManager.php | 206 ++ src/Index/IndexStructureChangedException.php | 34 + src/Ingest/IngestFlow.php | 182 ++ src/Knowledge/ChunkManager.php | 186 ++ .../Ingest/KnowledgeIngestService.php | 100 +- src/Repository/DocumentVersionRepository.php | 32 + src/Repository/UserRepository.php | 43 + src/Service/DocumentService.php | 123 + src/Service/IngestJobService.php | 54 + src/Service/IngestOrchestrator.php | 130 + src/Service/LockService.php | 71 + src/Vector/VectorIndexBuilder.php | 164 + src/Vector/vector_ingest.py | 105 +- symfony.lock | 83 + templates/admin/base.html.twig | 49 + templates/admin/dashboard/index.html.twig | 21 + templates/admin/document/index.html.twig | 61 + templates/admin/document/new.html.twig | 23 + .../admin/document/new_version.html.twig | 29 + templates/admin/document/show.html.twig | 138 + templates/admin/job/index.html.twig | 109 + templates/admin/job/show.html.twig | 88 + templates/admin/security/login.html.twig | 36 + templates/base.html.twig | 16 + 51 files changed, 6864 insertions(+), 72 deletions(-) create mode 100644 config/packages/doctrine.yaml create mode 100644 config/packages/doctrine_migrations.yaml create mode 100644 config/packages/property_info.yaml create mode 100644 config/packages/security.yaml create mode 100644 config/packages/twig.yaml create mode 100644 config/routes/security.yaml create mode 100644 migrations/Version20260211134954.php create mode 100644 migrations/Version20260211142601.php create mode 100644 migrations/Version20260211145114.php create mode 100644 migrations/Version20260211145121.php create mode 100644 migrations/Version20260211153201.php create mode 100644 src/Command/CreateUserCommand.php create mode 100644 src/Controller/Admin/DashboardController.php create mode 100644 src/Controller/Admin/DocumentController.php create mode 100644 src/Controller/Admin/IngestJobController.php create mode 100644 src/Controller/Admin/SecurityController.php create mode 100644 src/Entity/Document.php create mode 100644 src/Entity/DocumentVersion.php create mode 100644 src/Entity/IngestJob.php create mode 100644 src/Entity/User.php create mode 100644 src/Index/IndexConfiguration.php create mode 100644 src/Index/IndexMetaManager.php create mode 100644 src/Index/IndexStructureChangedException.php create mode 100644 src/Ingest/IngestFlow.php create mode 100644 src/Knowledge/ChunkManager.php create mode 100644 src/Repository/DocumentVersionRepository.php create mode 100644 src/Repository/UserRepository.php create mode 100644 src/Service/DocumentService.php create mode 100644 src/Service/IngestJobService.php create mode 100644 src/Service/IngestOrchestrator.php create mode 100644 src/Service/LockService.php create mode 100644 src/Vector/VectorIndexBuilder.php create mode 100644 templates/admin/base.html.twig create mode 100644 templates/admin/dashboard/index.html.twig create mode 100644 templates/admin/document/index.html.twig create mode 100644 templates/admin/document/new.html.twig create mode 100644 templates/admin/document/new_version.html.twig create mode 100644 templates/admin/document/show.html.twig create mode 100644 templates/admin/job/index.html.twig create mode 100644 templates/admin/job/show.html.twig create mode 100644 templates/admin/security/login.html.twig create mode 100644 templates/base.html.twig diff --git a/.env b/.env index f048212..fdb1d7c 100644 --- a/.env +++ b/.env @@ -34,3 +34,13 @@ AI_DEBUG=false AI_LOG_PROMPT=false AI_LOG_CONTEXT=false ###< AI Agent Debug ### + +###> doctrine/doctrine-bundle ### +# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url +# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml +# +# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data_%kernel.environment%.db" +# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8.0.32&charset=utf8mb4" +# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4" +DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&charset=utf8" +###< doctrine/doctrine-bundle ### diff --git a/composer.json b/composer.json index 1681d50..6776b6c 100644 --- a/composer.json +++ b/composer.json @@ -8,12 +8,17 @@ "ext-ctype": "*", "ext-curl": "*", "ext-iconv": "*", + "doctrine/doctrine-bundle": "^2.18", + "doctrine/doctrine-migrations-bundle": "^3.7", + "doctrine/orm": "^3.6", "symfony/console": "^7.4", "symfony/dotenv": "^7.4", "symfony/flex": "^2", "symfony/framework-bundle": "^7.4", "symfony/monolog-bundle": "^4.0", "symfony/runtime": "^7.4", + "symfony/security-bundle": "7.4.*", + "symfony/twig-bundle": "7.4.*", "symfony/uid": "7.4.*", "symfony/yaml": "^7.4" }, @@ -53,7 +58,11 @@ "extra": { "symfony": { "allow-contrib": false, - "require": "7.4.*" + "require": "7.4.*", + "docker": false } + }, + "require-dev": { + "symfony/maker-bundle": "^1.66" } } diff --git a/composer.lock b/composer.lock index 43393b8..1db89f5 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,1121 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "649e732d1e7a4117fca1150811de38a6", + "content-hash": "6eb5735f5c0cf7cb184127323d55e384", "packages": [ + { + "name": "doctrine/collections", + "version": "2.6.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/collections.git", + "reference": "7713da39d8e237f28411d6a616a3dce5e20d5de2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/collections/zipball/7713da39d8e237f28411d6a616a3dce5e20d5de2", + "reference": "7713da39d8e237f28411d6a616a3dce5e20d5de2", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1", + "php": "^8.1", + "symfony/polyfill-php84": "^1.30" + }, + "require-dev": { + "doctrine/coding-standard": "^14", + "ext-json": "*", + "phpstan/phpstan": "^2.1.30", + "phpstan/phpstan-phpunit": "^2.0.7", + "phpunit/phpunit": "^10.5.58 || ^11.5.42 || ^12.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Collections\\": "src" + } + }, + "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 Collections library that adds additional functionality on top of PHP arrays.", + "homepage": "https://www.doctrine-project.org/projects/collections.html", + "keywords": [ + "array", + "collections", + "iterators", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/collections/issues", + "source": "https://github.com/doctrine/collections/tree/2.6.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%2Fcollections", + "type": "tidelift" + } + ], + "time": "2026-01-15T10:01:58+00:00" + }, + { + "name": "doctrine/dbal", + "version": "4.4.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/dbal.git", + "reference": "3d544473fb93f5c25b483ea4f4ce99f8c4d9d44c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/3d544473fb93f5c25b483ea4f4ce99f8c4d9d44c", + "reference": "3d544473fb93f5c25b483ea4f4ce99f8c4d9d44c", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.1.5", + "php": "^8.2", + "psr/cache": "^1|^2|^3", + "psr/log": "^1|^2|^3" + }, + "require-dev": { + "doctrine/coding-standard": "14.0.0", + "fig/log-test": "^1", + "jetbrains/phpstorm-stubs": "2023.2", + "phpstan/phpstan": "2.1.30", + "phpstan/phpstan-phpunit": "2.0.7", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "11.5.23", + "slevomat/coding-standard": "8.24.0", + "squizlabs/php_codesniffer": "4.0.0", + "symfony/cache": "^6.3.8|^7.0|^8.0", + "symfony/console": "^5.4|^6.3|^7.0|^8.0" + }, + "suggest": { + "symfony/console": "For helpful console commands such as SQL execution and import of files." + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\DBAL\\": "src" + } + }, + "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" + } + ], + "description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.", + "homepage": "https://www.doctrine-project.org/projects/dbal.html", + "keywords": [ + "abstraction", + "database", + "db2", + "dbal", + "mariadb", + "mssql", + "mysql", + "oci8", + "oracle", + "pdo", + "pgsql", + "postgresql", + "queryobject", + "sasql", + "sql", + "sqlite", + "sqlserver", + "sqlsrv" + ], + "support": { + "issues": "https://github.com/doctrine/dbal/issues", + "source": "https://github.com/doctrine/dbal/tree/4.4.1" + }, + "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%2Fdbal", + "type": "tidelift" + } + ], + "time": "2025-12-04T10:11:03+00:00" + }, + { + "name": "doctrine/deprecations", + "version": "1.1.6", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca" + }, + "dist": { + "type": "zip", + "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 || ^14", + "phpstan/phpstan": "1.4.10 || 2.1.30", + "phpstan/phpstan-phpunit": "^1.0 || ^2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0", + "psr/log": "^1 || ^2 || ^3" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.6" + }, + "time": "2026-02-07T07:09:04+00:00" + }, + { + "name": "doctrine/doctrine-bundle", + "version": "2.18.2", + "source": { + "type": "git", + "url": "https://github.com/doctrine/DoctrineBundle.git", + "reference": "0ff098b29b8b3c68307c8987dcaed7fd829c6546" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/0ff098b29b8b3c68307c8987dcaed7fd829c6546", + "reference": "0ff098b29b8b3c68307c8987dcaed7fd829c6546", + "shasum": "" + }, + "require": { + "doctrine/dbal": "^3.7.0 || ^4.0", + "doctrine/deprecations": "^1.0", + "doctrine/persistence": "^3.1 || ^4", + "doctrine/sql-formatter": "^1.0.1", + "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", + "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/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": "^10.5.53 || ^12.3.10", + "psr/log": "^1.1.4 || ^2.0 || ^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.", + "ext-pdo": "*", + "symfony/web-profiler-bundle": "To use the data collector." + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Doctrine\\Bundle\\DoctrineBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + }, + { + "name": "Doctrine Project", + "homepage": "https://www.doctrine-project.org/" + } + ], + "description": "Symfony DoctrineBundle", + "homepage": "https://www.doctrine-project.org", + "keywords": [ + "database", + "dbal", + "orm", + "persistence" + ], + "support": { + "issues": "https://github.com/doctrine/DoctrineBundle/issues", + "source": "https://github.com/doctrine/DoctrineBundle/tree/2.18.2" + }, + "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%2Fdoctrine-bundle", + "type": "tidelift" + } + ], + "time": "2025-12-20T21:35:32+00:00" + }, + { + "name": "doctrine/doctrine-migrations-bundle", + "version": "3.7.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/DoctrineMigrationsBundle.git", + "reference": "1e380c6dd8ac8488217f39cff6b77e367f1a644b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/DoctrineMigrationsBundle/zipball/1e380c6dd8ac8488217f39cff6b77e367f1a644b", + "reference": "1e380c6dd8ac8488217f39cff6b77e367f1a644b", + "shasum": "" + }, + "require": { + "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 || ^8.0" + }, + "require-dev": { + "composer/semver": "^3.0", + "doctrine/coding-standard": "^12 || ^14", + "doctrine/orm": "^2.6 || ^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 || ^8", + "symfony/var-exporter": "^5.4 || ^6 || ^7 || ^8" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Doctrine\\Bundle\\MigrationsBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Doctrine Project", + "homepage": "https://www.doctrine-project.org" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony DoctrineMigrationsBundle", + "homepage": "https://www.doctrine-project.org", + "keywords": [ + "dbal", + "migrations", + "schema" + ], + "support": { + "issues": "https://github.com/doctrine/DoctrineMigrationsBundle/issues", + "source": "https://github.com/doctrine/DoctrineMigrationsBundle/tree/3.7.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%2Fdoctrine-migrations-bundle", + "type": "tidelift" + } + ], + "time": "2025-11-15T19:02:59+00:00" + }, + { + "name": "doctrine/event-manager", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/event-manager.git", + "reference": "dda33921b198841ca8dbad2eaa5d4d34769d18cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/event-manager/zipball/dda33921b198841ca8dbad2eaa5d4d34769d18cf", + "reference": "dda33921b198841ca8dbad2eaa5d4d34769d18cf", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "conflict": { + "doctrine/common": "<2.9" + }, + "require-dev": { + "doctrine/coding-standard": "^14", + "phpdocumentor/guides-cli": "^1.4", + "phpstan/phpstan": "^2.1.32", + "phpunit/phpunit": "^10.5.58" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\": "src" + } + }, + "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" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.", + "homepage": "https://www.doctrine-project.org/projects/event-manager.html", + "keywords": [ + "event", + "event dispatcher", + "event manager", + "event system", + "events" + ], + "support": { + "issues": "https://github.com/doctrine/event-manager/issues", + "source": "https://github.com/doctrine/event-manager/tree/2.1.1" + }, + "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%2Fevent-manager", + "type": "tidelift" + } + ], + "time": "2026-01-29T07:11:08+00:00" + }, + { + "name": "doctrine/inflector", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "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\\": "src" + } + }, + "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 Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", + "homepage": "https://www.doctrine-project.org/projects/inflector.html", + "keywords": [ + "inflection", + "inflector", + "lowercase", + "manipulation", + "php", + "plural", + "singular", + "strings", + "uppercase", + "words" + ], + "support": { + "issues": "https://github.com/doctrine/inflector/issues", + "source": "https://github.com/doctrine/inflector/tree/2.1.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%2Finflector", + "type": "tidelift" + } + ], + "time": "2025-08-10T19:31:58+00:00" + }, + { + "name": "doctrine/instantiator", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^11", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^1.9.4", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.5.27", + "vimeo/psalm": "^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/2.0.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%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2022-12-30T00:23:10+00:00" + }, + { + "name": "doctrine/lexer", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5", + "psalm/plugin-phpunit": "^0.18.3", + "vimeo/psalm": "^5.21" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "src" + } + }, + "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": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/3.0.1" + }, + "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%2Flexer", + "type": "tidelift" + } + ], + "time": "2024-02-05T11:56:58+00:00" + }, + { + "name": "doctrine/migrations", + "version": "3.9.6", + "source": { + "type": "git", + "url": "https://github.com/doctrine/migrations.git", + "reference": "ffd8355cdd8505fc650d9604f058bf62aedd80a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/migrations/zipball/ffd8355cdd8505fc650d9604f058bf62aedd80a1", + "reference": "ffd8355cdd8505fc650d9604f058bf62aedd80a1", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2", + "doctrine/dbal": "^3.6 || ^4", + "doctrine/deprecations": "^0.5.3 || ^1", + "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 || ^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": "^14", + "doctrine/orm": "^2.13 || ^3", + "doctrine/persistence": "^2 || ^3 || ^4", + "doctrine/sql-formatter": "^1.0", + "ext-pdo_sqlite": "*", + "fig/log-test": "^1", + "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.", + "symfony/yaml": "Allows the use of yaml for migration configuration files." + }, + "bin": [ + "bin/doctrine-migrations" + ], + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Migrations\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Michael Simonson", + "email": "contact@mikesimonson.com" + } + ], + "description": "PHP Doctrine Migrations project offer additional functionality on top of the database abstraction layer (DBAL) for versioning your database schema and easily deploying changes to it. It is a very easy to use and a powerful tool.", + "homepage": "https://www.doctrine-project.org/projects/migrations.html", + "keywords": [ + "database", + "dbal", + "migrations" + ], + "support": { + "issues": "https://github.com/doctrine/migrations/issues", + "source": "https://github.com/doctrine/migrations/tree/3.9.6" + }, + "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%2Fmigrations", + "type": "tidelift" + } + ], + "time": "2026-02-11T06:46:11+00:00" + }, + { + "name": "doctrine/orm", + "version": "3.6.2", + "source": { + "type": "git", + "url": "https://github.com/doctrine/orm.git", + "reference": "4262eb495b4d2a53b45de1ac58881e0091f2970f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/orm/zipball/4262eb495b4d2a53b45de1ac58881e0091f2970f", + "reference": "4262eb495b4d2a53b45de1ac58881e0091f2970f", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2", + "doctrine/collections": "^2.2", + "doctrine/dbal": "^3.8.2 || ^4", + "doctrine/deprecations": "^0.5.3 || ^1", + "doctrine/event-manager": "^1.2 || ^2", + "doctrine/inflector": "^1.4 || ^2.0", + "doctrine/instantiator": "^1.3 || ^2", + "doctrine/lexer": "^3", + "doctrine/persistence": "^3.3.1 || ^4", + "ext-ctype": "*", + "php": "^8.1", + "psr/cache": "^1 || ^2 || ^3", + "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": "^14.0", + "phpbench/phpbench": "^1.0", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "2.1.23", + "phpstan/phpstan-deprecation-rules": "^2", + "phpunit/phpunit": "^10.5.0 || ^11.5", + "psr/log": "^1 || ^2 || ^3", + "symfony/cache": "^5.4 || ^6.2 || ^7.0 || ^8.0" + }, + "suggest": { + "ext-dom": "Provides support for XSD validation for XML mapping files", + "symfony/cache": "Provides cache support for Setup Tool with doctrine/cache 2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\ORM\\": "src" + } + }, + "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": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "Object-Relational-Mapper for PHP", + "homepage": "https://www.doctrine-project.org/projects/orm.html", + "keywords": [ + "database", + "orm" + ], + "support": { + "issues": "https://github.com/doctrine/orm/issues", + "source": "https://github.com/doctrine/orm/tree/3.6.2" + }, + "time": "2026-01-30T21:41:41+00:00" + }, + { + "name": "doctrine/persistence", + "version": "4.1.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/persistence.git", + "reference": "b9c49ad3558bb77ef973f4e173f2e9c2eca9be09" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/persistence/zipball/b9c49ad3558bb77ef973f4e173f2e9c2eca9be09", + "reference": "b9c49ad3558bb77ef973f4e173f2e9c2eca9be09", + "shasum": "" + }, + "require": { + "doctrine/event-manager": "^1 || ^2", + "php": "^8.1", + "psr/cache": "^1.0 || ^2.0 || ^3.0" + }, + "require-dev": { + "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", + "symfony/finder": "^4.4 || ^5.4 || ^6.0 || ^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Persistence\\": "src/Persistence" + } + }, + "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" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "The Doctrine Persistence project is a set of shared interfaces and functionality that the different Doctrine object mappers share.", + "homepage": "https://www.doctrine-project.org/projects/persistence.html", + "keywords": [ + "mapper", + "object", + "odm", + "orm", + "persistence" + ], + "support": { + "issues": "https://github.com/doctrine/persistence/issues", + "source": "https://github.com/doctrine/persistence/tree/4.1.1" + }, + "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%2Fpersistence", + "type": "tidelift" + } + ], + "time": "2025-10-16T20:13:18+00:00" + }, + { + "name": "doctrine/sql-formatter", + "version": "1.5.4", + "source": { + "type": "git", + "url": "https://github.com/doctrine/sql-formatter.git", + "reference": "9563949f5cd3bd12a17d12fb980528bc141c5806" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/sql-formatter/zipball/9563949f5cd3bd12a17d12fb980528bc141c5806", + "reference": "9563949f5cd3bd12a17d12fb980528bc141c5806", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "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" + ], + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\SqlFormatter\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jeremy Dorn", + "email": "jeremy@jeremydorn.com", + "homepage": "https://jeremydorn.com/" + } + ], + "description": "a PHP SQL highlighting library", + "homepage": "https://github.com/doctrine/sql-formatter/", + "keywords": [ + "highlight", + "sql" + ], + "support": { + "issues": "https://github.com/doctrine/sql-formatter/issues", + "source": "https://github.com/doctrine/sql-formatter/tree/1.5.4" + }, + "time": "2026-02-08T16:21:46+00:00" + }, { "name": "monolog/monolog", "version": "3.10.0", @@ -158,6 +1271,54 @@ }, "time": "2021-02-03T23:26:27+00:00" }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" + }, { "name": "psr/container", "version": "2.0.2", @@ -491,6 +1652,84 @@ ], "time": "2025-03-13T15:25:07+00:00" }, + { + "name": "symfony/clock", + "version": "v7.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/clock.git", + "reference": "9169f24776edde469914c1e7a1442a50f7a4e110" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/clock/zipball/9169f24776edde469914c1e7a1442a50f7a4e110", + "reference": "9169f24776edde469914c1e7a1442a50f7a4e110", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/clock": "^1.0", + "symfony/polyfill-php83": "^1.28" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/now.php" + ], + "psr-4": { + "Symfony\\Component\\Clock\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Decouples applications from the system clock", + "homepage": "https://symfony.com", + "keywords": [ + "clock", + "psr20", + "time" + ], + "support": { + "source": "https://github.com/symfony/clock/tree/v7.4.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-11-12T15:39:26+00:00" + }, { "name": "symfony/config", "version": "v7.4.3", @@ -819,6 +2058,119 @@ ], "time": "2024-09-25T14:21:43+00:00" }, + { + "name": "symfony/doctrine-bridge", + "version": "v7.4.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/doctrine-bridge.git", + "reference": "3408d9fb7bda6c8db9f3e4099863c9017bcbc62d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/3408d9fb7bda6c8db9f3e4099863c9017bcbc62d", + "reference": "3408d9fb7bda6c8db9f3e4099863c9017bcbc62d", + "shasum": "" + }, + "require": { + "doctrine/event-manager": "^2", + "doctrine/persistence": "^3.1|^4", + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "doctrine/collections": "<1.8", + "doctrine/dbal": "<3.6", + "doctrine/lexer": "<1.1", + "doctrine/orm": "<2.15", + "symfony/cache": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/form": "<6.4.6|>=7,<7.0.6", + "symfony/http-foundation": "<6.4", + "symfony/http-kernel": "<6.4", + "symfony/lock": "<6.4", + "symfony/messenger": "<6.4", + "symfony/property-info": "<6.4", + "symfony/security-bundle": "<6.4", + "symfony/security-core": "<6.4", + "symfony/validator": "<7.4" + }, + "require-dev": { + "doctrine/collections": "^1.8|^2.0", + "doctrine/data-fixtures": "^1.1|^2", + "doctrine/dbal": "^3.6|^4", + "doctrine/orm": "^2.15|^3", + "psr/log": "^1|^2|^3", + "symfony/cache": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/doctrine-messenger": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/form": "^7.2|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/security-core": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4|^7.0|^8.0", + "symfony/type-info": "^7.1.8|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", + "symfony/validator": "^7.4|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "type": "symfony-bridge", + "autoload": { + "psr-4": { + "Symfony\\Bridge\\Doctrine\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides integration for Doctrine with various Symfony components", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/doctrine-bridge/tree/v7.4.4" + }, + "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-20T16:42:42+00:00" + }, { "name": "symfony/dotenv", "version": "v7.4.0", @@ -1872,6 +3224,82 @@ ], "time": "2025-12-08T08:00:13+00:00" }, + { + "name": "symfony/password-hasher", + "version": "v7.4.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/password-hasher.git", + "reference": "ab8e0ef42483f31c417c82ecfcf7be7b91d784fe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/password-hasher/zipball/ab8e0ef42483f31c417c82ecfcf7be7b91d784fe", + "reference": "ab8e0ef42483f31c417c82ecfcf7be7b91d784fe", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "conflict": { + "symfony/security-core": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/security-core": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PasswordHasher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Robin Chalas", + "email": "robin.chalas@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides password hashing utilities", + "homepage": "https://symfony.com", + "keywords": [ + "hashing", + "password" + ], + "support": { + "source": "https://github.com/symfony/password-hasher/tree/v7.4.4" + }, + "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-01T22:13:48+00:00" + }, { "name": "symfony/polyfill-ctype", "version": "v1.33.0", @@ -2207,6 +3635,86 @@ ], "time": "2024-12-23T08:48:59+00:00" }, + { + "name": "symfony/polyfill-php83", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "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\\Php83\\": "" + }, + "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.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-08T02:45:35+00:00" + }, { "name": "symfony/polyfill-php84", "version": "v1.33.0", @@ -2450,6 +3958,177 @@ ], "time": "2024-09-09T11:45:10+00:00" }, + { + "name": "symfony/property-access", + "version": "v7.4.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/property-access.git", + "reference": "fa49bf1ca8fce1ba0e2dba4e4658554cfb9364b1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/property-access/zipball/fa49bf1ca8fce1ba0e2dba4e4658554cfb9364b1", + "reference": "fa49bf1ca8fce1ba0e2dba4e4658554cfb9364b1", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/property-info": "^6.4.32|~7.3.10|^7.4.4|^8.0.4" + }, + "require-dev": { + "symfony/cache": "^6.4|^7.0|^8.0", + "symfony/var-exporter": "^6.4.1|^7.0.1|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PropertyAccess\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides functions to read and write from/to an object or array using a simple string notation", + "homepage": "https://symfony.com", + "keywords": [ + "access", + "array", + "extraction", + "index", + "injection", + "object", + "property", + "property-path", + "reflection" + ], + "support": { + "source": "https://github.com/symfony/property-access/tree/v7.4.4" + }, + "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-05T08:47:25+00:00" + }, + { + "name": "symfony/property-info", + "version": "v7.4.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/property-info.git", + "reference": "1c9d326bd69602561e2ea467a16c09b5972eee21" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/property-info/zipball/1c9d326bd69602561e2ea467a16c09b5972eee21", + "reference": "1c9d326bd69602561e2ea467a16c09b5972eee21", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/string": "^6.4|^7.0|^8.0", + "symfony/type-info": "~7.3.10|^7.4.4|^8.0.4" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "<5.2|>=6", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/cache": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/serializer": "<6.4" + }, + "require-dev": { + "phpdocumentor/reflection-docblock": "^5.2", + "phpstan/phpdoc-parser": "^1.0|^2.0", + "symfony/cache": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PropertyInfo\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Dunglas", + "email": "dunglas@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Extracts information about PHP class' properties using metadata of popular sources", + "homepage": "https://symfony.com", + "keywords": [ + "doctrine", + "phpdoc", + "property", + "symfony", + "type", + "validator" + ], + "support": { + "source": "https://github.com/symfony/property-info/tree/v7.4.5" + }, + "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-27T16:16:02+00:00" + }, { "name": "symfony/routing", "version": "v7.4.3", @@ -2618,6 +4297,375 @@ ], "time": "2025-12-05T14:04:53+00:00" }, + { + "name": "symfony/security-bundle", + "version": "v7.4.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/security-bundle.git", + "reference": "7281b644c76985ddf3927f5e65152b0cc29d175b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/security-bundle/zipball/7281b644c76985ddf3927f5e65152b0cc29d175b", + "reference": "7281b644c76985ddf3927f5e65152b0cc29d175b", + "shasum": "" + }, + "require": { + "composer-runtime-api": ">=2.1", + "ext-xml": "*", + "php": ">=8.2", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^6.4.11|^7.1.4|^8.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4.13|^7.1.6|^8.0", + "symfony/password-hasher": "^6.4|^7.0|^8.0", + "symfony/security-core": "^7.4|^8.0", + "symfony/security-csrf": "^6.4|^7.0|^8.0", + "symfony/security-http": "^7.4|^8.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/browser-kit": "<6.4", + "symfony/console": "<6.4", + "symfony/framework-bundle": "<6.4", + "symfony/http-client": "<6.4", + "symfony/ldap": "<6.4", + "symfony/serializer": "<6.4", + "symfony/twig-bundle": "<6.4", + "symfony/validator": "<6.4" + }, + "require-dev": { + "symfony/asset": "^6.4|^7.0|^8.0", + "symfony/browser-kit": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/css-selector": "^6.4|^7.0|^8.0", + "symfony/dom-crawler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/form": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4.13|^7.1.6|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/ldap": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0", + "symfony/runtime": "^6.4.13|^7.1.6|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4|^7.0|^8.0", + "symfony/twig-bridge": "^6.4|^7.0|^8.0", + "symfony/twig-bundle": "^6.4|^7.0|^8.0", + "symfony/validator": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0", + "twig/twig": "^3.15", + "web-token/jwt-library": "^3.3.2|^4.0" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Symfony\\Bundle\\SecurityBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a tight integration of the Security component into the Symfony full-stack framework", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/security-bundle/tree/v7.4.4" + }, + "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-10T13:56:23+00:00" + }, + { + "name": "symfony/security-core", + "version": "v7.4.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/security-core.git", + "reference": "958a70725a8d669bec6721f4cd318d209712e944" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/security-core/zipball/958a70725a8d669bec6721f4cd318d209712e944", + "reference": "958a70725a8d669bec6721f4cd318d209712e944", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/event-dispatcher-contracts": "^2.5|^3", + "symfony/password-hasher": "^6.4|^7.0|^8.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/http-foundation": "<6.4", + "symfony/ldap": "<6.4", + "symfony/translation": "<6.4.3|>=7.0,<7.0.3", + "symfony/validator": "<6.4" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "psr/container": "^1.1|^2.0", + "psr/log": "^1|^2|^3", + "symfony/cache": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/ldap": "^6.4|^7.0|^8.0", + "symfony/string": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4.3|^7.0.3|^8.0", + "symfony/validator": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Security\\Core\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Security Component - Core Library", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/security-core/tree/v7.4.4" + }, + "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-14T09:36:49+00:00" + }, + { + "name": "symfony/security-csrf", + "version": "v7.4.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/security-csrf.git", + "reference": "06a2a2f90f355b8b4ec23685fa6ceff8d5dc41cc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/security-csrf/zipball/06a2a2f90f355b8b4ec23685fa6ceff8d5dc41cc", + "reference": "06a2a2f90f355b8b4ec23685fa6ceff8d5dc41cc", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/security-core": "^6.4|^7.0|^8.0" + }, + "conflict": { + "symfony/http-foundation": "<6.4" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Security\\Csrf\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Security Component - CSRF Library", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/security-csrf/tree/v7.4.4" + }, + "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-14T10:11:16+00:00" + }, + { + "name": "symfony/security-http", + "version": "v7.4.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/security-http.git", + "reference": "9d41a473637bf5d074c5f5a73177fd9d769407fd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/security-http/zipball/9d41a473637bf5d074c5f5a73177fd9d769407fd", + "reference": "9d41a473637bf5d074c5f5a73177fd9d769407fd", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/polyfill-mbstring": "~1.0", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/security-core": "^7.3|^8.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/clock": "<6.4", + "symfony/http-client-contracts": "<3.0", + "symfony/security-bundle": "<6.4", + "symfony/security-csrf": "<6.4" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/cache": "^6.4|^7.0|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/http-client-contracts": "^3.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/security-csrf": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4|^7.0|^8.0", + "web-token/jwt-library": "^3.3.2|^4.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Security\\Http\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Security Component - HTTP Integration", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/security-http/tree/v7.4.4" + }, + "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-14T10:11:16+00:00" + }, { "name": "symfony/service-contracts", "version": "v3.6.1", @@ -2705,6 +4753,72 @@ ], "time": "2025-07-15T11:30:57+00:00" }, + { + "name": "symfony/stopwatch", + "version": "v7.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/stopwatch.git", + "reference": "8a24af0a2e8a872fb745047180649b8418303084" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/8a24af0a2e8a872fb745047180649b8418303084", + "reference": "8a24af0a2e8a872fb745047180649b8418303084", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/service-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Stopwatch\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a way to profile code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/stopwatch/tree/v7.4.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-08-04T07:05:15+00:00" + }, { "name": "symfony/string", "version": "v7.4.0", @@ -2797,17 +4911,387 @@ "time": "2025-11-27T13:27:24+00:00" }, { - "name": "symfony/uid", - "version": "v7.4.0", + "name": "symfony/translation-contracts", + "version": "v3.6.1", "source": { "type": "git", - "url": "https://github.com/symfony/uid.git", - "reference": "2498e9f81b7baa206f44de583f2f48350b90142c" + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "65a8bc82080447fae78373aa10f8d13b38338977" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/uid/zipball/2498e9f81b7baa206f44de583f2f48350b90142c", - "reference": "2498e9f81b7baa206f44de583f2f48350b90142c", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/65a8bc82080447fae78373aa10f8d13b38338977", + "reference": "65a8bc82080447fae78373aa10f8d13b38338977", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-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.6.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": "2025-07-15T13:41:35+00:00" + }, + { + "name": "symfony/twig-bridge", + "version": "v7.4.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/twig-bridge.git", + "reference": "f2dd26b604e856476ef7e0efa4568bc07eb7ddc8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/f2dd26b604e856476ef7e0efa4568bc07eb7ddc8", + "reference": "f2dd26b604e856476ef7e0efa4568bc07eb7ddc8", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/translation-contracts": "^2.5|^3", + "twig/twig": "^3.21" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "<5.2|>=6", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/console": "<6.4", + "symfony/form": "<6.4.32|>7,<7.3.10|>7.4,<7.4.4|>8.0,<8.0.4", + "symfony/http-foundation": "<6.4", + "symfony/http-kernel": "<6.4", + "symfony/mime": "<6.4", + "symfony/serializer": "<6.4", + "symfony/translation": "<6.4", + "symfony/workflow": "<6.4" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3|^4", + "league/html-to-markdown": "^5.0", + "phpdocumentor/reflection-docblock": "^5.2", + "symfony/asset": "^6.4|^7.0|^8.0", + "symfony/asset-mapper": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/emoji": "^7.1|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/form": "^6.4.32|~7.3.10|^7.4.4|^8.0.4", + "symfony/html-sanitizer": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^7.3|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/polyfill-intl-icu": "~1.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/security-acl": "^2.8|^3.0", + "symfony/security-core": "^6.4|^7.0|^8.0", + "symfony/security-csrf": "^6.4|^7.0|^8.0", + "symfony/security-http": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4.3|^7.0.3|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4|^7.0|^8.0", + "symfony/validator": "^6.4|^7.0|^8.0", + "symfony/web-link": "^6.4|^7.0|^8.0", + "symfony/workflow": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0", + "twig/cssinliner-extra": "^3", + "twig/inky-extra": "^3", + "twig/markdown-extra": "^3" + }, + "type": "symfony-bridge", + "autoload": { + "psr-4": { + "Symfony\\Bridge\\Twig\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides integration for Twig with various Symfony components", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/twig-bridge/tree/v7.4.5" + }, + "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-27T08:59:58+00:00" + }, + { + "name": "symfony/twig-bundle", + "version": "v7.4.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/twig-bundle.git", + "reference": "e8829e02ff96a391ed0703bac9e7ff0537480b6b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/twig-bundle/zipball/e8829e02ff96a391ed0703bac9e7ff0537480b6b", + "reference": "e8829e02ff96a391ed0703bac9e7ff0537480b6b", + "shasum": "" + }, + "require": { + "composer-runtime-api": ">=2.1", + "php": ">=8.2", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4.13|^7.1.6|^8.0", + "symfony/twig-bridge": "^7.3|^8.0", + "twig/twig": "^3.12" + }, + "conflict": { + "symfony/framework-bundle": "<6.4", + "symfony/translation": "<6.4" + }, + "require-dev": { + "symfony/asset": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/form": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4.13|^7.1.6|^8.0", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/runtime": "^6.4.13|^7.1.6", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4|^7.0|^8.0", + "symfony/web-link": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Symfony\\Bundle\\TwigBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a tight integration of Twig into the Symfony full-stack framework", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/twig-bundle/tree/v7.4.4" + }, + "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-06T12:34:24+00:00" + }, + { + "name": "symfony/type-info", + "version": "v7.4.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/type-info.git", + "reference": "f83c725e72b39b2704b9d6fc85070ad6ac7a5889" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/type-info/zipball/f83c725e72b39b2704b9d6fc85070ad6ac7a5889", + "reference": "f83c725e72b39b2704b9d6fc85070ad6ac7a5889", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "phpstan/phpdoc-parser": "<1.30" + }, + "require-dev": { + "phpstan/phpdoc-parser": "^1.30|^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\TypeInfo\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mathias Arlaud", + "email": "mathias.arlaud@gmail.com" + }, + { + "name": "Baptiste LEDUC", + "email": "baptiste.leduc@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Extracts PHP types information.", + "homepage": "https://symfony.com", + "keywords": [ + "PHPStan", + "phpdoc", + "symfony", + "type" + ], + "support": { + "source": "https://github.com/symfony/type-info/tree/v7.4.4" + }, + "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-09T12:14:21+00:00" + }, + { + "name": "symfony/uid", + "version": "v7.4.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/uid.git", + "reference": "7719ce8aba76be93dfe249192f1fbfa52c588e36" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/uid/zipball/7719ce8aba76be93dfe249192f1fbfa52c588e36", + "reference": "7719ce8aba76be93dfe249192f1fbfa52c588e36", "shasum": "" }, "require": { @@ -2852,7 +5336,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/uid/tree/v7.4.0" + "source": "https://github.com/symfony/uid/tree/v7.4.4" }, "funding": [ { @@ -2872,7 +5356,7 @@ "type": "tidelift" } ], - "time": "2025-09-25T11:02:55+00:00" + "time": "2026-01-03T23:30:35+00:00" }, { "name": "symfony/var-dumper", @@ -3117,9 +5601,310 @@ } ], "time": "2025-12-04T18:11:45+00:00" + }, + { + "name": "twig/twig", + "version": "v3.23.0", + "source": { + "type": "git", + "url": "https://github.com/twigphp/Twig.git", + "reference": "a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9", + "reference": "a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9", + "shasum": "" + }, + "require": { + "php": ">=8.1.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-mbstring": "^1.3" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "psr/container": "^1.0|^2.0", + "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/Resources/core.php", + "src/Resources/debug.php", + "src/Resources/escaper.php", + "src/Resources/string_loader.php" + ], + "psr-4": { + "Twig\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + }, + { + "name": "Twig Team", + "role": "Contributors" + }, + { + "name": "Armin Ronacher", + "email": "armin.ronacher@active-4.com", + "role": "Project Founder" + } + ], + "description": "Twig, the flexible, fast, and secure template language for PHP", + "homepage": "https://twig.symfony.com", + "keywords": [ + "templating" + ], + "support": { + "issues": "https://github.com/twigphp/Twig/issues", + "source": "https://github.com/twigphp/Twig/tree/v3.23.0" + }, + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/twig/twig", + "type": "tidelift" + } + ], + "time": "2026-01-23T21:00:41+00:00" + } + ], + "packages-dev": [ + { + "name": "nikic/php-parser", + "version": "v5.7.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" + }, + "time": "2025-12-06T11:56:16+00:00" + }, + { + "name": "symfony/maker-bundle", + "version": "v1.66.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/maker-bundle.git", + "reference": "b5b4afa2a570b926682e9f34615a6766dd560ff4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/maker-bundle/zipball/b5b4afa2a570b926682e9f34615a6766dd560ff4", + "reference": "b5b4afa2a570b926682e9f34615a6766dd560ff4", + "shasum": "" + }, + "require": { + "doctrine/inflector": "^2.0", + "nikic/php-parser": "^5.0", + "php": ">=8.1", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/deprecation-contracts": "^2.2|^3", + "symfony/filesystem": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0" + }, + "conflict": { + "doctrine/doctrine-bundle": "<2.10", + "doctrine/orm": "<2.15" + }, + "require-dev": { + "composer/semver": "^3.0", + "doctrine/doctrine-bundle": "^2.10|^3.0", + "doctrine/orm": "^2.15|^3", + "doctrine/persistence": "^3.1|^4.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/phpunit-bridge": "^6.4.1|^7.0|^8.0", + "symfony/security-core": "^6.4|^7.0|^8.0", + "symfony/security-http": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0", + "twig/twig": "^3.0|^4.x-dev" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Bundle\\MakerBundle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Maker helps you create empty commands, controllers, form classes, tests and more so you can forget about writing boilerplate code.", + "homepage": "https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html", + "keywords": [ + "code generator", + "dev", + "generator", + "scaffold", + "scaffolding" + ], + "support": { + "issues": "https://github.com/symfony/maker-bundle/issues", + "source": "https://github.com/symfony/maker-bundle/tree/v1.66.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-02-09T08:55:54+00:00" + }, + { + "name": "symfony/process", + "version": "v7.4.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "608476f4604102976d687c483ac63a79ba18cc97" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/608476f4604102976d687c483ac63a79ba18cc97", + "reference": "608476f4604102976d687c483ac63a79ba18cc97", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v7.4.5" + }, + "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-26T15:07:59+00:00" } ], - "packages-dev": [], "aliases": [], "minimum-stability": "stable", "stability-flags": {}, diff --git a/config/bundles.php b/config/bundles.php index 5b11b41..4193f5d 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -3,4 +3,9 @@ return [ Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true], Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], + Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], + Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], + Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], + Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], + Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], ]; diff --git a/config/packages/doctrine.yaml b/config/packages/doctrine.yaml new file mode 100644 index 0000000..25138b9 --- /dev/null +++ b/config/packages/doctrine.yaml @@ -0,0 +1,54 @@ +doctrine: + dbal: + url: '%env(resolve:DATABASE_URL)%' + + # IMPORTANT: You MUST configure your server version, + # either here or in the DATABASE_URL env var (see .env file) + #server_version: '16' + + profiling_collect_backtrace: '%kernel.debug%' + use_savepoints: true + orm: + auto_generate_proxy_classes: true + enable_lazy_ghost_objects: true + report_fields_where_declared: true + validate_xml_mapping: true + naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware + identity_generation_preferences: + Doctrine\DBAL\Platforms\PostgreSQLPlatform: identity + auto_mapping: true + mappings: + App: + type: attribute + is_bundle: false + dir: '%kernel.project_dir%/src/Entity' + prefix: 'App\Entity' + alias: App + controller_resolver: + auto_mapping: false + +when@test: + doctrine: + dbal: + # "TEST_TOKEN" is typically set by ParaTest + dbname_suffix: '_test%env(default::TEST_TOKEN)%' + +when@prod: + doctrine: + orm: + auto_generate_proxy_classes: false + proxy_dir: '%kernel.build_dir%/doctrine/orm/Proxies' + query_cache_driver: + type: pool + pool: doctrine.system_cache_pool + result_cache_driver: + type: pool + pool: doctrine.result_cache_pool + + framework: + cache: + pools: + doctrine.result_cache_pool: + adapter: cache.app + doctrine.system_cache_pool: + adapter: cache.system diff --git a/config/packages/doctrine_migrations.yaml b/config/packages/doctrine_migrations.yaml new file mode 100644 index 0000000..29231d9 --- /dev/null +++ b/config/packages/doctrine_migrations.yaml @@ -0,0 +1,6 @@ +doctrine_migrations: + migrations_paths: + # namespace is arbitrary but should be different from App\Migrations + # as migrations classes should NOT be autoloaded + 'DoctrineMigrations': '%kernel.project_dir%/migrations' + enable_profiler: false diff --git a/config/packages/property_info.yaml b/config/packages/property_info.yaml new file mode 100644 index 0000000..dd31b9d --- /dev/null +++ b/config/packages/property_info.yaml @@ -0,0 +1,3 @@ +framework: + property_info: + with_constructor_extractor: true diff --git a/config/packages/security.yaml b/config/packages/security.yaml new file mode 100644 index 0000000..9103aa5 --- /dev/null +++ b/config/packages/security.yaml @@ -0,0 +1,47 @@ +security: + password_hashers: + App\Entity\User: + algorithm: auto + + providers: + app_user_provider: + entity: + class: App\Entity\User + property: email + + firewalls: + + # 🔐 Admin zuerst! + admin: + pattern: ^/admin + lazy: true + provider: app_user_provider + + form_login: + login_path: admin_login + check_path: admin_login + default_target_path: admin_dashboard + + logout: + path: admin_logout + target: admin_login + + remember_me: + secret: '%kernel.secret%' + lifetime: 604800 + path: /admin + + # 🌍 Alles andere ist public (Chat etc.) + main: + pattern: ^/ + security: false + + role_hierarchy: + ROLE_SUPER_ADMIN: [ROLE_KNOWLEDGE_ADMIN, ROLE_EDITOR, ROLE_USER] + ROLE_KNOWLEDGE_ADMIN: [ROLE_EDITOR, ROLE_USER] + ROLE_EDITOR: [ROLE_USER] + + access_control: + - { path: ^/admin/login$, roles: PUBLIC_ACCESS } + - { path: ^/admin/logout$, roles: PUBLIC_ACCESS } + - { path: ^/admin, roles: ROLE_USER } diff --git a/config/packages/twig.yaml b/config/packages/twig.yaml new file mode 100644 index 0000000..3f795d9 --- /dev/null +++ b/config/packages/twig.yaml @@ -0,0 +1,6 @@ +twig: + file_name_pattern: '*.twig' + +when@test: + twig: + strict_variables: true diff --git a/config/reference.php b/config/reference.php index 8165bdc..d31f1d3 100644 --- a/config/reference.php +++ b/config/reference.php @@ -369,7 +369,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * }>, * }, * property_access?: bool|array{ // Property access configuration - * enabled?: bool|Param, // Default: false + * enabled?: bool|Param, // Default: true * magic_call?: bool|Param, // Default: false * magic_get?: bool|Param, // Default: true * magic_set?: bool|Param, // Default: true @@ -377,11 +377,11 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * throw_exception_on_invalid_property_path?: bool|Param, // Default: true * }, * type_info?: bool|array{ // Type info configuration - * enabled?: bool|Param, // Default: false + * enabled?: bool|Param, // Default: true * aliases?: array, * }, * property_info?: bool|array{ // Property info configuration - * enabled?: bool|Param, // Default: false + * enabled?: bool|Param, // Default: true * with_constructor_extractor?: bool|Param, // Registers the constructor extractor. * }, * cache?: array{ // Cache configuration @@ -833,18 +833,629 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * }, * }>, * } + * @psalm-type SecurityConfig = array{ + * access_denied_url?: scalar|null|Param, // Default: null + * session_fixation_strategy?: "none"|"migrate"|"invalidate"|Param, // Default: "migrate" + * hide_user_not_found?: bool|Param, // Deprecated: The "hide_user_not_found" option is deprecated and will be removed in 8.0. Use the "expose_security_errors" option instead. + * expose_security_errors?: \Symfony\Component\Security\Http\Authentication\ExposeSecurityLevel::None|\Symfony\Component\Security\Http\Authentication\ExposeSecurityLevel::AccountStatus|\Symfony\Component\Security\Http\Authentication\ExposeSecurityLevel::All|Param, // Default: "none" + * erase_credentials?: bool|Param, // Default: true + * access_decision_manager?: array{ + * strategy?: "affirmative"|"consensus"|"unanimous"|"priority"|Param, + * service?: scalar|null|Param, + * strategy_service?: scalar|null|Param, + * allow_if_all_abstain?: bool|Param, // Default: false + * allow_if_equal_granted_denied?: bool|Param, // Default: true + * }, + * password_hashers?: array, + * hash_algorithm?: scalar|null|Param, // Name of hashing algorithm for PBKDF2 (i.e. sha256, sha512, etc..) See hash_algos() for a list of supported algorithms. // Default: "sha512" + * key_length?: scalar|null|Param, // Default: 40 + * ignore_case?: bool|Param, // Default: false + * encode_as_base64?: bool|Param, // Default: true + * iterations?: scalar|null|Param, // Default: 5000 + * cost?: int|Param, // Default: null + * memory_cost?: scalar|null|Param, // Default: null + * time_cost?: scalar|null|Param, // Default: null + * id?: scalar|null|Param, + * }>, + * providers?: array, + * }, + * memory?: array{ + * users?: array, + * }>, + * }, + * ldap?: array{ + * service: scalar|null|Param, + * base_dn: scalar|null|Param, + * search_dn?: scalar|null|Param, // Default: null + * search_password?: scalar|null|Param, // Default: null + * extra_fields?: list, + * default_roles?: list, + * role_fetcher?: scalar|null|Param, // Default: null + * uid_key?: scalar|null|Param, // Default: "sAMAccountName" + * filter?: scalar|null|Param, // Default: "({uid_key}={user_identifier})" + * password_attribute?: scalar|null|Param, // Default: null + * }, + * entity?: array{ + * class: scalar|null|Param, // The full entity class name of your user class. + * property?: scalar|null|Param, // Default: null + * manager_name?: scalar|null|Param, // Default: null + * }, + * }>, + * firewalls: array, + * security?: bool|Param, // Default: true + * user_checker?: scalar|null|Param, // The UserChecker to use when authenticating users in this firewall. // Default: "security.user_checker" + * request_matcher?: scalar|null|Param, + * access_denied_url?: scalar|null|Param, + * access_denied_handler?: scalar|null|Param, + * entry_point?: scalar|null|Param, // An enabled authenticator name or a service id that implements "Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface". + * provider?: scalar|null|Param, + * stateless?: bool|Param, // Default: false + * lazy?: bool|Param, // Default: false + * context?: scalar|null|Param, + * logout?: array{ + * enable_csrf?: bool|null|Param, // Default: null + * csrf_token_id?: scalar|null|Param, // Default: "logout" + * csrf_parameter?: scalar|null|Param, // Default: "_csrf_token" + * csrf_token_manager?: scalar|null|Param, + * path?: scalar|null|Param, // Default: "/logout" + * target?: scalar|null|Param, // Default: "/" + * invalidate_session?: bool|Param, // Default: true + * clear_site_data?: list<"*"|"cache"|"cookies"|"storage"|"executionContexts"|Param>, + * delete_cookies?: array, + * }, + * switch_user?: array{ + * provider?: scalar|null|Param, + * parameter?: scalar|null|Param, // Default: "_switch_user" + * role?: scalar|null|Param, // Default: "ROLE_ALLOWED_TO_SWITCH" + * target_route?: scalar|null|Param, // Default: null + * }, + * required_badges?: list, + * custom_authenticators?: list, + * login_throttling?: array{ + * limiter?: scalar|null|Param, // A service id implementing "Symfony\Component\HttpFoundation\RateLimiter\RequestRateLimiterInterface". + * max_attempts?: int|Param, // Default: 5 + * interval?: scalar|null|Param, // Default: "1 minute" + * lock_factory?: scalar|null|Param, // The service ID of the lock factory used by the login rate limiter (or null to disable locking). // Default: null + * cache_pool?: string|Param, // The cache pool to use for storing the limiter state // Default: "cache.rate_limiter" + * storage_service?: string|Param, // The service ID of a custom storage implementation, this precedes any configured "cache_pool" // Default: null + * }, + * x509?: array{ + * provider?: scalar|null|Param, + * user?: scalar|null|Param, // Default: "SSL_CLIENT_S_DN_Email" + * credentials?: scalar|null|Param, // Default: "SSL_CLIENT_S_DN" + * user_identifier?: scalar|null|Param, // Default: "emailAddress" + * }, + * remote_user?: array{ + * provider?: scalar|null|Param, + * user?: scalar|null|Param, // Default: "REMOTE_USER" + * }, + * login_link?: array{ + * check_route: scalar|null|Param, // Route that will validate the login link - e.g. "app_login_link_verify". + * check_post_only?: scalar|null|Param, // If true, only HTTP POST requests to "check_route" will be handled by the authenticator. // Default: false + * signature_properties: list, + * lifetime?: int|Param, // The lifetime of the login link in seconds. // Default: 600 + * max_uses?: int|Param, // Max number of times a login link can be used - null means unlimited within lifetime. // Default: null + * used_link_cache?: scalar|null|Param, // Cache service id used to expired links of max_uses is set. + * success_handler?: scalar|null|Param, // A service id that implements Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface. + * failure_handler?: scalar|null|Param, // A service id that implements Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface. + * provider?: scalar|null|Param, // The user provider to load users from. + * secret?: scalar|null|Param, // Default: "%kernel.secret%" + * always_use_default_target_path?: bool|Param, // Default: false + * default_target_path?: scalar|null|Param, // Default: "/" + * login_path?: scalar|null|Param, // Default: "/login" + * target_path_parameter?: scalar|null|Param, // Default: "_target_path" + * use_referer?: bool|Param, // Default: false + * failure_path?: scalar|null|Param, // Default: null + * failure_forward?: bool|Param, // Default: false + * failure_path_parameter?: scalar|null|Param, // Default: "_failure_path" + * }, + * form_login?: array{ + * provider?: scalar|null|Param, + * remember_me?: bool|Param, // Default: true + * success_handler?: scalar|null|Param, + * failure_handler?: scalar|null|Param, + * check_path?: scalar|null|Param, // Default: "/login_check" + * use_forward?: bool|Param, // Default: false + * login_path?: scalar|null|Param, // Default: "/login" + * username_parameter?: scalar|null|Param, // Default: "_username" + * password_parameter?: scalar|null|Param, // Default: "_password" + * csrf_parameter?: scalar|null|Param, // Default: "_csrf_token" + * csrf_token_id?: scalar|null|Param, // Default: "authenticate" + * enable_csrf?: bool|Param, // Default: false + * post_only?: bool|Param, // Default: true + * form_only?: bool|Param, // Default: false + * always_use_default_target_path?: bool|Param, // Default: false + * default_target_path?: scalar|null|Param, // Default: "/" + * target_path_parameter?: scalar|null|Param, // Default: "_target_path" + * use_referer?: bool|Param, // Default: false + * failure_path?: scalar|null|Param, // Default: null + * failure_forward?: bool|Param, // Default: false + * failure_path_parameter?: scalar|null|Param, // Default: "_failure_path" + * }, + * form_login_ldap?: array{ + * provider?: scalar|null|Param, + * remember_me?: bool|Param, // Default: true + * success_handler?: scalar|null|Param, + * failure_handler?: scalar|null|Param, + * check_path?: scalar|null|Param, // Default: "/login_check" + * use_forward?: bool|Param, // Default: false + * login_path?: scalar|null|Param, // Default: "/login" + * username_parameter?: scalar|null|Param, // Default: "_username" + * password_parameter?: scalar|null|Param, // Default: "_password" + * csrf_parameter?: scalar|null|Param, // Default: "_csrf_token" + * csrf_token_id?: scalar|null|Param, // Default: "authenticate" + * enable_csrf?: bool|Param, // Default: false + * post_only?: bool|Param, // Default: true + * form_only?: bool|Param, // Default: false + * always_use_default_target_path?: bool|Param, // Default: false + * default_target_path?: scalar|null|Param, // Default: "/" + * target_path_parameter?: scalar|null|Param, // Default: "_target_path" + * use_referer?: bool|Param, // Default: false + * failure_path?: scalar|null|Param, // Default: null + * failure_forward?: bool|Param, // Default: false + * failure_path_parameter?: scalar|null|Param, // Default: "_failure_path" + * service?: scalar|null|Param, // Default: "ldap" + * dn_string?: scalar|null|Param, // Default: "{user_identifier}" + * query_string?: scalar|null|Param, + * search_dn?: scalar|null|Param, // Default: "" + * search_password?: scalar|null|Param, // Default: "" + * }, + * json_login?: array{ + * provider?: scalar|null|Param, + * remember_me?: bool|Param, // Default: true + * success_handler?: scalar|null|Param, + * failure_handler?: scalar|null|Param, + * check_path?: scalar|null|Param, // Default: "/login_check" + * use_forward?: bool|Param, // Default: false + * login_path?: scalar|null|Param, // Default: "/login" + * username_path?: scalar|null|Param, // Default: "username" + * password_path?: scalar|null|Param, // Default: "password" + * }, + * json_login_ldap?: array{ + * provider?: scalar|null|Param, + * remember_me?: bool|Param, // Default: true + * success_handler?: scalar|null|Param, + * failure_handler?: scalar|null|Param, + * check_path?: scalar|null|Param, // Default: "/login_check" + * use_forward?: bool|Param, // Default: false + * login_path?: scalar|null|Param, // Default: "/login" + * username_path?: scalar|null|Param, // Default: "username" + * password_path?: scalar|null|Param, // Default: "password" + * service?: scalar|null|Param, // Default: "ldap" + * dn_string?: scalar|null|Param, // Default: "{user_identifier}" + * query_string?: scalar|null|Param, + * search_dn?: scalar|null|Param, // Default: "" + * search_password?: scalar|null|Param, // Default: "" + * }, + * access_token?: array{ + * provider?: scalar|null|Param, + * remember_me?: bool|Param, // Default: true + * success_handler?: scalar|null|Param, + * failure_handler?: scalar|null|Param, + * realm?: scalar|null|Param, // Default: null + * token_extractors?: list, + * token_handler: string|array{ + * id?: scalar|null|Param, + * oidc_user_info?: string|array{ + * base_uri: scalar|null|Param, // Base URI of the userinfo endpoint on the OIDC server, or the OIDC server URI to use the discovery (require "discovery" to be configured). + * discovery?: array{ // Enable the OIDC discovery. + * cache?: array{ + * id: scalar|null|Param, // Cache service id to use to cache the OIDC discovery configuration. + * }, + * }, + * claim?: scalar|null|Param, // Claim which contains the user identifier (e.g. sub, email, etc.). // Default: "sub" + * client?: scalar|null|Param, // HttpClient service id to use to call the OIDC server. + * }, + * oidc?: array{ + * discovery?: array{ // Enable the OIDC discovery. + * base_uri: list, + * cache?: array{ + * id: scalar|null|Param, // Cache service id to use to cache the OIDC discovery configuration. + * }, + * }, + * claim?: scalar|null|Param, // Claim which contains the user identifier (e.g.: sub, email..). // Default: "sub" + * audience: scalar|null|Param, // Audience set in the token, for validation purpose. + * issuers: list, + * algorithm?: array, + * algorithms: list, + * key?: scalar|null|Param, // Deprecated: The "key" option is deprecated and will be removed in 8.0. Use the "keyset" option instead. // JSON-encoded JWK used to sign the token (must contain a "kty" key). + * keyset?: scalar|null|Param, // JSON-encoded JWKSet used to sign the token (must contain a list of valid public keys). + * encryption?: bool|array{ + * enabled?: bool|Param, // Default: false + * enforce?: bool|Param, // When enabled, the token shall be encrypted. // Default: false + * algorithms: list, + * keyset: scalar|null|Param, // JSON-encoded JWKSet used to decrypt the token (must contain a list of valid private keys). + * }, + * }, + * cas?: array{ + * validation_url: scalar|null|Param, // CAS server validation URL + * prefix?: scalar|null|Param, // CAS prefix // Default: "cas" + * http_client?: scalar|null|Param, // HTTP Client service // Default: null + * }, + * oauth2?: scalar|null|Param, + * }, + * }, + * http_basic?: array{ + * provider?: scalar|null|Param, + * realm?: scalar|null|Param, // Default: "Secured Area" + * }, + * http_basic_ldap?: array{ + * provider?: scalar|null|Param, + * realm?: scalar|null|Param, // Default: "Secured Area" + * service?: scalar|null|Param, // Default: "ldap" + * dn_string?: scalar|null|Param, // Default: "{user_identifier}" + * query_string?: scalar|null|Param, + * search_dn?: scalar|null|Param, // Default: "" + * search_password?: scalar|null|Param, // Default: "" + * }, + * remember_me?: array{ + * secret?: scalar|null|Param, // Default: "%kernel.secret%" + * service?: scalar|null|Param, + * user_providers?: list, + * catch_exceptions?: bool|Param, // Default: true + * signature_properties?: list, + * token_provider?: string|array{ + * service?: scalar|null|Param, // The service ID of a custom remember-me token provider. + * doctrine?: bool|array{ + * enabled?: bool|Param, // Default: false + * connection?: scalar|null|Param, // Default: null + * }, + * }, + * token_verifier?: scalar|null|Param, // The service ID of a custom rememberme token verifier. + * name?: scalar|null|Param, // Default: "REMEMBERME" + * lifetime?: int|Param, // Default: 31536000 + * path?: scalar|null|Param, // Default: "/" + * domain?: scalar|null|Param, // Default: null + * secure?: true|false|"auto"|Param, // Default: null + * httponly?: bool|Param, // Default: true + * samesite?: null|"lax"|"strict"|"none"|Param, // Default: "lax" + * always_remember_me?: bool|Param, // Default: false + * remember_me_parameter?: scalar|null|Param, // Default: "_remember_me" + * }, + * }>, + * access_control?: list, + * attributes?: array, + * route?: scalar|null|Param, // Default: null + * methods?: list, + * allow_if?: scalar|null|Param, // Default: null + * roles?: list, + * }>, + * role_hierarchy?: array>, + * } + * @psalm-type DoctrineConfig = array{ + * dbal?: array{ + * default_connection?: scalar|null|Param, + * types?: array, + * driver_schemes?: array, + * connections?: array, + * mapping_types?: array, + * default_table_options?: array, + * schema_manager_factory?: scalar|null|Param, // Default: "doctrine.dbal.default_schema_manager_factory" + * result_cache?: scalar|null|Param, + * slaves?: array, + * replicas?: array, + * }>, + * }, + * orm?: array{ + * default_entity_manager?: scalar|null|Param, + * auto_generate_proxy_classes?: scalar|null|Param, // Auto generate mode possible values are: "NEVER", "ALWAYS", "FILE_NOT_EXISTS", "EVAL", "FILE_NOT_EXISTS_OR_CHANGED", this option is ignored when the "enable_native_lazy_objects" option is true // Default: false + * enable_lazy_ghost_objects?: bool|Param, // Enables the new implementation of proxies based on lazy ghosts instead of using the legacy implementation // Default: true + * enable_native_lazy_objects?: bool|Param, // Enables the new native implementation of PHP lazy objects instead of generated proxies // Default: false + * proxy_dir?: scalar|null|Param, // Configures the path where generated proxy classes are saved when using non-native lazy objects, this option is ignored when the "enable_native_lazy_objects" option is true // Default: "%kernel.build_dir%/doctrine/orm/Proxies" + * proxy_namespace?: scalar|null|Param, // Defines the root namespace for generated proxy classes when using non-native lazy objects, this option is ignored when the "enable_native_lazy_objects" option is true // Default: "Proxies" + * controller_resolver?: bool|array{ + * enabled?: bool|Param, // Default: true + * auto_mapping?: bool|null|Param, // Set to false to disable using route placeholders as lookup criteria when the primary key doesn't match the argument name // Default: null + * evict_cache?: bool|Param, // Set to true to fetch the entity from the database instead of using the cache, if any // Default: false + * }, + * entity_managers?: array, + * }>, + * }>, + * }, + * connection?: scalar|null|Param, + * class_metadata_factory_name?: scalar|null|Param, // Default: "Doctrine\\ORM\\Mapping\\ClassMetadataFactory" + * default_repository_class?: scalar|null|Param, // Default: "Doctrine\\ORM\\EntityRepository" + * auto_mapping?: scalar|null|Param, // Default: false + * naming_strategy?: scalar|null|Param, // Default: "doctrine.orm.naming_strategy.default" + * quote_strategy?: scalar|null|Param, // Default: "doctrine.orm.quote_strategy.default" + * typed_field_mapper?: scalar|null|Param, // Default: "doctrine.orm.typed_field_mapper.default" + * entity_listener_resolver?: scalar|null|Param, // Default: null + * fetch_mode_subselect_batch_size?: scalar|null|Param, + * repository_factory?: scalar|null|Param, // Default: "doctrine.orm.container_repository_factory" + * schema_ignore_classes?: list, + * report_fields_where_declared?: bool|Param, // Set to "true" to opt-in to the new mapping driver mode that was added in Doctrine ORM 2.16 and will be mandatory in ORM 3.0. See https://github.com/doctrine/orm/pull/10455. // Default: true + * validate_xml_mapping?: bool|Param, // Set to "true" to opt-in to the new mapping driver mode that was added in Doctrine ORM 2.14. See https://github.com/doctrine/orm/pull/6728. // Default: false + * second_level_cache?: array{ + * region_cache_driver?: string|array{ + * type?: scalar|null|Param, // Default: null + * id?: scalar|null|Param, + * pool?: scalar|null|Param, + * }, + * region_lock_lifetime?: scalar|null|Param, // Default: 60 + * log_enabled?: bool|Param, // Default: true + * region_lifetime?: scalar|null|Param, // Default: 3600 + * enabled?: bool|Param, // Default: true + * factory?: scalar|null|Param, + * regions?: array, + * loggers?: array, + * }, + * hydrators?: array, + * mappings?: array, + * dql?: array{ + * string_functions?: array, + * numeric_functions?: array, + * datetime_functions?: array, + * }, + * filters?: array, + * }>, + * identity_generation_preferences?: array, + * }>, + * resolve_target_entities?: array, + * }, + * } + * @psalm-type DoctrineMigrationsConfig = array{ + * enable_service_migrations?: bool|Param, // Whether to enable fetching migrations from the service container. // Default: false + * migrations_paths?: array, + * services?: array, + * factories?: array, + * storage?: array{ // Storage to use for migration status metadata. + * table_storage?: array{ // The default metadata storage, implemented as a table in the database. + * table_name?: scalar|null|Param, // Default: null + * version_column_name?: scalar|null|Param, // Default: null + * version_column_length?: scalar|null|Param, // Default: null + * executed_at_column_name?: scalar|null|Param, // Default: null + * execution_time_column_name?: scalar|null|Param, // Default: null + * }, + * }, + * migrations?: list, + * connection?: scalar|null|Param, // Connection name to use for the migrations database. // Default: null + * em?: scalar|null|Param, // Entity manager name to use for the migrations database (available when doctrine/orm is installed). // Default: null + * all_or_nothing?: scalar|null|Param, // Run all migrations in a transaction. // Default: false + * check_database_platform?: scalar|null|Param, // Adds an extra check in the generated migrations to allow execution only on the same platform as they were initially generated on. // Default: true + * custom_template?: scalar|null|Param, // Custom template path for generated migration classes. // Default: null + * organize_migrations?: scalar|null|Param, // Organize migrations mode. Possible values are: "BY_YEAR", "BY_YEAR_AND_MONTH", false // Default: false + * enable_profiler?: bool|Param, // Whether or not to enable the profiler collector to calculate and visualize migration status. This adds some queries overhead. // Default: false + * transactional?: bool|Param, // Whether or not to wrap migrations in a single transaction. // Default: true + * } + * @psalm-type MakerConfig = array{ + * root_namespace?: scalar|null|Param, // Default: "App" + * generate_final_classes?: bool|Param, // Default: true + * generate_final_entities?: bool|Param, // Default: false + * } + * @psalm-type TwigConfig = array{ + * form_themes?: list, + * globals?: array, + * autoescape_service?: scalar|null|Param, // Default: null + * autoescape_service_method?: scalar|null|Param, // Default: null + * base_template_class?: scalar|null|Param, // Deprecated: The child node "base_template_class" at path "twig.base_template_class" is deprecated. + * cache?: scalar|null|Param, // Default: true + * charset?: scalar|null|Param, // Default: "%kernel.charset%" + * debug?: bool|Param, // Default: "%kernel.debug%" + * strict_variables?: bool|Param, // Default: "%kernel.debug%" + * auto_reload?: scalar|null|Param, + * optimizations?: int|Param, + * default_path?: scalar|null|Param, // The default path used to load templates. // Default: "%kernel.project_dir%/templates" + * file_name_pattern?: list, + * paths?: array, + * date?: array{ // The default format options used by the date filter. + * format?: scalar|null|Param, // Default: "F j, Y H:i" + * interval_format?: scalar|null|Param, // Default: "%d days" + * timezone?: scalar|null|Param, // The timezone used when formatting dates, when set to null, the timezone returned by date_default_timezone_get() is used. // Default: null + * }, + * number_format?: array{ // The default format options for the number_format filter. + * decimals?: int|Param, // Default: 0 + * decimal_point?: scalar|null|Param, // Default: "." + * thousands_separator?: scalar|null|Param, // Default: "," + * }, + * mailer?: array{ + * html_to_text_converter?: scalar|null|Param, // A service implementing the "Symfony\Component\Mime\HtmlToTextConverter\HtmlToTextConverterInterface". // Default: null + * }, + * } * @psalm-type ConfigType = array{ * imports?: ImportsConfig, * parameters?: ParametersConfig, * services?: ServicesConfig, * framework?: FrameworkConfig, * monolog?: MonologConfig, + * security?: SecurityConfig, + * doctrine?: DoctrineConfig, + * doctrine_migrations?: DoctrineMigrationsConfig, + * twig?: TwigConfig, * "when@dev"?: array{ * imports?: ImportsConfig, * parameters?: ParametersConfig, * services?: ServicesConfig, * framework?: FrameworkConfig, * monolog?: MonologConfig, + * security?: SecurityConfig, + * doctrine?: DoctrineConfig, + * doctrine_migrations?: DoctrineMigrationsConfig, + * maker?: MakerConfig, + * twig?: TwigConfig, * }, * "when@prod"?: array{ * imports?: ImportsConfig, @@ -852,6 +1463,10 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * services?: ServicesConfig, * framework?: FrameworkConfig, * monolog?: MonologConfig, + * security?: SecurityConfig, + * doctrine?: DoctrineConfig, + * doctrine_migrations?: DoctrineMigrationsConfig, + * twig?: TwigConfig, * }, * "when@test"?: array{ * imports?: ImportsConfig, @@ -859,6 +1474,10 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * services?: ServicesConfig, * framework?: FrameworkConfig, * monolog?: MonologConfig, + * security?: SecurityConfig, + * doctrine?: DoctrineConfig, + * doctrine_migrations?: DoctrineMigrationsConfig, + * twig?: TwigConfig, * }, * ...addSql('CREATE TABLE user (id BINARY(16) NOT NULL, email VARCHAR(180) NOT NULL, password VARCHAR(255) NOT NULL, roles JSON NOT NULL, is_active TINYINT NOT NULL, created_at DATETIME NOT NULL, updated_at DATETIME DEFAULT NULL, UNIQUE INDEX UNIQ_8D93D649E7927C74 (email), PRIMARY KEY (id)) DEFAULT CHARACTER SET utf8mb4'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('DROP TABLE user'); + } +} diff --git a/migrations/Version20260211142601.php b/migrations/Version20260211142601.php new file mode 100644 index 0000000..5b6810d --- /dev/null +++ b/migrations/Version20260211142601.php @@ -0,0 +1,41 @@ +addSql('CREATE TABLE document (id BINARY(16) NOT NULL, title VARCHAR(255) NOT NULL, status VARCHAR(20) NOT NULL, created_at DATETIME NOT NULL, created_by_id BINARY(16) NOT NULL, current_version_id BINARY(16) DEFAULT NULL, INDEX IDX_D8698A76B03A8386 (created_by_id), INDEX IDX_D8698A769407EE77 (current_version_id), PRIMARY KEY (id)) DEFAULT CHARACTER SET utf8mb4'); + $this->addSql('CREATE TABLE document_version (id BINARY(16) NOT NULL, version_number INT NOT NULL, file_path VARCHAR(255) NOT NULL, checksum VARCHAR(64) NOT NULL, ingest_status VARCHAR(20) NOT NULL, created_at DATETIME NOT NULL, is_active TINYINT NOT NULL, document_id BINARY(16) NOT NULL, created_by_id BINARY(16) NOT NULL, INDEX IDX_1B73751FC33F7837 (document_id), INDEX IDX_1B73751FB03A8386 (created_by_id), PRIMARY KEY (id)) DEFAULT CHARACTER SET utf8mb4'); + $this->addSql('ALTER TABLE document ADD CONSTRAINT FK_D8698A76B03A8386 FOREIGN KEY (created_by_id) REFERENCES user (id)'); + $this->addSql('ALTER TABLE document ADD CONSTRAINT FK_D8698A769407EE77 FOREIGN KEY (current_version_id) REFERENCES document_version (id)'); + $this->addSql('ALTER TABLE document_version ADD CONSTRAINT FK_1B73751FC33F7837 FOREIGN KEY (document_id) REFERENCES document (id)'); + $this->addSql('ALTER TABLE document_version ADD CONSTRAINT FK_1B73751FB03A8386 FOREIGN KEY (created_by_id) REFERENCES user (id)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE document DROP FOREIGN KEY FK_D8698A76B03A8386'); + $this->addSql('ALTER TABLE document DROP FOREIGN KEY FK_D8698A769407EE77'); + $this->addSql('ALTER TABLE document_version DROP FOREIGN KEY FK_1B73751FC33F7837'); + $this->addSql('ALTER TABLE document_version DROP FOREIGN KEY FK_1B73751FB03A8386'); + $this->addSql('DROP TABLE document'); + $this->addSql('DROP TABLE document_version'); + } +} diff --git a/migrations/Version20260211145114.php b/migrations/Version20260211145114.php new file mode 100644 index 0000000..1907667 --- /dev/null +++ b/migrations/Version20260211145114.php @@ -0,0 +1,33 @@ +addSql('CREATE TABLE ingest_job (id BINARY(16) NOT NULL, type VARCHAR(30) NOT NULL, status VARCHAR(20) NOT NULL, document_id VARCHAR(255) DEFAULT NULL, document_version_id VARCHAR(255) DEFAULT NULL, started_at DATETIME NOT NULL, finished_at DATETIME DEFAULT NULL, log_path VARCHAR(255) DEFAULT NULL, error_message LONGTEXT DEFAULT NULL, started_by_id BINARY(16) DEFAULT NULL, INDEX IDX_4F6AC8649740C9D5 (started_by_id), PRIMARY KEY (id)) DEFAULT CHARACTER SET utf8mb4'); + $this->addSql('ALTER TABLE ingest_job ADD CONSTRAINT FK_4F6AC8649740C9D5 FOREIGN KEY (started_by_id) REFERENCES user (id)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE ingest_job DROP FOREIGN KEY FK_4F6AC8649740C9D5'); + $this->addSql('DROP TABLE ingest_job'); + } +} diff --git a/migrations/Version20260211145121.php b/migrations/Version20260211145121.php new file mode 100644 index 0000000..3d71b3a --- /dev/null +++ b/migrations/Version20260211145121.php @@ -0,0 +1,33 @@ +addSql('CREATE TABLE ingest_job (id BINARY(16) NOT NULL, type VARCHAR(30) NOT NULL, status VARCHAR(20) NOT NULL, document_id VARCHAR(255) DEFAULT NULL, document_version_id VARCHAR(255) DEFAULT NULL, started_at DATETIME NOT NULL, finished_at DATETIME DEFAULT NULL, log_path VARCHAR(255) DEFAULT NULL, error_message LONGTEXT DEFAULT NULL, started_by_id BINARY(16) DEFAULT NULL, INDEX IDX_4F6AC8649740C9D5 (started_by_id), PRIMARY KEY (id)) DEFAULT CHARACTER SET utf8mb4'); + $this->addSql('ALTER TABLE ingest_job ADD CONSTRAINT FK_4F6AC8649740C9D5 FOREIGN KEY (started_by_id) REFERENCES user (id)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE ingest_job DROP FOREIGN KEY FK_4F6AC8649740C9D5'); + $this->addSql('DROP TABLE ingest_job'); + } +} diff --git a/migrations/Version20260211153201.php b/migrations/Version20260211153201.php new file mode 100644 index 0000000..c99f6e4 --- /dev/null +++ b/migrations/Version20260211153201.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE ingest_job CHANGE document_id document_id BINARY(16) DEFAULT NULL, CHANGE document_version_id document_version_id BINARY(16) DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE ingest_job CHANGE document_id document_id VARCHAR(255) DEFAULT NULL, CHANGE document_version_id document_version_id VARCHAR(255) DEFAULT NULL'); + } +} diff --git a/src/Command/CreateUserCommand.php b/src/Command/CreateUserCommand.php new file mode 100644 index 0000000..1acfcc6 --- /dev/null +++ b/src/Command/CreateUserCommand.php @@ -0,0 +1,108 @@ +getHelper('question'); + + // ============================= + // Email + // ============================= + $emailQuestion = new Question('E-Mail: '); + $emailQuestion->setValidator(function ($value) { + if (!filter_var($value, FILTER_VALIDATE_EMAIL)) { + throw new \RuntimeException('Invalid email address.'); + } + return strtolower(trim($value)); + }); + + $email = $helper->ask($input, $output, $emailQuestion); + + // Prüfen ob User existiert + $existingUser = $this->em + ->getRepository(User::class) + ->findOneBy(['email' => $email]); + + if ($existingUser) { + $output->writeln('User already exists.'); + return Command::FAILURE; + } + + // ============================= + // Passwort + // ============================= + $passwordQuestion = new Question('Password: '); + $passwordQuestion->setHidden(true); + $passwordQuestion->setHiddenFallback(false); + + $plainPassword = $helper->ask($input, $output, $passwordQuestion); + + if (strlen($plainPassword) < 8) { + $output->writeln('Password must be at least 8 characters.'); + return Command::FAILURE; + } + + // ============================= + // Rolle auswählen + // ============================= + $roleQuestion = new ChoiceQuestion( + 'Select role:', + [ + 'ROLE_SUPER_ADMIN', + 'ROLE_KNOWLEDGE_ADMIN', + 'ROLE_EDITOR', + 'ROLE_USER', + ], + 0 + ); + + $role = $helper->ask($input, $output, $roleQuestion); + + // ============================= + // User erzeugen + // ============================= + $user = new User(); + $user->setEmail($email); + $user->setRoles([$role]); + + $hashedPassword = $this->passwordHasher->hashPassword($user, $plainPassword); + $user->setPassword($hashedPassword); + + $this->em->persist($user); + $this->em->flush(); + + $output->writeln('User created successfully.'); + $output->writeln('Email: ' . $email); + $output->writeln('Role: ' . $role); + + return Command::SUCCESS; + } +} diff --git a/src/Controller/Admin/DashboardController.php b/src/Controller/Admin/DashboardController.php new file mode 100644 index 0000000..a349225 --- /dev/null +++ b/src/Controller/Admin/DashboardController.php @@ -0,0 +1,17 @@ +render('admin/dashboard/index.html.twig'); + } +} diff --git a/src/Controller/Admin/DocumentController.php b/src/Controller/Admin/DocumentController.php new file mode 100644 index 0000000..28d5a14 --- /dev/null +++ b/src/Controller/Admin/DocumentController.php @@ -0,0 +1,217 @@ +getRepository(Document::class) + ->findBy([], ['createdAt' => 'DESC']); + + return $this->render('admin/document/index.html.twig', [ + 'documents' => $documents + ]); + } + + #[Route( + '/{id}', + name: 'admin_document_show', + requirements: ['id' => '[0-9a-fA-F\-]{36}'] + )] + public function show(string $id, EntityManagerInterface $em): Response + { + try { + $uuid = Uuid::fromString($id); + } catch (\Exception $e) { + throw new NotFoundHttpException(); + } + + $document = $em->getRepository(Document::class)->find($uuid); + + if (!$document) { + throw new NotFoundHttpException(); + } + + return $this->render('admin/document/show.html.twig', [ + 'document' => $document + ]); + } + + #[Route('/new', name: 'admin_document_new')] + public function new(Request $request, DocumentService $documentService): Response + { + if ($request->isMethod('POST')) { + + $title = $request->request->get('title'); + $file = $request->files->get('file'); + + if (!$file || !$title) { + $this->addFlash('error', 'Titel und Datei sind erforderlich.'); + return $this->redirectToRoute('admin_document_new'); + } + + $uploadDir = $this->getParameter('kernel.project_dir') . '/var/knowledge/uploads'; + + if (!is_dir($uploadDir)) { + mkdir($uploadDir, 0777, true); + } + + $newFilename = uniqid() . '_' . $file->getClientOriginalName(); + + try { + $file->move($uploadDir, $newFilename); + } catch (FileException $e) { + throw new \RuntimeException('File upload failed.'); + } + + $filePath = $uploadDir . '/' . $newFilename; + + $documentService->createDocument( + $title, + $filePath, + $this->getUser() + ); + + return $this->redirectToRoute('admin_documents'); + } + + return $this->render('admin/document/new.html.twig'); + } + + #[Route('/{id}/version/new', name: 'admin_document_version_new', requirements: ['id' => '[0-9a-fA-F\-]{36}'])] + public function newVersion( + string $id, + Request $request, + EntityManagerInterface $em, + DocumentService $documentService + ): Response { + + $document = $em->getRepository(Document::class)->find($id); + + if (!$document) { + throw $this->createNotFoundException(); + } + + if ($request->isMethod('POST')) { + + $file = $request->files->get('file'); + + if (!$file) { + $this->addFlash('error', 'Datei ist erforderlich.'); + return $this->redirectToRoute('admin_document_version_new', ['id' => $id]); + } + + $uploadDir = $this->getParameter('kernel.project_dir') . '/var/knowledge/uploads'; + + if (!is_dir($uploadDir)) { + mkdir($uploadDir, 0777, true); + } + + $newFilename = uniqid() . '_' . $file->getClientOriginalName(); + + try { + $file->move($uploadDir, $newFilename); + } catch (FileException $e) { + throw new \RuntimeException('File upload failed.'); + } + + $filePath = $uploadDir . '/' . $newFilename; + + $documentService->addVersion( + $document, + $filePath, + $this->getUser() + ); + + return $this->redirectToRoute('admin_document_show', ['id' => $id]); + } + + return $this->render('admin/document/new_version.html.twig', [ + 'document' => $document + ]); + } + + #[Route( + '/version/{versionId}/activate', + name: 'admin_document_version_activate', + requirements: ['versionId' => '[0-9a-fA-F\-]{36}'], + methods: ['POST'] + )] + public function activateVersion( + string $versionId, + Request $request, + EntityManagerInterface $em, + DocumentService $documentService + ): RedirectResponse { + + if (!$this->isCsrfTokenValid('activate_version', $request->request->get('_token'))) { + throw $this->createAccessDeniedException(); + } + + $version = $em->getRepository(DocumentVersion::class)->find($versionId); + + if (!$version) { + throw $this->createNotFoundException(); + } + + $documentService->activateVersion($version); + + return $this->redirectToRoute('admin_document_show', [ + 'id' => $version->getDocument()->getId() + ]); + } + + #[Route( + '/version/{versionId}/ingest', + name: 'admin_document_version_ingest', + methods: ['POST'], + requirements: ['versionId' => '[0-9a-fA-F\-]{36}'] + )] + public function ingestVersion( + string $versionId, + Request $request, + EntityManagerInterface $em, + IngestOrchestrator $orchestrator + ): RedirectResponse { + + if (!$this->isCsrfTokenValid('ingest_version', $request->request->get('_token'))) { + throw $this->createAccessDeniedException(); + } + + $version = $em->getRepository(DocumentVersion::class)->find($versionId); + + if (!$version) { + throw $this->createNotFoundException(); + } + + $orchestrator->runForVersion( + $version, + $this->getUser(), + true // erstmal DryRun + ); + + return $this->redirectToRoute('admin_document_show', [ + 'id' => $version->getDocument()->getId() + ]); + } + +} diff --git a/src/Controller/Admin/IngestJobController.php b/src/Controller/Admin/IngestJobController.php new file mode 100644 index 0000000..dfc43db --- /dev/null +++ b/src/Controller/Admin/IngestJobController.php @@ -0,0 +1,57 @@ +getRepository(IngestJob::class) + ->findBy([], ['startedAt' => 'DESC']); + + return $this->render('admin/job/index.html.twig', [ + 'jobs' => $jobs + ]); + } + + #[Route( + '/{id}', + name: 'admin_job_show', + requirements: ['id' => '[0-9a-fA-F\-]{36}'] + )] + public function show(string $id, EntityManagerInterface $em): Response + { + $job = $em->getRepository(IngestJob::class)->find($id); + + if (!$job) { + throw new NotFoundHttpException(); + } + + return $this->render('admin/job/show.html.twig', [ + 'job' => $job + ]); + } + + #[Route('/global-reindex', name: 'admin_global_reindex', methods: ['POST'])] + public function globalReindex( + IngestFlow $flow + ): RedirectResponse { + $this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN'); + + $flow->globalReindex($this->getUser()); + + return $this->redirectToRoute('admin_jobs'); + } +} diff --git a/src/Controller/Admin/SecurityController.php b/src/Controller/Admin/SecurityController.php new file mode 100644 index 0000000..97ca73f --- /dev/null +++ b/src/Controller/Admin/SecurityController.php @@ -0,0 +1,33 @@ +getUser()) { + return $this->redirectToRoute('admin_dashboard'); + } + + return $this->render('admin/security/login.html.twig', [ + 'last_username' => $authUtils->getLastUsername(), + 'error' => $authUtils->getLastAuthenticationError(), + ]); + } + + #[Route('/admin/logout', name: 'admin_logout')] + public function logout(): void + { + // Symfony interceptet diese Route, daher bleibt sie leer. + throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.'); + } +} diff --git a/src/Entity/Document.php b/src/Entity/Document.php new file mode 100644 index 0000000..106b7c4 --- /dev/null +++ b/src/Entity/Document.php @@ -0,0 +1,110 @@ +id = Uuid::v4(); + $this->createdAt = new \DateTimeImmutable(); + $this->versions = new ArrayCollection(); + } + + public function getId(): Uuid + { + return $this->id; + } + + public function getCreatedAt(): \DateTimeImmutable + { + return $this->createdAt; + } + + public function getTitle(): string + { + return $this->title; + } + + public function setTitle(string $title): static + { + $this->title = $title; + return $this; + } + + + public function getStatus(): string + { + return $this->status; + } + + public function archive(): void + { + $this->status = self::STATUS_ARCHIVED; + } + + public function getCreatedBy(): User + { + return $this->createdBy; + } + + public function setCreatedBy(User $user): static + { + $this->createdBy = $user; + return $this; + } + + public function getVersions(): Collection + { + return $this->versions; + } + + public function addVersion(DocumentVersion $version): void + { + $this->versions->add($version); + $version->setDocument($this); + } + + public function setCurrentVersion(?DocumentVersion $version): void + { + $this->currentVersion = $version; + } + + public function getCurrentVersion(): ?DocumentVersion + { + return $this->currentVersion; + } +} diff --git a/src/Entity/DocumentVersion.php b/src/Entity/DocumentVersion.php new file mode 100644 index 0000000..fa3a132 --- /dev/null +++ b/src/Entity/DocumentVersion.php @@ -0,0 +1,184 @@ +id = Uuid::v4(); + $this->createdAt = new \DateTimeImmutable(); + } + + // ========================= + // ID + // ========================= + + public function getId(): Uuid + { + return $this->id; + } + + // ========================= + // Document Relation + // ========================= + + public function setDocument(Document $document): void + { + $this->document = $document; + } + + public function getDocument(): Document + { + return $this->document; + } + + // ========================= + // Version Number + // ========================= + + public function getVersionNumber(): int + { + return $this->versionNumber; + } + + public function setVersionNumber(int $number): void + { + $this->versionNumber = $number; + } + + // ========================= + // File Path + // ========================= + + public function setFilePath(string $path): void + { + $this->filePath = $path; + } + + public function getFilePath(): string + { + return $this->filePath; + } + + // ========================= + // Checksum + // ========================= + + public function setChecksum(string $checksum): void + { + $this->checksum = $checksum; + } + + public function getChecksum(): string + { + return $this->checksum; + } + + // ========================= + // Ingest Status + // ========================= + + public function setIngestStatus(string $status): void + { + if (!in_array($status, self::INGEST_STATUSES, true)) { + throw new \InvalidArgumentException('Invalid ingest status.'); + } + + $this->ingestStatus = $status; + } + + public function getIngestStatus(): string + { + return $this->ingestStatus; + } + + public function isIndexed(): bool + { + return $this->ingestStatus === self::INGEST_INDEXED; + } + + // ========================= + // Created By + // ========================= + + public function setCreatedBy(User $user): void + { + $this->createdBy = $user; + } + + public function getCreatedBy(): User + { + return $this->createdBy; + } + + // ========================= + // Created At + // ========================= + + public function getCreatedAt(): \DateTimeImmutable + { + return $this->createdAt; + } + + // ========================= + // Active Flag + // ========================= + + public function setActive(bool $active): void + { + $this->isActive = $active; + } + + public function isActive(): bool + { + return $this->isActive; + } +} diff --git a/src/Entity/IngestJob.php b/src/Entity/IngestJob.php new file mode 100644 index 0000000..8ee801b --- /dev/null +++ b/src/Entity/IngestJob.php @@ -0,0 +1,101 @@ +id = Uuid::v4(); + $this->type = $type; + $this->startedAt = new \DateTimeImmutable(); + $this->status = self::STATUS_RUNNING; + } + + public function getId(): Uuid { return $this->id; } + public function getType(): string { return $this->type; } + public function getStatus(): string { return $this->status; } + + public function setDocumentId(?Uuid $id): void { $this->documentId = $id; } + public function getDocumentId(): ?Uuid { return $this->documentId; } + + public function setDocumentVersionId(?Uuid $id): void { $this->documentVersionId = $id; } + public function getDocumentVersionId(): ?Uuid { return $this->documentVersionId; } + + public function setStartedBy(?User $user): void { $this->startedBy = $user; } + public function getStartedBy(): ?User { return $this->startedBy; } + + public function setLogPath(?string $path): void { $this->logPath = $path; } + public function getLogPath(): ?string { return $this->logPath; } + + public function getStartedAt(): \DateTimeImmutable { return $this->startedAt; } + public function getFinishedAt(): ?\DateTimeImmutable { return $this->finishedAt; } + + public function markCompleted(): void + { + $this->status = self::STATUS_COMPLETED; + $this->finishedAt = new \DateTimeImmutable(); + } + + public function markFailed(string $message): void + { + $this->status = self::STATUS_FAILED; + $this->errorMessage = $message; + $this->finishedAt = new \DateTimeImmutable(); + } + + public function markAborted(): void + { + $this->status = self::STATUS_ABORTED; + $this->finishedAt = new \DateTimeImmutable(); + } + + public function getErrorMessage(): ?string + { + return $this->errorMessage; + } +} diff --git a/src/Entity/User.php b/src/Entity/User.php new file mode 100644 index 0000000..e35e7c6 --- /dev/null +++ b/src/Entity/User.php @@ -0,0 +1,161 @@ +id = Uuid::v4(); + $this->createdAt = new \DateTimeImmutable(); + $this->roles = ['ROLE_USER']; + } + + // ========================= + // Security Identifier + // ========================= + + public function getUserIdentifier(): string + { + return $this->email; + } + + // Symfony < 6 compatibility (optional) + public function getUsername(): string + { + return $this->email; + } + + // ========================= + // ID + // ========================= + + public function getId(): Uuid + { + return $this->id; + } + + // ========================= + // Email + // ========================= + + public function getEmail(): string + { + return $this->email; + } + + public function setEmail(string $email): static + { + $this->email = strtolower($email); + return $this; + } + + // ========================= + // Password + // ========================= + + public function getPassword(): string + { + return $this->password; + } + + public function setPassword(string $password): static + { + $this->password = $password; + return $this; + } + + // ========================= + // Roles + // ========================= + + public function getRoles(): array + { + $roles = $this->roles; + + // Jeder User hat mindestens ROLE_USER + $roles[] = 'ROLE_USER'; + + return array_unique($roles); + } + + public function setRoles(array $roles): static + { + $this->roles = $roles; + return $this; + } + + // ========================= + // Active Status + // ========================= + + public function isActive(): bool + { + return $this->isActive; + } + + public function setIsActive(bool $isActive): static + { + $this->isActive = $isActive; + return $this; + } + + // ========================= + // Timestamps + // ========================= + + public function getCreatedAt(): \DateTimeImmutable + { + return $this->createdAt; + } + + public function getUpdatedAt(): ?\DateTimeImmutable + { + return $this->updatedAt; + } + + #[ORM\PreUpdate] + public function updateTimestamp(): void + { + $this->updatedAt = new \DateTimeImmutable(); + } + + // ========================= + // Erase Credentials (Pflicht) + // ========================= + + public function eraseCredentials(): void + { + // Falls später sensible Daten gespeichert werden + } +} diff --git a/src/Index/IndexConfiguration.php b/src/Index/IndexConfiguration.php new file mode 100644 index 0000000..29a0658 --- /dev/null +++ b/src/Index/IndexConfiguration.php @@ -0,0 +1,95 @@ +chunkSize <= 0) { + throw new \InvalidArgumentException('chunkSize must be > 0'); + } + if ($this->chunkOverlap < 0) { + throw new \InvalidArgumentException('chunkOverlap must be >= 0'); + } + if ($this->chunkOverlap >= $this->chunkSize) { + throw new \InvalidArgumentException('chunkOverlap must be < chunkSize'); + } + if ($this->embeddingDimension <= 0) { + throw new \InvalidArgumentException('embeddingDimension must be > 0'); + } + if ($this->scoringVersion <= 0) { + throw new \InvalidArgumentException('scoringVersion must be > 0'); + } + if ($this->indexFormat !== 'ndjson') { + throw new \InvalidArgumentException('indexFormat must be "ndjson"'); + } + } + + public function getChunkSize(): int + { + return $this->chunkSize; + } + + public function getChunkOverlap(): int + { + return $this->chunkOverlap; + } + + public function getEmbeddingModel(): string + { + return $this->embeddingModel; + } + + public function getEmbeddingDimension(): int + { + return $this->embeddingDimension; + } + + public function getScoringVersion(): int + { + return $this->scoringVersion; + } + + public function getIndexFormat(): string + { + return $this->indexFormat; + } + + public function getVectorBackend(): string + { + return $this->vectorBackend; + } + + /** + * Canonical representation: nur strukturelle Felder (ohne created_at, index_version). + */ + public function toStructureArray(): array + { + return [ + 'embedding_model' => $this->embeddingModel, + 'embedding_dimension' => $this->embeddingDimension, + 'chunk_size' => $this->chunkSize, + 'chunk_overlap' => $this->chunkOverlap, + 'scoring_version' => $this->scoringVersion, + 'index_format' => $this->indexFormat, + 'vector_backend' => $this->vectorBackend, + ]; + } +} diff --git a/src/Index/IndexMetaManager.php b/src/Index/IndexMetaManager.php new file mode 100644 index 0000000..2b0669f --- /dev/null +++ b/src/Index/IndexMetaManager.php @@ -0,0 +1,206 @@ +metaPath = rtrim($projectDir, '/') . $relativeMetaPath; + } + + public function getMetaPath(): string + { + return $this->metaPath; + } + + /** + * Gibt null zurück, wenn noch kein Meta existiert (frisches System). + * + * @return array|null + */ + public function readMeta(): ?array + { + if (!is_file($this->metaPath)) { + return null; + } + + $raw = file_get_contents($this->metaPath); + if ($raw === false) { + throw new \RuntimeException('Unable to read index_meta.json'); + } + + $data = json_decode($raw, true); + if (!is_array($data)) { + throw new \RuntimeException('index_meta.json is invalid JSON'); + } + + return $data; + } + + /** + * Erstellt Meta, falls nicht vorhanden (z. B. nach erstem Global Reindex). + * Überschreibt NICHT automatisch, wenn vorhanden. + * + * @return array + */ + public function createInitialMetaIfMissing(): array + { + $existing = $this->readMeta(); + if ($existing !== null) { + return $existing; + } + + $meta = $this->buildMetaPayload(indexVersion: 1); + $this->atomicWriteJson($meta); + + return $meta; + } + + /** + * Guardrail: Prüft, ob die aktuelle Config kompatibel zur gespeicherten Meta ist. + * Wenn nicht: IndexStructureChangedException -> Global Reindex erzwingen. + */ + public function validateAgainstCurrent(): void + { + $meta = $this->readMeta(); + + // Wenn noch kein Meta existiert, lassen wir lokale Ingests NICHT einfach laufen. + // Governance: Erst Global Reindex erzeugt Meta sauber. + if ($meta === null) { + throw new IndexStructureChangedException( + 'index_meta.json missing. Please run a Global Reindex to initialize index structure metadata.', + ['reason' => 'missing_meta'] + ); + } + + $expected = $this->config->toStructureArray(); + + $diff = $this->diffStructure($meta, $expected); + + if ($diff !== []) { + throw new IndexStructureChangedException( + 'Index structure changed. Global Reindex required.', + $diff + ); + } + } + + /** + * Wird beim Global Reindex verwendet: + * - index_version++ (oder initialisieren) + * - Meta atomar schreiben + * + * @return array new meta + */ + public function writeMetaForGlobalReindex(): array + { + $current = $this->readMeta(); + + $nextVersion = 1; + if (is_array($current) && isset($current['index_version']) && is_int($current['index_version'])) { + $nextVersion = $current['index_version'] + 1; + } + + $meta = $this->buildMetaPayload($nextVersion); + $this->atomicWriteJson($meta); + + return $meta; + } + + public function getConfig(): IndexConfiguration + { + return $this->config; + } + + // ------------------------- + // Internals + // ------------------------- + + /** + * @return array + */ + private function buildMetaPayload(int $indexVersion): array + { + $structure = $this->config->toStructureArray(); + + return [ + 'index_version' => $indexVersion, + 'created_at' => (new \DateTimeImmutable())->format(DATE_ATOM), + 'embedding_model' => $structure['embedding_model'], + 'embedding_dimension' => $structure['embedding_dimension'], + 'chunk_size' => $structure['chunk_size'], + 'chunk_overlap' => $structure['chunk_overlap'], + 'scoring_version' => $structure['scoring_version'], + 'index_format' => $structure['index_format'], + 'vector_backend' => $structure['vector_backend'], + ]; + } + + /** + * @param array $meta + * @param array $expected + * @return array diff + */ + private function diffStructure(array $meta, array $expected): array + { + $diff = []; + + foreach ($expected as $key => $value) { + $actual = $meta[$key] ?? null; + if ($actual !== $value) { + $diff[$key] = [ + 'expected' => $value, + 'actual' => $actual, + ]; + } + } + + // index_format ist zwingend + if (($meta['index_format'] ?? null) !== 'ndjson') { + $diff['index_format'] = [ + 'expected' => 'ndjson', + 'actual' => $meta['index_format'] ?? null, + ]; + } + + return $diff; + } + + /** + * @param array $payload + */ + private function atomicWriteJson(array $payload): void + { + $dir = \dirname($this->metaPath); + if (!is_dir($dir) && !mkdir($dir, 0777, true) && !is_dir($dir)) { + throw new \RuntimeException('Unable to create directory: ' . $dir); + } + + $tmp = $this->metaPath . '.tmp'; + + $json = json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + if ($json === false) { + throw new \RuntimeException('Unable to encode index_meta.json'); + } + + if (file_put_contents($tmp, $json . PHP_EOL) === false) { + throw new \RuntimeException('Unable to write temp meta file'); + } + + // atomarer Switch + if (!rename($tmp, $this->metaPath)) { + @unlink($tmp); + throw new \RuntimeException('Unable to switch meta file atomically'); + } + } +} diff --git a/src/Index/IndexStructureChangedException.php b/src/Index/IndexStructureChangedException.php new file mode 100644 index 0000000..2f76ee5 --- /dev/null +++ b/src/Index/IndexStructureChangedException.php @@ -0,0 +1,34 @@ + $diff + */ + public function __construct( + string $message, + private readonly array $diff = [], + int $code = 0, + ?\Throwable $previous = null + ) + { + parent::__construct($message, $code, $previous); + } + + /** + * @return array + */ + public function getDiff(): array + { + return $this->diff; + } +} diff --git a/src/Ingest/IngestFlow.php b/src/Ingest/IngestFlow.php new file mode 100644 index 0000000..568a38a --- /dev/null +++ b/src/Ingest/IngestFlow.php @@ -0,0 +1,182 @@ +lockService->acquire()) { + throw new \RuntimeException('Another ingest job is already running.'); + } + + $job = null; + + try { + + $job = $this->jobService->startJob( + IngestJob::TYPE_DOCUMENT, + $user, + $version->getDocument()->getId(), + $version->getId(), + ); + + $version->setIngestStatus(DocumentVersion::INGEST_RUNNING); + $this->em->flush(); + + // -------------------------------------------------- + // Guardrail: Struktur prüfen + // -------------------------------------------------- + $this->metaManager->validateAgainstCurrent(); + + // -------------------------------------------------- + // Alte Chunks dieses Dokuments entfernen (Streaming) + // -------------------------------------------------- + $this->chunkManager->compactByDocument( + $version->getDocument()->getId() + ); + + // -------------------------------------------------- + // Neue Chunks erzeugen + // -------------------------------------------------- + $records = $this->knowledgeIngestService + ->buildChunkRecords($version); + + // -------------------------------------------------- + // Append in NDJSON + // -------------------------------------------------- + $this->chunkManager->appendChunks($records); + + // -------------------------------------------------- + // FAISS komplett neu bauen (deterministisch) + // -------------------------------------------------- + $logPath = $job->getLogPath(); + $this->vectorBuilder->rebuildFromNdjson($logPath); + + // -------------------------------------------------- + // Erfolg + // -------------------------------------------------- + $version->setIngestStatus(DocumentVersion::INGEST_INDEXED); + $this->jobService->markCompleted($job); + + $this->em->flush(); + + } catch (IndexStructureChangedException $e) { + + if ($job) { + $this->jobService->markFailed($job, $e->getMessage()); + } + + $version->setIngestStatus(DocumentVersion::INGEST_FAILED); + $this->em->flush(); + + throw $e; + + } catch (\Throwable $e) { + + if ($job) { + $this->jobService->markFailed($job, $e->getMessage()); + } + + $version->setIngestStatus(DocumentVersion::INGEST_FAILED); + $this->em->flush(); + + throw $e; + + } finally { + $this->lockService->release(); + } + + return $job; + } + + // ============================================================ + // GLOBAL REINDEX + // ============================================================ + + public function globalReindex(User $user): IngestJob + { + if (!$this->lockService->acquire()) { + throw new \RuntimeException('Another ingest job is already running.'); + } + + $job = null; + + try { + + $job = $this->jobService->startJob( + IngestJob::TYPE_GLOBAL_REINDEX, + $user + ); + + // -------------------------------------------------- + // Alle aktiven Dokumente neu ingestieren + // -------------------------------------------------- + $allRecords = $this->knowledgeIngestService + ->buildAllActiveChunkRecords(); + + // -------------------------------------------------- + // Komplettes NDJSON neu schreiben + // -------------------------------------------------- + $this->chunkManager->rewriteAll($allRecords); + + // -------------------------------------------------- + // FAISS komplett neu bauen + // -------------------------------------------------- + $logPath = $job->getLogPath(); + $this->vectorBuilder->rebuildFromNdjson($logPath); + + // -------------------------------------------------- + // Meta aktualisieren + index_version++ + // -------------------------------------------------- + $this->metaManager->writeMetaForGlobalReindex(); + + $this->jobService->markCompleted($job); + + } catch (\Throwable $e) { + + if ($job) { + $this->jobService->markFailed($job, $e->getMessage()); + } + + throw $e; + + } finally { + $this->lockService->release(); + } + + return $job; + } +} diff --git a/src/Knowledge/ChunkManager.php b/src/Knowledge/ChunkManager.php new file mode 100644 index 0000000..549dd56 --- /dev/null +++ b/src/Knowledge/ChunkManager.php @@ -0,0 +1,186 @@ +indexPath = rtrim($projectDir, '/') . $relativeIndexPath; + } + + public function getIndexPath(): string + { + return $this->indexPath; + } + + // ============================================================ + // APPEND + // ============================================================ + + /** + * @param iterable> $records + */ + public function appendChunks(iterable $records): void + { + $dir = \dirname($this->indexPath); + + if (!is_dir($dir) && !mkdir($dir, 0777, true) && !is_dir($dir)) { + throw new \RuntimeException('Unable to create index directory'); + } + + $handle = fopen($this->indexPath, 'ab'); + if (!$handle) { + throw new \RuntimeException('Unable to open index.ndjson for append'); + } + + foreach ($records as $record) { + $json = json_encode($record, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + if ($json === false) { + fclose($handle); + throw new \RuntimeException('Unable to encode chunk record'); + } + + fwrite($handle, $json . PHP_EOL); + } + + fclose($handle); + } + + // ============================================================ + // COMPACTION – Entfernt alle Chunks eines Dokuments + // ============================================================ + + public function compactByDocument(Uuid $documentId): void + { + if (!is_file($this->indexPath)) { + return; // nichts zu kompaktieren + } + + $tmpPath = $this->indexPath . '.tmp'; + + $in = fopen($this->indexPath, 'rb'); + $out = fopen($tmpPath, 'wb'); + + if (!$in || !$out) { + throw new \RuntimeException('Unable to open index for compaction'); + } + + $docIdString = $documentId->toRfc4122(); + + while (($line = fgets($in)) !== false) { + $line = trim($line); + if ($line === '') { + continue; + } + + $data = json_decode($line, true); + if (!is_array($data)) { + continue; // skip corrupted line + } + + if (($data['document_id'] ?? null) === $docIdString) { + continue; // skip this document's chunks + } + + fwrite($out, $line . PHP_EOL); + } + + fclose($in); + fclose($out); + + $this->atomicSwitch($tmpPath, $this->indexPath); + } + + // ============================================================ + // FULL REWRITE (Global Reindex) + // ============================================================ + + /** + * @param iterable> $records + */ + public function rewriteAll(iterable $records): void + { + $dir = \dirname($this->indexPath); + + if (!is_dir($dir) && !mkdir($dir, 0777, true) && !is_dir($dir)) { + throw new \RuntimeException('Unable to create index directory'); + } + + $tmpPath = $this->indexPath . '.tmp'; + + $handle = fopen($tmpPath, 'wb'); + if (!$handle) { + throw new \RuntimeException('Unable to open temp index file'); + } + + foreach ($records as $record) { + $json = json_encode($record, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + if ($json === false) { + fclose($handle); + throw new \RuntimeException('Unable to encode chunk record'); + } + + fwrite($handle, $json . PHP_EOL); + } + + fclose($handle); + + $this->atomicSwitch($tmpPath, $this->indexPath); + } + + // ============================================================ + // STREAM READ (für FAISS rebuild) + // ============================================================ + + /** + * @return \Generator> + */ + public function streamAll(): \Generator + { + if (!is_file($this->indexPath)) { + return; + } + + $handle = fopen($this->indexPath, 'rb'); + if (!$handle) { + throw new \RuntimeException('Unable to open index.ndjson for read'); + } + + try { + while (($line = fgets($handle)) !== false) { + $line = trim($line); + if ($line === '') { + continue; + } + + $data = json_decode($line, true); + if (is_array($data)) { + yield $data; + } + } + } finally { + fclose($handle); + } + } + + // ============================================================ + // INTERNAL ATOMIC SWITCH + // ============================================================ + + private function atomicSwitch(string $tmp, string $final): void + { + if (!rename($tmp, $final)) { + @unlink($tmp); + throw new \RuntimeException('Atomic switch failed for index.ndjson'); + } + } +} diff --git a/src/Knowledge/Ingest/KnowledgeIngestService.php b/src/Knowledge/Ingest/KnowledgeIngestService.php index 52bcb1c..647acad 100644 --- a/src/Knowledge/Ingest/KnowledgeIngestService.php +++ b/src/Knowledge/Ingest/KnowledgeIngestService.php @@ -1,39 +1,93 @@ > + */ + public function buildChunkRecords(DocumentVersion $version): iterable { - $text = $this->loader->load($path); - - if ($optimize) { - $text = preg_replace("/\n{3,}/", "\n\n", $text); - $text = preg_replace("/[ \t]+$/m", "", $text); - } - - $sourceHash = sha1($text); - $sourceName = basename($path); - - if ($this->indexWriter->hasSourceHash($sourceName, $sourceHash)) { - return []; - } + $text = $this->loader->load($version->getFilePath()); + $text = $this->optimizeText($text); $chunks = $this->chunker->chunk($text); - return $this->writer->write($sourceName, $chunks, $sourceHash); + + $documentId = $version->getDocument()->getId()->toRfc4122(); + $versionId = $version->getId()->toRfc4122(); + + $index = 0; + + foreach ($chunks as $chunkText) { + yield [ + 'chunk_id' => Uuid::v4()->toRfc4122(), + 'document_id' => $documentId, + 'version_id' => $versionId, + 'chunk_index' => $index++, + 'text' => $chunkText, + 'checksum' => sha1($chunkText), + 'metadata' => $this->buildMetadata($version), + ]; + } + } + + /** + * Global Reindex: iteriert streamingfähig über alle aktiven Versionen. + * Keine RAM-Explosion, da alles generatorbasiert bleibt. + * + * @return iterable> + */ + public function buildAllActiveChunkRecords(): iterable + { + foreach ($this->versionRepo->iterateActiveVersions() as $version) { + // yield from hält das Ganze streamingfähig (Generator-Kaskade) + yield from $this->buildChunkRecords($version); + } + } + + private function optimizeText(string $text): string + { + $text = preg_replace("/\n{3,}/", "\n\n", $text); + $text = preg_replace("/[ \t]+$/m", "", $text); + + return $text; + } + + /** + * @return array + */ + private function buildMetadata(DocumentVersion $version): array + { + $doc = $version->getDocument(); + + // Optional: Titel/Name, falls vorhanden + $title = null; + if (method_exists($doc, 'getTitle')) { + $title = $doc->getTitle(); + } elseif (method_exists($doc, 'getName')) { + $title = $doc->getName(); + } + + return array_filter([ + 'document_title' => $title, + 'version_number' => method_exists($version, 'getVersionNumber') ? $version->getVersionNumber() : null, + 'file_path' => $version->getFilePath(), + ], static fn($v) => $v !== null && $v !== ''); } } diff --git a/src/Repository/DocumentVersionRepository.php b/src/Repository/DocumentVersionRepository.php new file mode 100644 index 0000000..d9aff66 --- /dev/null +++ b/src/Repository/DocumentVersionRepository.php @@ -0,0 +1,32 @@ + + */ + public function iterateActiveVersions(): iterable + { + $qb = $this->createQueryBuilder('v') + ->andWhere('v.isActive = :active') + ->setParameter('active', true) + ->orderBy('v.createdAt', 'ASC'); + + return $qb->getQuery()->toIterable(); + } +} diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php new file mode 100644 index 0000000..b29153b --- /dev/null +++ b/src/Repository/UserRepository.php @@ -0,0 +1,43 @@ + + */ +class UserRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, User::class); + } + + // /** + // * @return User[] Returns an array of User objects + // */ + // public function findByExampleField($value): array + // { + // return $this->createQueryBuilder('u') + // ->andWhere('u.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('u.id', 'ASC') + // ->setMaxResults(10) + // ->getQuery() + // ->getResult() + // ; + // } + + // public function findOneBySomeField($value): ?User + // { + // return $this->createQueryBuilder('u') + // ->andWhere('u.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +} diff --git a/src/Service/DocumentService.php b/src/Service/DocumentService.php new file mode 100644 index 0000000..1cbb750 --- /dev/null +++ b/src/Service/DocumentService.php @@ -0,0 +1,123 @@ +setTitle($title); + $document->setCreatedBy($user); + + $version = new DocumentVersion(); + $version->setVersionNumber(1); + $version->setFilePath($filePath); + $version->setChecksum($this->calculateChecksum($filePath)); + $version->setCreatedBy($user); + $version->setActive(true); + + $document->addVersion($version); + $document->setCurrentVersion($version); + + $this->em->persist($document); + $this->em->persist($version); + $this->em->flush(); + + return $document; + } + + /** + * Fügt neue Version hinzu (immutable) + */ + public function addVersion( + Document $document, + string $filePath, + User $user + ): DocumentVersion { + + $nextVersionNumber = $this->getNextVersionNumber($document); + + $version = new DocumentVersion(); + $version->setVersionNumber($nextVersionNumber); + $version->setFilePath($filePath); + $version->setChecksum($this->calculateChecksum($filePath)); + $version->setCreatedBy($user); + $version->setActive(false); + + $document->addVersion($version); + + $this->em->persist($version); + $this->em->flush(); + + return $version; + } + + /** + * Aktiviert eine Version (setzt andere inaktiv) + */ + public function activateVersion(DocumentVersion $version): void + { + $document = $version->getDocument(); + + foreach ($document->getVersions() as $existingVersion) { + $existingVersion->setActive(false); + } + + $version->setActive(true); + $document->setCurrentVersion($version); + + $this->em->flush(); + } + + /** + * Archiviert Dokument + */ + public function archive(Document $document): void + { + $document->archive(); + $this->em->flush(); + } + + /** + * Berechnet SHA256 Checksum + */ + private function calculateChecksum(string $filePath): string + { + if (!file_exists($filePath)) { + throw new \RuntimeException('File not found for checksum.'); + } + + return hash_file('sha256', $filePath); + } + + /** + * Ermittelt nächste Versionsnummer + */ + private function getNextVersionNumber(Document $document): int + { + $max = 0; + + foreach ($document->getVersions() as $version) { + $max = max($max, $version->getVersionNumber()); + } + + return $max + 1; + } +} diff --git a/src/Service/IngestJobService.php b/src/Service/IngestJobService.php new file mode 100644 index 0000000..7689629 --- /dev/null +++ b/src/Service/IngestJobService.php @@ -0,0 +1,54 @@ +setStartedBy($user); + $job->setDocumentId($documentId); + $job->setDocumentVersionId($documentVersionId); + $job->setLogPath($logPath); + + $this->em->persist($job); + $this->em->flush(); + + return $job; + } + + public function markCompleted(IngestJob $job): void + { + $job->markCompleted(); + $this->em->flush(); + } + + public function markFailed(IngestJob $job, string $message): void + { + $job->markFailed($message); + $this->em->flush(); + } + + public function markAborted(IngestJob $job): void + { + $job->markAborted(); + $this->em->flush(); + } +} diff --git a/src/Service/IngestOrchestrator.php b/src/Service/IngestOrchestrator.php new file mode 100644 index 0000000..01595f6 --- /dev/null +++ b/src/Service/IngestOrchestrator.php @@ -0,0 +1,130 @@ +lockService->acquire()) { + throw new \RuntimeException('Another ingest job is already running.'); + } + + $job = null; + + try { + + // -------------------------------------- + // Job anlegen + // -------------------------------------- + $job = $this->jobService->startJob( + IngestJob::TYPE_DOCUMENT, + $user, + $version->getDocument()->getId(), + $version->getId(), + ); + + // -------------------------------------- + // Version Status RUNNING + // -------------------------------------- + $version->setIngestStatus(DocumentVersion::INGEST_RUNNING); + $this->em->flush(); + + // -------------------------------------- + // Simulierter Ablauf (noch kein echter Ingest) + // -------------------------------------- + if ($dryRun) { + usleep(200000); + } else { + // Später: + // - KnowledgeIngestService + // - ChunkWriter + // - VectorIngestCommand + } + + // -------------------------------------- + // Erfolg + // -------------------------------------- + $version->setIngestStatus(DocumentVersion::INGEST_INDEXED); + $this->jobService->markCompleted($job); + $this->em->flush(); + + } catch (\Throwable $e) { + + if ($job) { + $this->jobService->markFailed($job, $e->getMessage()); + } + + $version->setIngestStatus(DocumentVersion::INGEST_FAILED); + $this->em->flush(); + + throw $e; + + } finally { + $this->lockService->release(); + } + + return $job; + } + + /** + * Globaler Reindex + */ + public function runGlobal(User $user, bool $dryRun = false): IngestJob + { + if (!$this->lockService->acquire()) { + throw new \RuntimeException('Another ingest job is already running.'); + } + + $job = null; + + try { + + $job = $this->jobService->startJob( + IngestJob::TYPE_GLOBAL_REINDEX, + $user + ); + + if ($dryRun) { + usleep(200000); + } else { + // Später: + // - Alle aktiven Dokumente neu ingestieren + // - Global vector rebuild + } + + $this->jobService->markCompleted($job); + + } catch (\Throwable $e) { + + if ($job) { + $this->jobService->markFailed($job, $e->getMessage()); + } + + throw $e; + + } finally { + $this->lockService->release(); + } + + return $job; + } +} diff --git a/src/Service/LockService.php b/src/Service/LockService.php new file mode 100644 index 0000000..41af597 --- /dev/null +++ b/src/Service/LockService.php @@ -0,0 +1,71 @@ +lockFile = $dir . '/ingest.lock'; + } + + /** + * Gibt true zurück, wenn Lock erfolgreich gesetzt wurde. + * Wenn false: ein anderer Prozess hält den Lock. + */ + public function acquire(): bool + { + $handle = fopen($this->lockFile, 'c+'); + if (!$handle) { + throw new \RuntimeException('Could not open lock file.'); + } + + // Nicht-blockierend: sofort true/false + if (!flock($handle, LOCK_EX | LOCK_NB)) { + fclose($handle); + return false; + } + + // Lock halten: Handle offen lassen + $this->handle = $handle; + + // Optional: Metainfo reinschreiben + ftruncate($this->handle, 0); + fwrite($this->handle, (string) time()); + + return true; + } + + public function release(): void + { + if ($this->handle) { + flock($this->handle, LOCK_UN); + fclose($this->handle); + $this->handle = null; + } + } + + public function isLocked(): bool + { + $handle = fopen($this->lockFile, 'c+'); + if (!$handle) { + return false; + } + + $locked = !flock($handle, LOCK_EX | LOCK_NB); + if (!$locked) { + flock($handle, LOCK_UN); + } + fclose($handle); + + return $locked; + } +} diff --git a/src/Vector/VectorIndexBuilder.php b/src/Vector/VectorIndexBuilder.php new file mode 100644 index 0000000..6d3efc3 --- /dev/null +++ b/src/Vector/VectorIndexBuilder.php @@ -0,0 +1,164 @@ +pythonBin = $pythonBin; + $this->scriptPath = $base . $relativeScriptPath; + $this->indexNdjsonPath = $base . $relativeIndexNdjsonPath; + $this->vectorIndexPath = $base . $relativeVectorIndexPath; + $this->timeoutSeconds = $timeoutSeconds; + } + + public function getIndexNdjsonPath(): string + { + return $this->indexNdjsonPath; + } + + public function getVectorIndexPath(): string + { + return $this->vectorIndexPath; + } + + public function getScriptPath(): string + { + return $this->scriptPath; + } + + /** + * Rebuild FAISS Index deterministisch aus index.ndjson. + * + * Erwartung: Python schreibt in $tmpVectorIndexPath, wir schalten atomar um. + * + * @param string|null $logPath Optional: stdout/stderr dorthin appenden + */ + public function rebuildFromNdjson(?string $logPath = null): void + { + if (!is_file($this->scriptPath)) { + throw new \RuntimeException('vector_ingest.py not found at: ' . $this->scriptPath); + } + + if (!is_file($this->indexNdjsonPath)) { + throw new \RuntimeException('index.ndjson not found at: ' . $this->indexNdjsonPath); + } + + $dir = \dirname($this->vectorIndexPath); + if (!is_dir($dir) && !mkdir($dir, 0777, true) && !is_dir($dir)) { + throw new \RuntimeException('Unable to create vector index directory: ' . $dir); + } + + $tmpVectorIndexPath = $this->vectorIndexPath . '.tmp'; + + // Vorheriges tmp entfernen (Sicherheit) + if (is_file($tmpVectorIndexPath)) { + @unlink($tmpVectorIndexPath); + } + + // ---------------------------- + // Python-Aufruf (konservativ) + // ---------------------------- + // Wir erwarten/standardisieren (ab jetzt) CLI-Args: + // --index + // --out + // + // Falls dein Python-Script aktuell andere Args hat, + // passen wir es im nächsten Schritt konsistent an. + $cmd = [ + $this->pythonBin, + $this->scriptPath, + '--index', $this->indexNdjsonPath, + '--out', $tmpVectorIndexPath, + ]; + + $process = new Process($cmd); + $process->setTimeout($this->timeoutSeconds); + + $this->runProcess($process, $logPath); + + // Python muss tmp erzeugt haben + if (!is_file($tmpVectorIndexPath) || filesize($tmpVectorIndexPath) === 0) { + throw new \RuntimeException('Vector index rebuild failed: tmp output missing or empty: ' . $tmpVectorIndexPath); + } + + // Atomarer Switch + $this->atomicSwitch($tmpVectorIndexPath, $this->vectorIndexPath); + } + + // ------------------------- + // Internals + // ------------------------- + + private function runProcess(Process $process, ?string $logPath): void + { + if ($logPath !== null) { + $this->appendLog($logPath, "\n=== VectorIndexBuilder START " . (new \DateTimeImmutable())->format(DATE_ATOM) . " ===\n"); + $this->appendLog($logPath, "CMD: " . $process->getCommandLine() . "\n"); + } + + $process->run(function (string $type, string $buffer) use ($logPath) { + if ($logPath === null) { + return; + } + + // TYPE: Process::OUT / Process::ERR + $this->appendLog($logPath, $buffer); + }); + + if (!$process->isSuccessful()) { + if ($logPath !== null) { + $this->appendLog($logPath, "\n=== VectorIndexBuilder FAILED ===\n"); + $this->appendLog($logPath, "ExitCode: " . $process->getExitCode() . "\n"); + $this->appendLog($logPath, "STDERR:\n" . $process->getErrorOutput() . "\n"); + } + + throw new ProcessFailedException($process); + } + + if ($logPath !== null) { + $this->appendLog($logPath, "\n=== VectorIndexBuilder OK " . (new \DateTimeImmutable())->format(DATE_ATOM) . " ===\n"); + } + } + + private function appendLog(string $logPath, string $content): void + { + $dir = \dirname($logPath); + if (!is_dir($dir) && !mkdir($dir, 0777, true) && !is_dir($dir)) { + // Wenn Log nicht möglich ist: nicht hart scheitern (Build ist wichtiger) + return; + } + + @file_put_contents($logPath, $content, FILE_APPEND); + } + + private function atomicSwitch(string $tmp, string $final): void + { + if (!rename($tmp, $final)) { + @unlink($tmp); + throw new \RuntimeException('Atomic switch failed for vector.index'); + } + } +} diff --git a/src/Vector/vector_ingest.py b/src/Vector/vector_ingest.py index ed9f135..fd97374 100644 --- a/src/Vector/vector_ingest.py +++ b/src/Vector/vector_ingest.py @@ -2,88 +2,125 @@ import sys import json +import argparse from pathlib import Path # --------------------------------------------------------- -# Argument handling +# Argument parsing # --------------------------------------------------------- -if len(sys.argv) < 3: - print("ERROR: Missing arguments (vectorDir, knowledgeDir)") - sys.exit(2) +parser = argparse.ArgumentParser(description="Build FAISS index from NDJSON") -vector_dir = Path(sys.argv[1]).resolve() -knowledge_dir = Path(sys.argv[2]).resolve() +parser.add_argument("--index", required=True, help="Path to index.ndjson") +parser.add_argument("--out", required=True, help="Path to output vector.index") +parser.add_argument("--model", default="all-MiniLM-L6-v2", help="SentenceTransformer model") -index_json = knowledge_dir / "index.json" -index_out = vector_dir / "vector.index" -meta_out = vector_dir / "vector_meta.json" +args = parser.parse_args() + +index_path = Path(args.index).resolve() +out_path = Path(args.out).resolve() # --------------------------------------------------------- # Dependency checks # --------------------------------------------------------- try: - import faiss # noqa + import faiss except Exception: print("ERROR: Python module 'faiss' not found.") sys.exit(10) try: - from sentence_transformers import SentenceTransformer # noqa + from sentence_transformers import SentenceTransformer except Exception: print("ERROR: Python module 'sentence-transformers' not found.") sys.exit(11) +import numpy as np import faiss from sentence_transformers import SentenceTransformer # --------------------------------------------------------- # File checks # --------------------------------------------------------- -if not index_json.is_file(): - print(f"ERROR: index.json not found at {index_json}") +if not index_path.is_file(): + print(f"ERROR: index.ndjson not found at {index_path}") sys.exit(20) # --------------------------------------------------------- -# Load chunks from index.json +# Load model # --------------------------------------------------------- -with open(index_json, "r", encoding="utf-8") as f: - data = json.load(f) +print(f"Loading embedding model: {args.model}") +model = SentenceTransformer(args.model) +# --------------------------------------------------------- +# Streaming read NDJSON +# --------------------------------------------------------- texts = [] ids = [] -for entry in data: - if "file" not in entry: - continue +print("Reading NDJSON...") - chunk_path = knowledge_dir / "chunks" / entry["file"] - if not chunk_path.is_file(): - continue +with open(index_path, "r", encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line: + continue - text = chunk_path.read_text(encoding="utf-8").strip() - if not text: - continue + try: + entry = json.loads(line) + except Exception: + continue - texts.append(text) - ids.append(entry["file"]) + text = entry.get("text") + chunk_id = entry.get("chunk_id") + + if not text or not chunk_id: + continue + + texts.append(text) + ids.append(chunk_id) if not texts: - print("ERROR: No chunks loaded from index.json") + print("ERROR: No valid chunks found in index.ndjson") sys.exit(21) +print(f"Loaded {len(texts)} chunks.") + # --------------------------------------------------------- -# Build vector index +# Build embeddings # --------------------------------------------------------- -model = SentenceTransformer("all-MiniLM-L6-v2") -embeddings = model.encode(texts, normalize_embeddings=True) +print("Encoding embeddings...") +embeddings = model.encode( + texts, + normalize_embeddings=True, + show_progress_bar=True, + batch_size=64 +) + +embeddings = np.array(embeddings).astype("float32") dim = embeddings.shape[1] +print(f"Embedding dimension: {dim}") + +# --------------------------------------------------------- +# Build FAISS index +# --------------------------------------------------------- +print("Building FAISS index...") index = faiss.IndexFlatIP(dim) index.add(embeddings) -faiss.write_index(index, str(index_out)) +# Ensure output directory exists +out_path.parent.mkdir(parents=True, exist_ok=True) -with open(meta_out, "w", encoding="utf-8") as f: +print(f"Writing FAISS index to {out_path}") +faiss.write_index(index, str(out_path)) + +# --------------------------------------------------------- +# Write ID mapping meta +# --------------------------------------------------------- +meta_path = out_path.with_suffix(".meta.json") + +with open(meta_path, "w", encoding="utf-8") as f: json.dump(ids, f) -print(f"Indexed {len(ids)} chunks.") +print(f"Indexed {len(ids)} chunks successfully.") +sys.exit(0) diff --git a/symfony.lock b/symfony.lock index 1d5d3d5..79374c6 100644 --- a/symfony.lock +++ b/symfony.lock @@ -1,4 +1,40 @@ { + "doctrine/deprecations": { + "version": "1.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.0", + "ref": "87424683adc81d7dc305eefec1fced883084aab9" + } + }, + "doctrine/doctrine-bundle": { + "version": "2.18", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "2.13", + "ref": "620b57f496f2e599a6015a9fa222c2ee0a32adcb" + }, + "files": [ + "config/packages/doctrine.yaml", + "src/Entity/.gitignore", + "src/Repository/.gitignore" + ] + }, + "doctrine/doctrine-migrations-bundle": { + "version": "3.7", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "3.1", + "ref": "1d01ec03c6ecbd67c3375c5478c9a423ae5d6a33" + }, + "files": [ + "config/packages/doctrine_migrations.yaml", + "migrations/.gitignore" + ] + }, "symfony/console": { "version": "5.4", "recipe": { @@ -42,6 +78,15 @@ "src/Kernel.php" ] }, + "symfony/maker-bundle": { + "version": "1.66", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.0", + "ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f" + } + }, "symfony/monolog-bundle": { "version": "4.0", "recipe": { @@ -54,6 +99,18 @@ "config/packages/monolog.yaml" ] }, + "symfony/property-info": { + "version": "7.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "7.3", + "ref": "dae70df71978ae9226ae915ffd5fad817f5ca1f7" + }, + "files": [ + "config/packages/property_info.yaml" + ] + }, "symfony/routing": { "version": "5.4", "recipe": { @@ -67,6 +124,32 @@ "config/routes.yaml" ] }, + "symfony/security-bundle": { + "version": "7.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "7.4", + "ref": "c42fee7802181cdd50f61b8622715829f5d2335c" + }, + "files": [ + "config/packages/security.yaml", + "config/routes/security.yaml" + ] + }, + "symfony/twig-bundle": { + "version": "7.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "6.4", + "ref": "cab5fd2a13a45c266d45a7d9337e28dee6272877" + }, + "files": [ + "config/packages/twig.yaml", + "templates/base.html.twig" + ] + }, "symfony/uid": { "version": "7.4", "recipe": { diff --git a/templates/admin/base.html.twig b/templates/admin/base.html.twig new file mode 100644 index 0000000..481b107 --- /dev/null +++ b/templates/admin/base.html.twig @@ -0,0 +1,49 @@ + + + + + + {% block title %}Admin{% endblock %} + + + + + + + + +
+
+
+ +
+ +
+ {% block body %}{% endblock %} +
+
+
+ + + diff --git a/templates/admin/dashboard/index.html.twig b/templates/admin/dashboard/index.html.twig new file mode 100644 index 0000000..255a8f7 --- /dev/null +++ b/templates/admin/dashboard/index.html.twig @@ -0,0 +1,21 @@ +{% extends 'admin/base.html.twig' %} + +{% block title %}Admin Dashboard{% endblock %} + +{% block body %} +

Dashboard

+ +
+
+
+ User: {{ app.user.userIdentifier }} +
+
+ Rollen: {{ app.user.roles|join(', ') }} +
+ +
+ +
+
+{% endblock %} diff --git a/templates/admin/document/index.html.twig b/templates/admin/document/index.html.twig new file mode 100644 index 0000000..dc720bb --- /dev/null +++ b/templates/admin/document/index.html.twig @@ -0,0 +1,61 @@ +{% extends 'admin/base.html.twig' %} + +{% block title %}Dokumente{% endblock %} + +{% block body %} +
+

Dokumente

+ + + Neues Dokument + +
+ + {% if documents is empty %} +
+ Keine Dokumente vorhanden. +
+ {% else %} +
+
+ + + + + + + + + + + + {% for document in documents %} + + + + + + + + {% endfor %} + +
TitelStatusVersionenAktive VersionErstellt am
+ + {{ document.title }} + + + {% if document.status == 'ACTIVE' %} + Aktiv + {% else %} + Archiviert + {% endif %} + {{ document.versions|length }} + {% if document.currentVersion %} + v{{ document.currentVersion.versionNumber }} + {% else %} + - + {% endif %} + {{ document.createdAt|date('d.m.Y H:i') }}
+
+
+ {% endif %} +{% endblock %} diff --git a/templates/admin/document/new.html.twig b/templates/admin/document/new.html.twig new file mode 100644 index 0000000..20fac7d --- /dev/null +++ b/templates/admin/document/new.html.twig @@ -0,0 +1,23 @@ +{% extends 'admin/base.html.twig' %} + +{% block title %}Neues Dokument{% endblock %} + +{% block body %} +

Neues Dokument

+ +
+ +
+ + +
+ +
+ + +
+ + + +
+{% endblock %} diff --git a/templates/admin/document/new_version.html.twig b/templates/admin/document/new_version.html.twig new file mode 100644 index 0000000..416e98d --- /dev/null +++ b/templates/admin/document/new_version.html.twig @@ -0,0 +1,29 @@ +{% extends 'admin/base.html.twig' %} + +{% block title %}Neue Version{% endblock %} + +{% block body %} + + + ← Zurück + + +

+ Neue Version für: {{ document.title }} +

+ +
+ +
+ + +
+ + + +
+ +{% endblock %} diff --git a/templates/admin/document/show.html.twig b/templates/admin/document/show.html.twig new file mode 100644 index 0000000..a85fc7f --- /dev/null +++ b/templates/admin/document/show.html.twig @@ -0,0 +1,138 @@ +{% extends 'admin/base.html.twig' %} + +{% block title %}Dokument{% endblock %} + +{% block body %} + + + ← Zurück + + +

{{ document.title }}

+ +
+
+ +
+ Status: + {% if document.status == 'ACTIVE' %} + Aktiv + {% else %} + Archiviert + {% endif %} +
+ +
+ Erstellt von: + {{ document.createdBy.email }} +
+ +
+ Erstellt am: + {{ document.createdAt|date('d.m.Y H:i') }} +
+ +
+ Aktive Version: + {% if document.currentVersion %} + v{{ document.currentVersion.versionNumber }} + {% else %} + - + {% endif %} +
+ +
+
+ +

Versionen

+ + + Neue Version + + {% if document.versions is empty %} +
+ Keine Versionen vorhanden. +
+ {% else %} +
+
+ + + + + + + + + + + + + + {% for version in document.versions %} + + + + + + + + + + + + + + + + {% endfor %} + +
VersionAktivIngestChecksumErstellt vonDatumAktion
v{{ version.versionNumber }} + {% if version.isActive %} + Ja + {% else %} + Nein + {% endif %} + + {% if version.ingestStatus == 'INDEXED' %} + INDEXED + {% elseif version.ingestStatus == 'RUNNING' %} + RUNNING + {% elseif version.ingestStatus == 'FAILED' %} + FAILED + {% else %} + PENDING + {% endif %} + + {{ version.checksum[:10] }}... + + {{ version.createdBy.email }} + + {{ version.createdAt|date('d.m.Y H:i') }} + + {% if version.isActive %} + + {% if version.ingestStatus != constant('App\\Entity\\DocumentVersion::INGEST_RUNNING') %} + +
+ + +
+ + {% else %} + Läuft... + {% endif %} + + {% else %} + Nicht aktiv + {% endif %} +
+
+
+ {% endif %} + +{% endblock %} diff --git a/templates/admin/job/index.html.twig b/templates/admin/job/index.html.twig new file mode 100644 index 0000000..b2e3ae4 --- /dev/null +++ b/templates/admin/job/index.html.twig @@ -0,0 +1,109 @@ +{% extends 'admin/base.html.twig' %} + +{% block title %}Ingest Jobs{% endblock %} + +{% block body %} + +

Ingest Jobs

+ +
+ + +
+ + {% if jobs is empty %} +
+ Keine Jobs vorhanden. +
+ {% else %} + +
+
+ + + + + + + + + + + + + + + + + {% for job in jobs %} + + + + + + + + + + + + + + + + + + {% endfor %} + + +
IDTypStatusDokumentVersionGestartetBeendetUser
+ + {{ job.id }} + + {{ job.type }} + {% if job.status == 'COMPLETED' %} + COMPLETED + {% elseif job.status == 'RUNNING' %} + RUNNING + {% elseif job.status == 'FAILED' %} + FAILED + {% else %} + {{ job.status }} + {% endif %} + + {% if job.documentId %} + {{ job.documentId }} + {% else %} + - + {% endif %} + + {% if job.documentVersionId %} + {{ job.documentVersionId }} + {% else %} + - + {% endif %} + {{ job.startedAt|date('d.m.Y H:i:s') }} + {% if job.finishedAt %} + {{ job.finishedAt|date('d.m.Y H:i:s') }} + {% else %} + - + {% endif %} + + {% if job.startedBy %} + {{ job.startedBy.email }} + {% else %} + - + {% endif %} +
+
+
+ + {% endif %} + +{% endblock %} diff --git a/templates/admin/job/show.html.twig b/templates/admin/job/show.html.twig new file mode 100644 index 0000000..4a959c9 --- /dev/null +++ b/templates/admin/job/show.html.twig @@ -0,0 +1,88 @@ +{% extends 'admin/base.html.twig' %} + +{% block title %}Ingest Job{% endblock %} + +{% block body %} + + + ← Zurück + + +

Ingest Job

+ +
+
+ +
+ ID: {{ job.id }} +
+ +
+ Typ: {{ job.type }} +
+ +
+ Status: + {% if job.status == 'COMPLETED' %} + COMPLETED + {% elseif job.status == 'RUNNING' %} + RUNNING + {% elseif job.status == 'FAILED' %} + FAILED + {% else %} + {{ job.status }} + {% endif %} +
+ +
+ Dokument: + {{ job.documentId ?? '-' }} +
+ +
+ Version: + {{ job.documentVersionId ?? '-' }} +
+ +
+ Gestartet: + {{ job.startedAt|date('d.m.Y H:i:s') }} +
+ +
+ Beendet: + {% if job.finishedAt %} + {{ job.finishedAt|date('d.m.Y H:i:s') }} + {% else %} + - + {% endif %} +
+ +
+ Gestartet von: + {% if job.startedBy %} + {{ job.startedBy.email }} + {% else %} + - + {% endif %} +
+ + {% if job.errorMessage %} +
+ Fehler:
+ {{ job.errorMessage }} +
+ {% endif %} + + {% if job.logPath %} +
+ Log Datei:
+ {{ job.logPath }} +
+ {% endif %} + +
+
+ +{% endblock %} diff --git a/templates/admin/security/login.html.twig b/templates/admin/security/login.html.twig new file mode 100644 index 0000000..d185d8c --- /dev/null +++ b/templates/admin/security/login.html.twig @@ -0,0 +1,36 @@ +{% extends 'admin/base.html.twig' %} + +{% block title %}Admin Login{% endblock %} + +{% block body %} +
+
+ +

Admin Login

+ + {% if error %} +
+ {{ error.messageKey|trans(error.messageData, 'security') }} +
+ {% endif %} + +
+
+ + +
+ +
+ + +
+ + {# CSRF #} + + + +
+ +
+
+{% endblock %} diff --git a/templates/base.html.twig b/templates/base.html.twig new file mode 100644 index 0000000..1069c14 --- /dev/null +++ b/templates/base.html.twig @@ -0,0 +1,16 @@ + + + + + {% block title %}Welcome!{% endblock %} + + {% block stylesheets %} + {% endblock %} + + {% block javascripts %} + {% endblock %} + + + {% block body %}{% endblock %} + +