diff --git a/.env b/.env index 79bdcefe..2ea04325 100644 --- a/.env +++ b/.env @@ -155,3 +155,11 @@ APP_ENV=prod APP_SECRET=a03498528f5a5fc089273ec9ae5b2849 APP_SHARE_DIR=var/share ###< symfony/framework-bundle ### + +###> symfony/ai-generic-platform ### +# GENERIC_BASE_URL=https://api.example.com/v1 +###< symfony/ai-generic-platform ### + +###> symfony/ai-open-router-platform ### +OPENROUTER_API_KEY= +###< symfony/ai-open-router-platform ### diff --git a/README.md b/README.md index ad37e9c6..b857711f 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ for the first time. * Automatic thumbnail generation for pictures * Use cloud providers (like Octopart, Digikey, Farnell, LCSC or TME) to automatically get part information, datasheets, and prices for parts +* Retrieve part information from arbitrary shop websites, using either conventional data extraction from structured metadata, or AI based data extraction * API to access Part-DB from other applications/scripts * [Integration with KiCad](https://docs.part-db.de/usage/eda_integration.html): Use Part-DB as the central datasource for your KiCad and see available parts from Part-DB directly inside KiCad. diff --git a/assets/controllers/elements/ai_model_autocomplete_controller.js b/assets/controllers/elements/ai_model_autocomplete_controller.js new file mode 100644 index 00000000..e36e6b1f --- /dev/null +++ b/assets/controllers/elements/ai_model_autocomplete_controller.js @@ -0,0 +1,152 @@ +/* + * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony). + * + * Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import {Controller} from "@hotwired/stimulus"; + +import "tom-select/dist/css/tom-select.bootstrap5.css"; +import '../../css/components/tom-select_extensions.css'; +import TomSelect from "tom-select"; + +import TomSelect_click_to_edit from '../../tomselect/click_to_edit/click_to_edit' +import TomSelect_autoselect_typed from '../../tomselect/autoselect_typed/autoselect_typed' + +TomSelect.define('click_to_edit', TomSelect_click_to_edit) +TomSelect.define('autoselect_typed', TomSelect_autoselect_typed) + +export default class extends Controller { + _tomSelect; + + _platformSelector; + + connect() { + + let dropdownParent = "body"; + if (this.element.closest('.modal')) { + dropdownParent = null + } + + //Try to find the platform selector + const platformSelector = document.querySelector("select[data-platform-selector-label='" + this.element.dataset.platformSelector + "']"); + //Clear tomselect options, if the platform selector changes + if (platformSelector) { + this.platformSelector = platformSelector; + platformSelector.addEventListener('change', () => { + //Force reload of options by clearing the cache and options of TomSelect and triggering a search with an empty string + this._tomSelect.clearOptions(); + this._tomSelect.clearCache(); + this._tomSelect.load(''); + }); + } + + let settings = { + persistent: false, + create: true, + maxItems: 1, + preload: 'focus', + createOnBlur: true, + selectOnTab: true, + clearAfterSelect: true, + shouldLoad: ((query) => true), + maxOptions: null, + //This a an ugly solution to disable the delimiter parsing of the TomSelect plugin + delimiter: 'VERY_L0NG_D€LIMITER_WHICH_WILL_NEVER_BE_ENCOUNTERED_IN_A_STRING', + dropdownParent: dropdownParent, + render: { + item: (data, escape) => { + return '' + escape(data.label) + ''; + }, + option: (data, escape) => { + if (data.image) { + return "
" + data.label + "
" + } + return '
' + escape(data.label) + '
'; + } + }, + plugins: { + 'autoselect_typed': {}, + 'click_to_edit': {}, + 'clear_button': {}, + "restore_on_backspace": {} + } + }; + + if(this.element.dataset.urlTemplate) { + const base_url = this.element.dataset.urlTemplate; + settings.searchField = "label"; + settings.sortField = "label"; + settings.valueField = "label"; + settings.load = (query, callback) => { + + + if (!this.platformSelector) { + console.error("Platform selector not found for AI model autocomplete"); + callback(); + return; + } + + //Platform is the selected option + const platform = this.platformSelector.value; + if (!platform) { + callback(); + return; + } + + const self = this; + + //Only fetch each platform once + if(self.platformLoaded === platform) { + callback(); + } + + + const url = base_url.replace('__PLATFORM__', encodeURIComponent(platform)); + + fetch(url) + .then(response => response.json()) + .then(json => { + + self.platformLoaded = platform; + + var data = []; + + for (const name in json) { + data.push({ + "label": name, + "capabilities": json[name].capabilities, + }); + } + + callback(data); + }).catch(()=>{ + callback(); + }); + }; + } + this._tomSelect = new TomSelect(this.element, settings); + } + + disconnect() { + super.disconnect(); + //Destroy the TomSelect instance + this._tomSelect.destroy(); + } + +} + + diff --git a/composer.json b/composer.json index 9d51033e..b23ea92b 100644 --- a/composer.json +++ b/composer.json @@ -33,6 +33,7 @@ "jbtronics/dompdf-font-loader-bundle": "^1.0.0", "jbtronics/settings-bundle": "^3.0.0", "jfcherng/php-diff": "^6.14", + "jkphl/micrometa": "^v3.4.0", "knpuniversity/oauth2-client-bundle": "^2.15", "league/commonmark": "^2.7", "league/csv": "^9.8.0", @@ -56,6 +57,9 @@ "scheb/2fa-trusted-device": "^v7.11.0", "shivas/versioning-bundle": "^4.0", "spatie/db-dumper": "^3.3.1", + "symfony/ai-bundle": "^0.8.0", + "symfony/ai-lm-studio-platform": "^0.8.0", + "symfony/ai-open-router-platform": "^0.8.0", "symfony/apache-pack": "^1.0", "symfony/asset": "7.4.*", "symfony/console": "7.4.*", diff --git a/composer.lock b/composer.lock index 3ad11a45..52eb5562 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8fd737684b48f8d24fcad35fce37a297", + "content-hash": "31a276e9a2b45a04facbe2d88f4a042f", "packages": [ { "name": "amphp/amp", @@ -977,16 +977,16 @@ }, { "name": "api-platform/doctrine-common", - "version": "v4.3.3", + "version": "v4.3.4", "source": { "type": "git", "url": "https://github.com/api-platform/doctrine-common.git", - "reference": "0b0e9328e08f38ff381afd7cba68976ed1dc714a" + "reference": "2072247e3c8126d815f20324e7aaa97c2b5ee889" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/doctrine-common/zipball/0b0e9328e08f38ff381afd7cba68976ed1dc714a", - "reference": "0b0e9328e08f38ff381afd7cba68976ed1dc714a", + "url": "https://api.github.com/repos/api-platform/doctrine-common/zipball/2072247e3c8126d815f20324e7aaa97c2b5ee889", + "reference": "2072247e3c8126d815f20324e7aaa97c2b5ee889", "shasum": "" }, "require": { @@ -1004,7 +1004,7 @@ "doctrine/mongodb-odm": "^2.10", "doctrine/orm": "^2.17 || ^3.0", "phpspec/prophecy-phpunit": "^2.2", - "phpunit/phpunit": "^12.2", + "phpunit/phpunit": "^11.5 || ^12.2", "symfony/type-info": "^7.3 || ^8.0" }, "suggest": { @@ -1061,22 +1061,22 @@ "rest" ], "support": { - "source": "https://github.com/api-platform/doctrine-common/tree/v4.3.3" + "source": "https://github.com/api-platform/doctrine-common/tree/v4.3.4" }, - "time": "2026-03-27T06:51:10+00:00" + "time": "2026-04-30T12:21:24+00:00" }, { "name": "api-platform/doctrine-orm", - "version": "v4.3.3", + "version": "v4.3.4", "source": { "type": "git", "url": "https://github.com/api-platform/doctrine-orm.git", - "reference": "553f7fe120c840a8db08d8e970860e209f40ebc2" + "reference": "3dc88ee48ffcdb6eee45ec1d3e9f25ea2aad4eaa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/doctrine-orm/zipball/553f7fe120c840a8db08d8e970860e209f40ebc2", - "reference": "553f7fe120c840a8db08d8e970860e209f40ebc2", + "url": "https://api.github.com/repos/api-platform/doctrine-orm/zipball/3dc88ee48ffcdb6eee45ec1d3e9f25ea2aad4eaa", + "reference": "3dc88ee48ffcdb6eee45ec1d3e9f25ea2aad4eaa", "shasum": "" }, "require": { @@ -1091,7 +1091,7 @@ "require-dev": { "doctrine/doctrine-bundle": "^2.11 || ^3.1", "phpspec/prophecy-phpunit": "^2.2", - "phpunit/phpunit": "^12.2", + "phpunit/phpunit": "^11.5 || ^12.2", "ramsey/uuid": "^4.7", "ramsey/uuid-doctrine": "^2.0", "symfony/cache": "^6.4 || ^7.0 || ^8.0", @@ -1150,22 +1150,22 @@ "rest" ], "support": { - "source": "https://github.com/api-platform/doctrine-orm/tree/v4.3.3" + "source": "https://github.com/api-platform/doctrine-orm/tree/v4.3.4" }, - "time": "2026-03-27T06:51:10+00:00" + "time": "2026-04-30T12:21:24+00:00" }, { "name": "api-platform/documentation", - "version": "v4.3.3", + "version": "v4.3.4", "source": { "type": "git", "url": "https://github.com/api-platform/documentation.git", - "reference": "a63a3a7f020b6a001b1228c5bd916e1a76887d1d" + "reference": "f07b444aef1f75bb07beb9f8d799213f05070e5f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/documentation/zipball/a63a3a7f020b6a001b1228c5bd916e1a76887d1d", - "reference": "a63a3a7f020b6a001b1228c5bd916e1a76887d1d", + "url": "https://api.github.com/repos/api-platform/documentation/zipball/f07b444aef1f75bb07beb9f8d799213f05070e5f", + "reference": "f07b444aef1f75bb07beb9f8d799213f05070e5f", "shasum": "" }, "require": { @@ -1173,7 +1173,7 @@ "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^12.2" + "phpunit/phpunit": "^11.5 || ^12.2" }, "type": "project", "extra": { @@ -1213,22 +1213,22 @@ ], "description": "API Platform documentation controller.", "support": { - "source": "https://github.com/api-platform/documentation/tree/v4.3.3" + "source": "https://github.com/api-platform/documentation/tree/v4.3.4" }, - "time": "2026-03-06T15:07:49+00:00" + "time": "2026-04-30T12:21:24+00:00" }, { "name": "api-platform/http-cache", - "version": "v4.3.3", + "version": "v4.3.4", "source": { "type": "git", "url": "https://github.com/api-platform/http-cache.git", - "reference": "9ffbe58f8872932ed7c2afca8207c2d68629e037" + "reference": "dd7c092b9abee06e72fd58544fe714b6c2a61efa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/http-cache/zipball/9ffbe58f8872932ed7c2afca8207c2d68629e037", - "reference": "9ffbe58f8872932ed7c2afca8207c2d68629e037", + "url": "https://api.github.com/repos/api-platform/http-cache/zipball/dd7c092b9abee06e72fd58544fe714b6c2a61efa", + "reference": "dd7c092b9abee06e72fd58544fe714b6c2a61efa", "shasum": "" }, "require": { @@ -1240,7 +1240,7 @@ "require-dev": { "guzzlehttp/guzzle": "^6.0 || ^7.0 || ^8.0", "phpspec/prophecy-phpunit": "^2.2", - "phpunit/phpunit": "^12.2", + "phpunit/phpunit": "^11.5 || ^12.2", "symfony/dependency-injection": "^6.4 || ^7.0 || ^8.0", "symfony/http-client": "^6.4 || ^7.0 || ^8.0", "symfony/type-info": "^7.3 || ^8.0" @@ -1293,22 +1293,22 @@ "rest" ], "support": { - "source": "https://github.com/api-platform/http-cache/tree/v4.3.3" + "source": "https://github.com/api-platform/http-cache/tree/v4.3.4" }, - "time": "2026-03-06T15:07:49+00:00" + "time": "2026-04-30T12:21:24+00:00" }, { "name": "api-platform/hydra", - "version": "v4.3.3", + "version": "v4.3.4", "source": { "type": "git", "url": "https://github.com/api-platform/hydra.git", - "reference": "bb9f7107a0bbfbe6e7b93130bde9829f0cc71529" + "reference": "9b0a677b21ee4f2ec255386a84bdcf1d12ea7bc4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/hydra/zipball/bb9f7107a0bbfbe6e7b93130bde9829f0cc71529", - "reference": "bb9f7107a0bbfbe6e7b93130bde9829f0cc71529", + "url": "https://api.github.com/repos/api-platform/hydra/zipball/9b0a677b21ee4f2ec255386a84bdcf1d12ea7bc4", + "reference": "9b0a677b21ee4f2ec255386a84bdcf1d12ea7bc4", "shasum": "" }, "require": { @@ -1328,7 +1328,7 @@ "api-platform/doctrine-orm": "^4.3", "phpspec/prophecy": "^1.19", "phpspec/prophecy-phpunit": "^2.2", - "phpunit/phpunit": "^12.2" + "phpunit/phpunit": "^11.5 || ^12.2" }, "type": "library", "extra": { @@ -1380,22 +1380,22 @@ "rest" ], "support": { - "source": "https://github.com/api-platform/hydra/tree/v4.3.3" + "source": "https://github.com/api-platform/hydra/tree/v4.3.4" }, - "time": "2026-03-20T09:00:10+00:00" + "time": "2026-04-30T12:21:24+00:00" }, { "name": "api-platform/json-api", - "version": "v4.3.3", + "version": "v4.3.4", "source": { "type": "git", "url": "https://github.com/api-platform/json-api.git", - "reference": "64bf11cbcaaf5cba3f6fceb243ed57d1ed6e2827" + "reference": "30e399ea2266403d04fd93df83c6983cf0a30e5d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/json-api/zipball/64bf11cbcaaf5cba3f6fceb243ed57d1ed6e2827", - "reference": "64bf11cbcaaf5cba3f6fceb243ed57d1ed6e2827", + "url": "https://api.github.com/repos/api-platform/json-api/zipball/30e399ea2266403d04fd93df83c6983cf0a30e5d", + "reference": "30e399ea2266403d04fd93df83c6983cf0a30e5d", "shasum": "" }, "require": { @@ -1412,7 +1412,7 @@ "require-dev": { "phpspec/prophecy": "^1.19", "phpspec/prophecy-phpunit": "^2.2", - "phpunit/phpunit": "^12.2", + "phpunit/phpunit": "^11.5 || ^12.2", "symfony/type-info": "^7.3 || ^8.0" }, "type": "library", @@ -1462,22 +1462,22 @@ "rest" ], "support": { - "source": "https://github.com/api-platform/json-api/tree/v4.3.3" + "source": "https://github.com/api-platform/json-api/tree/v4.3.4" }, - "time": "2026-03-13T11:03:46+00:00" + "time": "2026-04-30T12:21:24+00:00" }, { "name": "api-platform/json-schema", - "version": "v4.3.3", + "version": "v4.3.4", "source": { "type": "git", "url": "https://github.com/api-platform/json-schema.git", - "reference": "0f66e93719fd65938528aeff900737471acb515a" + "reference": "23dc2c388a08f2006b9189a0883a08f8837d7249" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/json-schema/zipball/0f66e93719fd65938528aeff900737471acb515a", - "reference": "0f66e93719fd65938528aeff900737471acb515a", + "url": "https://api.github.com/repos/api-platform/json-schema/zipball/23dc2c388a08f2006b9189a0883a08f8837d7249", + "reference": "23dc2c388a08f2006b9189a0883a08f8837d7249", "shasum": "" }, "require": { @@ -1491,7 +1491,7 @@ }, "require-dev": { "phpspec/prophecy-phpunit": "^2.2", - "phpunit/phpunit": "^12.2" + "phpunit/phpunit": "^11.5 || ^12.2" }, "type": "library", "extra": { @@ -1543,22 +1543,22 @@ "swagger" ], "support": { - "source": "https://github.com/api-platform/json-schema/tree/v4.3.3" + "source": "https://github.com/api-platform/json-schema/tree/v4.3.4" }, - "time": "2026-03-06T15:07:49+00:00" + "time": "2026-04-30T12:21:24+00:00" }, { "name": "api-platform/jsonld", - "version": "v4.3.3", + "version": "v4.3.4", "source": { "type": "git", "url": "https://github.com/api-platform/jsonld.git", - "reference": "64bd84ac44d6c6c51142a26bfa7d4c6237d20593" + "reference": "20ca6d7b5c11674c3046d710aaa0c9bc1795e54b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/jsonld/zipball/64bd84ac44d6c6c51142a26bfa7d4c6237d20593", - "reference": "64bd84ac44d6c6c51142a26bfa7d4c6237d20593", + "url": "https://api.github.com/repos/api-platform/jsonld/zipball/20ca6d7b5c11674c3046d710aaa0c9bc1795e54b", + "reference": "20ca6d7b5c11674c3046d710aaa0c9bc1795e54b", "shasum": "" }, "require": { @@ -1568,7 +1568,7 @@ "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^12.2", + "phpunit/phpunit": "^11.5 || ^12.2", "symfony/type-info": "^7.3 || ^8.0" }, "type": "library", @@ -1623,22 +1623,22 @@ "rest" ], "support": { - "source": "https://github.com/api-platform/jsonld/tree/v4.3.3" + "source": "https://github.com/api-platform/jsonld/tree/v4.3.4" }, - "time": "2026-03-13T08:23:46+00:00" + "time": "2026-04-30T12:21:24+00:00" }, { "name": "api-platform/metadata", - "version": "v4.3.3", + "version": "v4.3.4", "source": { "type": "git", "url": "https://github.com/api-platform/metadata.git", - "reference": "e5d54902ad9817f1734ef39fe5b34d3c9b505f27" + "reference": "e93caa26e7992ca138f2d12f79b5b25d2d091b7b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/metadata/zipball/e5d54902ad9817f1734ef39fe5b34d3c9b505f27", - "reference": "e5d54902ad9817f1734ef39fe5b34d3c9b505f27", + "url": "https://api.github.com/repos/api-platform/metadata/zipball/e93caa26e7992ca138f2d12f79b5b25d2d091b7b", + "reference": "e93caa26e7992ca138f2d12f79b5b25d2d091b7b", "shasum": "" }, "require": { @@ -1656,7 +1656,7 @@ "api-platform/state": "^4.3", "phpspec/prophecy-phpunit": "^2.2", "phpstan/phpdoc-parser": "^1.29 || ^2.0", - "phpunit/phpunit": "^12.2", + "phpunit/phpunit": "^11.5 || ^12.2", "symfony/config": "^6.4 || ^7.0 || ^8.0", "symfony/routing": "^6.4 || ^7.0 || ^8.0", "symfony/var-dumper": "^6.4 || ^7.0 || ^8.0", @@ -1721,22 +1721,22 @@ "swagger" ], "support": { - "source": "https://github.com/api-platform/metadata/tree/v4.3.3" + "source": "https://github.com/api-platform/metadata/tree/v4.3.4" }, - "time": "2026-03-27T06:51:10+00:00" + "time": "2026-04-30T12:21:24+00:00" }, { "name": "api-platform/openapi", - "version": "v4.3.3", + "version": "v4.3.4", "source": { "type": "git", "url": "https://github.com/api-platform/openapi.git", - "reference": "6ae725b4d2cee60972c18c4bd6148b63ccf16785" + "reference": "1562617e7500a50c2b6e6f43a0fb29a6a47e83a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/openapi/zipball/6ae725b4d2cee60972c18c4bd6148b63ccf16785", - "reference": "6ae725b4d2cee60972c18c4bd6148b63ccf16785", + "url": "https://api.github.com/repos/api-platform/openapi/zipball/1562617e7500a50c2b6e6f43a0fb29a6a47e83a2", + "reference": "1562617e7500a50c2b6e6f43a0fb29a6a47e83a2", "shasum": "" }, "require": { @@ -1756,7 +1756,7 @@ "api-platform/doctrine-orm": "^4.3", "api-platform/serializer": "^4.3", "phpspec/prophecy-phpunit": "^2.2", - "phpunit/phpunit": "^12.2", + "phpunit/phpunit": "^11.5 || ^12.2", "symfony/type-info": "^7.3 || ^8.0" }, "type": "library", @@ -1812,22 +1812,22 @@ "swagger" ], "support": { - "source": "https://github.com/api-platform/openapi/tree/v4.3.3" + "source": "https://github.com/api-platform/openapi/tree/v4.3.4" }, - "time": "2026-03-29T07:20:23+00:00" + "time": "2026-04-30T12:21:24+00:00" }, { "name": "api-platform/serializer", - "version": "v4.3.3", + "version": "v4.3.4", "source": { "type": "git", "url": "https://github.com/api-platform/serializer.git", - "reference": "c8a62096d277b89b0f146aa90c65eeaeb1412900" + "reference": "bd7c26cc8e6858abc9661d677c15eaf4c61e08e3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/serializer/zipball/c8a62096d277b89b0f146aa90c65eeaeb1412900", - "reference": "c8a62096d277b89b0f146aa90c65eeaeb1412900", + "url": "https://api.github.com/repos/api-platform/serializer/zipball/bd7c26cc8e6858abc9661d677c15eaf4c61e08e3", + "reference": "bd7c26cc8e6858abc9661d677c15eaf4c61e08e3", "shasum": "" }, "require": { @@ -1847,7 +1847,7 @@ "api-platform/openapi": "^4.3", "doctrine/collections": "^2.1", "phpspec/prophecy-phpunit": "^2.2", - "phpunit/phpunit": "^12.2", + "phpunit/phpunit": "^11.5 || ^12.2", "sebastian/exporter": "^6.3.2 || ^7.0.2", "symfony/mercure-bundle": "*", "symfony/type-info": "^7.3 || ^8.0", @@ -1906,22 +1906,22 @@ "serializer" ], "support": { - "source": "https://github.com/api-platform/serializer/tree/v4.3.3" + "source": "https://github.com/api-platform/serializer/tree/v4.3.4" }, - "time": "2026-03-27T06:51:10+00:00" + "time": "2026-04-30T12:21:24+00:00" }, { "name": "api-platform/state", - "version": "v4.3.3", + "version": "v4.3.4", "source": { "type": "git", "url": "https://github.com/api-platform/state.git", - "reference": "5387afbb9e79027b34dab731bc90e810fdd6ca60" + "reference": "dda8789e95b1627a6427edb48f9024b306fdf5ff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/state/zipball/5387afbb9e79027b34dab731bc90e810fdd6ca60", - "reference": "5387afbb9e79027b34dab731bc90e810fdd6ca60", + "url": "https://api.github.com/repos/api-platform/state/zipball/dda8789e95b1627a6427edb48f9024b306fdf5ff", + "reference": "dda8789e95b1627a6427edb48f9024b306fdf5ff", "shasum": "" }, "require": { @@ -1936,7 +1936,7 @@ "require-dev": { "api-platform/serializer": "^4.3", "api-platform/validator": "^4.3.1", - "phpunit/phpunit": "^12.2", + "phpunit/phpunit": "^11.5 || ^12.2", "symfony/http-foundation": "^6.4.14 || ^7.0 || ^8.0", "symfony/object-mapper": "^7.4 || ^8.0", "symfony/type-info": "^7.4 || ^8.0", @@ -2003,22 +2003,22 @@ "swagger" ], "support": { - "source": "https://github.com/api-platform/state/tree/v4.3.3" + "source": "https://github.com/api-platform/state/tree/v4.3.4" }, - "time": "2026-03-29T06:54:13+00:00" + "time": "2026-04-30T12:21:24+00:00" }, { "name": "api-platform/symfony", - "version": "v4.3.3", + "version": "v4.3.4", "source": { "type": "git", "url": "https://github.com/api-platform/symfony.git", - "reference": "69070b33f11ed7c163ba75ecfea192e460b89ce0" + "reference": "532063884e3f91a8a831322a572220cc55501a2f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/symfony/zipball/69070b33f11ed7c163ba75ecfea192e460b89ce0", - "reference": "69070b33f11ed7c163ba75ecfea192e460b89ce0", + "url": "https://api.github.com/repos/api-platform/symfony/zipball/532063884e3f91a8a831322a572220cc55501a2f", + "reference": "532063884e3f91a8a831322a572220cc55501a2f", "shasum": "" }, "require": { @@ -2049,7 +2049,7 @@ "api-platform/graphql": "^4.3", "api-platform/hal": "^4.3", "phpspec/prophecy-phpunit": "^2.2", - "phpunit/phpunit": "^12.2", + "phpunit/phpunit": "^11.5 || ^12.2", "symfony/expression-language": "^6.4 || ^7.0 || ^8.0", "symfony/intl": "^6.4 || ^7.0 || ^8.0", "symfony/mercure-bundle": "*", @@ -2131,22 +2131,22 @@ "symfony" ], "support": { - "source": "https://github.com/api-platform/symfony/tree/v4.3.3" + "source": "https://github.com/api-platform/symfony/tree/v4.3.4" }, - "time": "2026-03-27T06:51:10+00:00" + "time": "2026-04-30T12:21:24+00:00" }, { "name": "api-platform/validator", - "version": "v4.3.3", + "version": "v4.3.4", "source": { "type": "git", "url": "https://github.com/api-platform/validator.git", - "reference": "3c0d486a12d7ab9b8fcfb727437b5322dc619663" + "reference": "22693bc3d3538af700cf274b99c834c37b1d1a68" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/validator/zipball/3c0d486a12d7ab9b8fcfb727437b5322dc619663", - "reference": "3c0d486a12d7ab9b8fcfb727437b5322dc619663", + "url": "https://api.github.com/repos/api-platform/validator/zipball/22693bc3d3538af700cf274b99c834c37b1d1a68", + "reference": "22693bc3d3538af700cf274b99c834c37b1d1a68", "shasum": "" }, "require": { @@ -2160,7 +2160,7 @@ }, "require-dev": { "phpspec/prophecy-phpunit": "^2.2", - "phpunit/phpunit": "^12.2" + "phpunit/phpunit": "^11.5 || ^12.2" }, "type": "library", "extra": { @@ -2207,9 +2207,9 @@ "validator" ], "support": { - "source": "https://github.com/api-platform/validator/tree/v4.3.3" + "source": "https://github.com/api-platform/validator/tree/v4.3.4" }, - "time": "2026-03-17T15:23:21+00:00" + "time": "2026-04-30T12:21:24+00:00" }, { "name": "beberlei/assert", @@ -3883,16 +3883,16 @@ }, { "name": "doctrine/migrations", - "version": "3.9.6", + "version": "3.9.7", "source": { "type": "git", "url": "https://github.com/doctrine/migrations.git", - "reference": "ffd8355cdd8505fc650d9604f058bf62aedd80a1" + "reference": "96cb2a89b56c9efb0bac38e606dc0b0f13e650ec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/migrations/zipball/ffd8355cdd8505fc650d9604f058bf62aedd80a1", - "reference": "ffd8355cdd8505fc650d9604f058bf62aedd80a1", + "url": "https://api.github.com/repos/doctrine/migrations/zipball/96cb2a89b56c9efb0bac38e606dc0b0f13e650ec", + "reference": "96cb2a89b56c9efb0bac38e606dc0b0f13e650ec", "shasum": "" }, "require": { @@ -3966,7 +3966,7 @@ ], "support": { "issues": "https://github.com/doctrine/migrations/issues", - "source": "https://github.com/doctrine/migrations/tree/3.9.6" + "source": "https://github.com/doctrine/migrations/tree/3.9.7" }, "funding": [ { @@ -3982,7 +3982,7 @@ "type": "tidelift" } ], - "time": "2026-02-11T06:46:11+00:00" + "time": "2026-04-23T19:33:20+00:00" }, { "name": "doctrine/orm", @@ -4074,19 +4074,20 @@ }, { "name": "doctrine/persistence", - "version": "4.1.1", + "version": "4.2.0", "source": { "type": "git", "url": "https://github.com/doctrine/persistence.git", - "reference": "b9c49ad3558bb77ef973f4e173f2e9c2eca9be09" + "reference": "49ab73e0d3e2ac8d1f5ecda3dd8acd5503781e8b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/persistence/zipball/b9c49ad3558bb77ef973f4e173f2e9c2eca9be09", - "reference": "b9c49ad3558bb77ef973f4e173f2e9c2eca9be09", + "url": "https://api.github.com/repos/doctrine/persistence/zipball/49ab73e0d3e2ac8d1f5ecda3dd8acd5503781e8b", + "reference": "49ab73e0d3e2ac8d1f5ecda3dd8acd5503781e8b", "shasum": "" }, "require": { + "doctrine/deprecations": "^1", "doctrine/event-manager": "^1 || ^2", "php": "^8.1", "psr/cache": "^1.0 || ^2.0 || ^3.0" @@ -4097,13 +4098,13 @@ "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" + "symfony/cache": "^4.4 || ^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/finder": "^4.4 || ^5.4 || ^6.0 || ^7.0 || ^8.0" }, "type": "library", "autoload": { "psr-4": { - "Doctrine\\Persistence\\": "src/Persistence" + "Doctrine\\Persistence\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -4147,7 +4148,7 @@ ], "support": { "issues": "https://github.com/doctrine/persistence/issues", - "source": "https://github.com/doctrine/persistence/tree/4.1.1" + "source": "https://github.com/doctrine/persistence/tree/4.2.0" }, "funding": [ { @@ -4163,7 +4164,7 @@ "type": "tidelift" } ], - "time": "2025-10-16T20:13:18+00:00" + "time": "2026-04-26T12:12:52+00:00" }, { "name": "doctrine/sql-formatter", @@ -5201,16 +5202,16 @@ }, { "name": "jbtronics/settings-bundle", - "version": "v3.3.0", + "version": "v3.3.1", "source": { "type": "git", "url": "https://github.com/jbtronics/settings-bundle.git", - "reference": "9cce001eef4570ca3b2766da6a8a4abd327ea438" + "reference": "ca90a8b2255482d11f10cb30c49791a7dabd5d40" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jbtronics/settings-bundle/zipball/9cce001eef4570ca3b2766da6a8a4abd327ea438", - "reference": "9cce001eef4570ca3b2766da6a8a4abd327ea438", + "url": "https://api.github.com/repos/jbtronics/settings-bundle/zipball/ca90a8b2255482d11f10cb30c49791a7dabd5d40", + "reference": "ca90a8b2255482d11f10cb30c49791a7dabd5d40", "shasum": "" }, "require": { @@ -5283,7 +5284,7 @@ ], "support": { "issues": "https://github.com/jbtronics/settings-bundle/issues", - "source": "https://github.com/jbtronics/settings-bundle/tree/v3.3.0" + "source": "https://github.com/jbtronics/settings-bundle/tree/v3.3.1" }, "funding": [ { @@ -5295,7 +5296,7 @@ "type": "github" } ], - "time": "2026-04-21T21:26:26+00:00" + "time": "2026-04-28T10:57:15+00:00" }, { "name": "jfcherng/php-color-output", @@ -5534,6 +5535,174 @@ ], "time": "2023-05-21T07:57:08+00:00" }, + { + "name": "jkphl/dom-factory", + "version": "v1.0.1", + "source": { + "type": "git", + "url": "https://github.com/jkphl/dom-factory.git", + "reference": "dd32b8b2cc800f065c0eff8bb621d9f80147d45e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jkphl/dom-factory/zipball/dd32b8b2cc800f065c0eff8bb621d9f80147d45e", + "reference": "dd32b8b2cc800f065c0eff8bb621d9f80147d45e", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "guzzlehttp/guzzle": "^6.0||^7.0", + "masterminds/html5": "^2.7", + "php": ">=7.2" + }, + "require-dev": { + "clue/graph-composer": "^1.1", + "php-coveralls/php-coveralls": "^2.2", + "phpunit/phpunit": "^8.0||^9.0", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Jkphl\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Joschi Kuphal", + "email": "joschi@kuphal.net", + "homepage": "https://jkphl.is", + "role": "Developer" + } + ], + "description": "Simple HTML5/XML DOM factory", + "homepage": "https://github.com/jkphl/dom-factory", + "support": { + "email": "joschi@kuphal.net", + "issues": "https://github.com/jkphl/dom-factory/issues", + "source": "https://github.com/jkphl/dom-factory" + }, + "time": "2021-06-28T11:49:36+00:00" + }, + { + "name": "jkphl/micrometa", + "version": "v3.4.0", + "source": { + "type": "git", + "url": "https://github.com/jkphl/micrometa.git", + "reference": "003583fa91eab9c62e5a47e9d4f909b2fad44de2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jkphl/micrometa/zipball/003583fa91eab9c62e5a47e9d4f909b2fad44de2", + "reference": "003583fa91eab9c62e5a47e9d4f909b2fad44de2", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "jkphl/dom-factory": "^1", + "jkphl/rdfa-lite-microdata": "^0.4.4", + "league/uri": "^5.0|^6.5|^7.0", + "mf2/mf2": "^0.4", + "ml/json-ld": "^1.2", + "monolog/monolog": "^1.24 || ^2 || ^3", + "php": ">=7.1.3", + "psr/cache": "^1.0|^2|^3", + "psr/log": "^1.1|^2|^3", + "symfony/cache": "^4.0|^5.0|^6.0|^7.0|^8.0" + }, + "require-dev": { + "clue/graph-composer": "^1.1", + "mf2/tests": "@dev", + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^7.0 || ^8.5", + "squizlabs/php_codesniffer": "^3.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Jkphl\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Joschi Kuphal", + "email": "joschi@tollwerk.de", + "homepage": "https://jkphl.is", + "role": "Developer" + } + ], + "description": "A meta parser for extracting micro information out of web documents, currently supporting Microformats 1+2, HTML Microdata, RDFa Lite 1.1 and JSON-LD", + "homepage": "https://jkphl.is/projects/micrometa/", + "support": { + "email": "joschi@tollwerk.de", + "issues": "https://github.com/jkphl/micrometa/issues", + "source": "https://github.com/jkphl/micrometa" + }, + "time": "2026-04-28T07:20:59+00:00" + }, + { + "name": "jkphl/rdfa-lite-microdata", + "version": "v0.4.7", + "source": { + "type": "git", + "url": "https://github.com/jkphl/rdfa-lite-microdata.git", + "reference": "ffc4940e8be55798257a03da7ed7d4506a13c3e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jkphl/rdfa-lite-microdata/zipball/ffc4940e8be55798257a03da7ed7d4506a13c3e5", + "reference": "ffc4940e8be55798257a03da7ed7d4506a13c3e5", + "shasum": "" + }, + "require": { + "jkphl/dom-factory": "^1", + "php": ">=5.5" + }, + "require-dev": { + "clue/graph-composer": "dev-master", + "codeclimate/php-test-reporter": "^0.4.4", + "phpunit/phpunit": "^4.8", + "satooshi/php-coveralls": "^1.0", + "squizlabs/php_codesniffer": "^2.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Jkphl\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Joschi Kuphal", + "email": "joschi@tollwerk.de", + "homepage": "https://jkphl.is", + "role": "Developer" + } + ], + "description": "RDFa Lite 1.1 and HTML Microdata parser for web documents (HTML, SVG, XML)", + "homepage": "https://github.com/jkphl/rdfa-lite-microdata", + "support": { + "email": "joschi@tollwerk.de", + "issues": "https://github.com/jkphl/rdfa-lite-microdata/issues", + "source": "https://github.com/jkphl/rdfa-lite-microdata" + }, + "time": "2023-01-27T13:29:45+00:00" + }, { "name": "kelunik/certificate", "version": "v1.1.3", @@ -6899,6 +7068,170 @@ }, "time": "2025-07-25T09:04:22+00:00" }, + { + "name": "mf2/mf2", + "version": "0.4.6", + "source": { + "type": "git", + "url": "https://github.com/microformats/php-mf2.git", + "reference": "00b70ee7eb7f5b0585b1bd467f6c9cbd75055d23" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/microformats/php-mf2/zipball/00b70ee7eb7f5b0585b1bd467f6c9cbd75055d23", + "reference": "00b70ee7eb7f5b0585b1bd467f6c9cbd75055d23", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "mf2/tests": "@dev", + "phpdocumentor/phpdocumentor": "v2.8.4", + "phpunit/phpunit": "4.8.*" + }, + "suggest": { + "barnabywalters/mf-cleaner": "To more easily handle the canonical data php-mf2 gives you", + "masterminds/html5": "Alternative HTML parser for PHP, for better HTML5 support." + }, + "bin": [ + "bin/fetch-mf2", + "bin/parse-mf2" + ], + "type": "library", + "autoload": { + "files": [ + "Mf2/Parser.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "CC0-1.0" + ], + "authors": [ + { + "name": "Barnaby Walters", + "homepage": "http://waterpigs.co.uk" + } + ], + "description": "A pure, generic microformats2 parser — makes HTML as easy to consume as a JSON API", + "keywords": [ + "html", + "microformats", + "microformats 2", + "parser", + "semantic" + ], + "support": { + "issues": "https://github.com/microformats/php-mf2/issues", + "source": "https://github.com/microformats/php-mf2/tree/master" + }, + "time": "2018-08-24T14:47:04+00:00" + }, + { + "name": "ml/iri", + "version": "1.1.4", + "target-dir": "ML/IRI", + "source": { + "type": "git", + "url": "https://github.com/lanthaler/IRI.git", + "reference": "cbd44fa913e00ea624241b38cefaa99da8d71341" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lanthaler/IRI/zipball/cbd44fa913e00ea624241b38cefaa99da8d71341", + "reference": "cbd44fa913e00ea624241b38cefaa99da8d71341", + "shasum": "" + }, + "require": { + "lib-pcre": ">=4.0", + "php": ">=5.3.0" + }, + "type": "library", + "autoload": { + "psr-0": { + "ML\\IRI": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Markus Lanthaler", + "email": "mail@markus-lanthaler.com", + "homepage": "http://www.markus-lanthaler.com", + "role": "Developer" + } + ], + "description": "IRI handling for PHP", + "homepage": "http://www.markus-lanthaler.com", + "keywords": [ + "URN", + "iri", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/lanthaler/IRI/issues", + "source": "https://github.com/lanthaler/IRI/tree/master" + }, + "time": "2014-01-21T13:43:39+00:00" + }, + { + "name": "ml/json-ld", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/lanthaler/JsonLD.git", + "reference": "537e68e87a6bce23e57c575cd5dcac1f67ce25d8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lanthaler/JsonLD/zipball/537e68e87a6bce23e57c575cd5dcac1f67ce25d8", + "reference": "537e68e87a6bce23e57c575cd5dcac1f67ce25d8", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ml/iri": "^1.1.1", + "php": ">=5.3.0" + }, + "require-dev": { + "json-ld/tests": "1.0", + "phpunit/phpunit": "^4" + }, + "type": "library", + "autoload": { + "psr-4": { + "ML\\JsonLD\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Markus Lanthaler", + "email": "mail@markus-lanthaler.com", + "homepage": "http://www.markus-lanthaler.com", + "role": "Developer" + } + ], + "description": "JSON-LD Processor for PHP", + "homepage": "http://www.markus-lanthaler.com", + "keywords": [ + "JSON-LD", + "jsonld" + ], + "support": { + "issues": "https://github.com/lanthaler/JsonLD/issues", + "source": "https://github.com/lanthaler/JsonLD/tree/1.2.1" + }, + "time": "2022-09-29T08:45:17+00:00" + }, { "name": "monolog/monolog", "version": "3.10.0", @@ -7821,6 +8154,58 @@ ], "time": "2025-12-09T10:50:49+00:00" }, + { + "name": "oskarstark/enum-helper", + "version": "1.8.4", + "source": { + "type": "git", + "url": "https://github.com/OskarStark/enum-helper.git", + "reference": "14e185f1cc259d7cd3f61eea17f9b174a886a6da" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/OskarStark/enum-helper/zipball/14e185f1cc259d7cd3f61eea17f9b174a886a6da", + "reference": "14e185f1cc259d7cd3f61eea17f9b174a886a6da", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "conflict": { + "phpunit/phpunit": "<10" + }, + "require-dev": { + "ergebnis/php-cs-fixer-config": "^6.58", + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^10.5", + "rector/rector": "^2.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "OskarStark\\Enum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Oskar Stark", + "email": "oskarstark@googlemail.com" + } + ], + "description": "This library provides helpers for several enum operations", + "keywords": [ + "enum" + ], + "support": { + "issues": "https://github.com/OskarStark/enum-helper/issues", + "source": "https://github.com/OskarStark/enum-helper/tree/1.8.4" + }, + "time": "2026-02-05T08:59:09+00:00" + }, { "name": "paragonie/constant_time_encoding", "version": "v3.1.3", @@ -8446,16 +8831,16 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "5.6.7", + "version": "6.0.3", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "31a105931bc8ffa3a123383829772e832fd8d903" + "reference": "7bae67520aa9f5ecc506d646810bd40d9da54582" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/31a105931bc8ffa3a123383829772e832fd8d903", - "reference": "31a105931bc8ffa3a123383829772e832fd8d903", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/7bae67520aa9f5ecc506d646810bd40d9da54582", + "reference": "7bae67520aa9f5ecc506d646810bd40d9da54582", "shasum": "" }, "require": { @@ -8463,8 +8848,8 @@ "ext-filter": "*", "php": "^7.4 || ^8.0", "phpdocumentor/reflection-common": "^2.2", - "phpdocumentor/type-resolver": "^1.7", - "phpstan/phpdoc-parser": "^1.7|^2.0", + "phpdocumentor/type-resolver": "^2.0", + "phpstan/phpdoc-parser": "^2.0", "webmozart/assert": "^1.9.1 || ^2" }, "require-dev": { @@ -8474,7 +8859,8 @@ "phpstan/phpstan-mockery": "^1.1", "phpstan/phpstan-webmozart-assert": "^1.2", "phpunit/phpunit": "^9.5", - "psalm/phar": "^5.26" + "psalm/phar": "^5.26", + "shipmonk/dead-code-detector": "^0.5.1" }, "type": "library", "extra": { @@ -8504,44 +8890,44 @@ "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", "support": { "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.7" + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/6.0.3" }, - "time": "2026-03-18T20:47:46+00:00" + "time": "2026-03-18T20:49:53+00:00" }, { "name": "phpdocumentor/type-resolver", - "version": "1.12.0", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "92a98ada2b93d9b201a613cb5a33584dde25f195" + "reference": "327a05bbee54120d4786a0dc67aad30226ad4cf9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/92a98ada2b93d9b201a613cb5a33584dde25f195", - "reference": "92a98ada2b93d9b201a613cb5a33584dde25f195", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/327a05bbee54120d4786a0dc67aad30226ad4cf9", + "reference": "327a05bbee54120d4786a0dc67aad30226ad4cf9", "shasum": "" }, "require": { "doctrine/deprecations": "^1.0", - "php": "^7.3 || ^8.0", + "php": "^7.4 || ^8.0", "phpdocumentor/reflection-common": "^2.0", - "phpstan/phpdoc-parser": "^1.18|^2.0" + "phpstan/phpdoc-parser": "^2.0" }, "require-dev": { "ext-tokenizer": "*", "phpbench/phpbench": "^1.2", - "phpstan/extension-installer": "^1.1", - "phpstan/phpstan": "^1.8", - "phpstan/phpstan-phpunit": "^1.1", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", "phpunit/phpunit": "^9.5", - "rector/rector": "^0.13.9", - "vimeo/psalm": "^4.25" + "psalm/phar": "^4" }, "type": "library", "extra": { "branch-alias": { - "dev-1.x": "1.x-dev" + "dev-1.x": "1.x-dev", + "dev-2.x": "2.x-dev" } }, "autoload": { @@ -8562,9 +8948,9 @@ "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", "support": { "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.12.0" + "source": "https://github.com/phpDocumentor/TypeResolver/tree/2.0.0" }, - "time": "2025-11-21T15:09:14+00:00" + "time": "2026-01-06T21:53:42+00:00" }, { "name": "phpoffice/phpspreadsheet", @@ -9357,16 +9743,16 @@ }, { "name": "rhukster/dom-sanitizer", - "version": "1.0.10", + "version": "1.0.11", "source": { "type": "git", "url": "https://github.com/rhukster/dom-sanitizer.git", - "reference": "49a98046b708a4c92f754f5b0ef1720bb85142e2" + "reference": "02d08ec8b36b93b04517d74fe82b715ef06273bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rhukster/dom-sanitizer/zipball/49a98046b708a4c92f754f5b0ef1720bb85142e2", - "reference": "49a98046b708a4c92f754f5b0ef1720bb85142e2", + "url": "https://api.github.com/repos/rhukster/dom-sanitizer/zipball/02d08ec8b36b93b04517d74fe82b715ef06273bd", + "reference": "02d08ec8b36b93b04517d74fe82b715ef06273bd", "shasum": "" }, "require": { @@ -9396,9 +9782,9 @@ "description": "A simple but effective DOM/SVG/MathML Sanitizer for PHP 7.4+", "support": { "issues": "https://github.com/rhukster/dom-sanitizer/issues", - "source": "https://github.com/rhukster/dom-sanitizer/tree/1.0.10" + "source": "https://github.com/rhukster/dom-sanitizer/tree/1.0.11" }, - "time": "2026-04-10T17:00:11+00:00" + "time": "2026-04-23T22:56:32+00:00" }, { "name": "robrichards/xmlseclibs", @@ -9687,29 +10073,29 @@ }, { "name": "sabre/uri", - "version": "3.0.3", + "version": "3.1.0", "source": { "type": "git", "url": "https://github.com/sabre-io/uri.git", - "reference": "4fa0b2049e06a4fbe4aea4f0aa69e7b8410a13bc" + "reference": "a926c749dddfb289b8a9b5218d16ac06affdc631" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sabre-io/uri/zipball/4fa0b2049e06a4fbe4aea4f0aa69e7b8410a13bc", - "reference": "4fa0b2049e06a4fbe4aea4f0aa69e7b8410a13bc", + "url": "https://api.github.com/repos/sabre-io/uri/zipball/a926c749dddfb289b8a9b5218d16ac06affdc631", + "reference": "a926c749dddfb289b8a9b5218d16ac06affdc631", "shasum": "" }, "require": { - "php": "^7.4 || ^8.0" + "php": "^8.2" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.94", + "friendsofphp/php-cs-fixer": "^3.95", "phpstan/extension-installer": "^1.4", "phpstan/phpstan": "^2.1", "phpstan/phpstan-phpunit": "^2.0", "phpstan/phpstan-strict-rules": "^2.0", - "phpunit/phpunit": "^9.6", - "rector/rector": "^2.3" + "phpunit/phpunit": "^10.5", + "rector/rector": "^2.4" }, "type": "library", "autoload": { @@ -9744,7 +10130,7 @@ "issues": "https://github.com/sabre-io/uri/issues", "source": "https://github.com/fruux/sabre-uri" }, - "time": "2026-04-01T08:19:11+00:00" + "time": "2026-04-26T04:19:03+00:00" }, { "name": "scheb/2fa-backup-code", @@ -10341,6 +10727,552 @@ ], "time": "2026-03-23T22:56:56+00:00" }, + { + "name": "symfony/ai-bundle", + "version": "v0.8.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/ai-bundle.git", + "reference": "847365e0f885f8814421e9c94f03ce19e0b54bbc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/ai-bundle/zipball/847365e0f885f8814421e9c94f03ce19e0b54bbc", + "reference": "847365e0f885f8814421e9c94f03ce19e0b54bbc", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/ai-platform": "^0.8", + "symfony/clock": "^7.3|^8.0", + "symfony/config": "^7.3|^8.0", + "symfony/console": "^7.3|^8.0", + "symfony/dependency-injection": "^7.3|^8.0", + "symfony/framework-bundle": "^7.3|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.3|^8.0" + }, + "require-dev": { + "google/auth": "^1.47", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^11.5.53", + "symfony/ai-agent": "^0.8", + "symfony/ai-ai-ml-api-platform": "^0.8", + "symfony/ai-albert-platform": "^0.8", + "symfony/ai-amazee-ai-platform": "^0.8", + "symfony/ai-anthropic-platform": "^0.8", + "symfony/ai-azure-platform": "^0.8", + "symfony/ai-azure-search-store": "^0.8", + "symfony/ai-bedrock-platform": "^0.8", + "symfony/ai-cache-message-store": "^0.8", + "symfony/ai-cache-platform": "^0.8", + "symfony/ai-cache-store": "^0.8", + "symfony/ai-cartesia-platform": "^0.8", + "symfony/ai-cerebras-platform": "^0.8", + "symfony/ai-chat": "^0.8", + "symfony/ai-chroma-db-store": "^0.8", + "symfony/ai-click-house-store": "^0.8", + "symfony/ai-cloudflare-message-store": "^0.8", + "symfony/ai-cloudflare-store": "^0.8", + "symfony/ai-decart-platform": "^0.8", + "symfony/ai-deep-seek-platform": "^0.8", + "symfony/ai-docker-model-runner-platform": "^0.8", + "symfony/ai-doctrine-message-store": "^0.8", + "symfony/ai-elasticsearch-store": "^0.8", + "symfony/ai-eleven-labs-platform": "^0.8", + "symfony/ai-failover-platform": "^0.8", + "symfony/ai-gemini-platform": "^0.8", + "symfony/ai-generic-platform": "^0.8", + "symfony/ai-hugging-face-platform": "^0.8", + "symfony/ai-lm-studio-platform": "^0.8", + "symfony/ai-manticore-search-store": "^0.8", + "symfony/ai-maria-db-store": "^0.8", + "symfony/ai-meilisearch-message-store": "^0.8", + "symfony/ai-meilisearch-store": "^0.8", + "symfony/ai-meta-platform": "^0.8", + "symfony/ai-milvus-store": "^0.8", + "symfony/ai-mistral-platform": "^0.8", + "symfony/ai-mongo-db-message-store": "^0.8", + "symfony/ai-mongo-db-store": "^0.8", + "symfony/ai-neo4j-store": "^0.8", + "symfony/ai-ollama-platform": "^0.8", + "symfony/ai-open-ai-platform": "^0.8", + "symfony/ai-open-responses-platform": "^0.8", + "symfony/ai-open-router-platform": "^0.8", + "symfony/ai-open-search-store": "^0.8", + "symfony/ai-perplexity-platform": "^0.8", + "symfony/ai-pinecone-store": "^0.8", + "symfony/ai-pogocache-message-store": "^0.8", + "symfony/ai-postgres-store": "^0.8", + "symfony/ai-qdrant-store": "^0.8", + "symfony/ai-redis-message-store": "^0.8", + "symfony/ai-redis-store": "^0.8", + "symfony/ai-replicate-platform": "^0.8", + "symfony/ai-s3vectors-store": "^0.8", + "symfony/ai-scaleway-platform": "^0.8", + "symfony/ai-session-message-store": "^0.8", + "symfony/ai-sqlite-store": "^0.8", + "symfony/ai-store": "^0.8", + "symfony/ai-supabase-store": "^0.8", + "symfony/ai-surreal-db-message-store": "^0.8", + "symfony/ai-surreal-db-store": "^0.8", + "symfony/ai-transformers-php-platform": "^0.8", + "symfony/ai-typesense-store": "^0.8", + "symfony/ai-vektor-store": "^0.8", + "symfony/ai-vertex-ai-platform": "^0.8", + "symfony/ai-voyage-platform": "^0.8", + "symfony/ai-weaviate-store": "^0.8", + "symfony/expression-language": "^7.3|^8.0", + "symfony/security-core": "^7.3|^8.0", + "symfony/translation": "^7.3|^8.0", + "symfony/validator": "^7.3|^8.0" + }, + "type": "symfony-bundle", + "extra": { + "thanks": { + "url": "https://github.com/symfony/ai", + "name": "symfony/ai" + } + }, + "autoload": { + "psr-4": { + "Symfony\\AI\\AiBundle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christopher Hertel", + "email": "mail@christopher-hertel.de" + }, + { + "name": "Oskar Stark", + "email": "oskarstark@googlemail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Integration bundle for Symfony AI components", + "support": { + "source": "https://github.com/symfony/ai-bundle/tree/v0.8.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-04-20T21:23:24+00:00" + }, + { + "name": "symfony/ai-generic-platform", + "version": "v0.8.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/ai-generic-platform.git", + "reference": "2e358c0e88c676fad0b61b3df715f9822d29a7e3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/ai-generic-platform/zipball/2e358c0e88c676fad0b61b3df715f9822d29a7e3", + "reference": "2e358c0e88c676fad0b61b3df715f9822d29a7e3", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/ai-platform": "^0.8", + "symfony/http-client": "^7.3|^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^11.5.53" + }, + "type": "symfony-ai-platform", + "extra": { + "thanks": { + "url": "https://github.com/symfony/ai", + "name": "symfony/ai" + } + }, + "autoload": { + "psr-4": { + "Symfony\\AI\\Platform\\Bridge\\Generic\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christopher Hertel", + "email": "mail@christopher-hertel.de" + }, + { + "name": "Oskar Stark", + "email": "oskarstark@googlemail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic platform bridge for Symfony AI", + "keywords": [ + "Bridge", + "ai", + "generic", + "platform" + ], + "support": { + "source": "https://github.com/symfony/ai-generic-platform/tree/v0.8.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-04-20T21:23:24+00:00" + }, + { + "name": "symfony/ai-lm-studio-platform", + "version": "v0.8.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/ai-lm-studio-platform.git", + "reference": "ad1c046dd9e7d6e474bc86554443e2d9400a7826" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/ai-lm-studio-platform/zipball/ad1c046dd9e7d6e474bc86554443e2d9400a7826", + "reference": "ad1c046dd9e7d6e474bc86554443e2d9400a7826", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/ai-generic-platform": "^0.8", + "symfony/ai-platform": "^0.8", + "symfony/http-client": "^7.3|^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^11.5.53" + }, + "type": "symfony-ai-platform", + "extra": { + "thanks": { + "url": "https://github.com/symfony/ai", + "name": "symfony/ai" + } + }, + "autoload": { + "psr-4": { + "Symfony\\AI\\Platform\\Bridge\\LmStudio\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christopher Hertel", + "email": "mail@christopher-hertel.de" + }, + { + "name": "Oskar Stark", + "email": "oskarstark@googlemail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "LM Studio platform bridge for Symfony AI", + "keywords": [ + "Bridge", + "ai", + "lmstudio", + "local", + "platform" + ], + "support": { + "source": "https://github.com/symfony/ai-lm-studio-platform/tree/v0.8.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-04-20T21:23:24+00:00" + }, + { + "name": "symfony/ai-open-router-platform", + "version": "v0.8.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/ai-open-router-platform.git", + "reference": "eb5ed3176b78bc489bf325c5d6bc4efc255804be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/ai-open-router-platform/zipball/eb5ed3176b78bc489bf325c5d6bc4efc255804be", + "reference": "eb5ed3176b78bc489bf325c5d6bc4efc255804be", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/ai-generic-platform": "^0.8", + "symfony/ai-platform": "^0.8", + "symfony/http-client": "^7.3|^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^11.5.53" + }, + "type": "symfony-ai-platform", + "extra": { + "thanks": { + "url": "https://github.com/symfony/ai", + "name": "symfony/ai" + } + }, + "autoload": { + "psr-4": { + "Symfony\\AI\\Platform\\Bridge\\OpenRouter\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christopher Hertel", + "email": "mail@christopher-hertel.de" + }, + { + "name": "Oskar Stark", + "email": "oskarstark@googlemail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "OpenRouter platform bridge for Symfony AI", + "keywords": [ + "Bridge", + "OpenRouter", + "ai", + "platform" + ], + "support": { + "source": "https://github.com/symfony/ai-open-router-platform/tree/v0.8.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-04-20T21:23:24+00:00" + }, + { + "name": "symfony/ai-platform", + "version": "v0.8.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/ai-platform.git", + "reference": "86ed9396f53cad02b5d1ca8092956ea74f65823f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/ai-platform/zipball/86ed9396f53cad02b5d1ca8092956ea74f65823f", + "reference": "86ed9396f53cad02b5d1ca8092956ea74f65823f", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "oskarstark/enum-helper": "^1.5", + "php": ">=8.2", + "phpdocumentor/reflection-docblock": "^5.4|^6.0", + "phpstan/phpdoc-parser": "^2.1", + "psr/log": "^3.0", + "symfony/clock": "^7.3|^8.0", + "symfony/event-dispatcher": "^7.3|^8.0", + "symfony/property-access": "^7.3|^8.0", + "symfony/property-info": "^7.3|^8.0", + "symfony/serializer": "^7.3|^8.0", + "symfony/type-info": "^7.3|^8.0", + "symfony/uid": "^7.3|^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^11.5.53", + "symfony/cache": "^7.3|^8.0", + "symfony/console": "^7.3|^8.0", + "symfony/dotenv": "^7.3|^8.0", + "symfony/expression-language": "^7.3|^8.0", + "symfony/finder": "^7.3|^8.0", + "symfony/http-client": "^7.3|^8.0", + "symfony/http-client-contracts": "^3.5", + "symfony/intl": "^7.3|^8.0", + "symfony/process": "^7.3|^8.0", + "symfony/validator": "^7.3|^8.0", + "symfony/var-dumper": "^7.3|^8.0", + "symfony/yaml": "^7.3|^8.0" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/ai", + "name": "symfony/ai" + } + }, + "autoload": { + "psr-4": { + "Symfony\\AI\\Platform\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christopher Hertel", + "email": "mail@christopher-hertel.de" + }, + { + "name": "Oskar Stark", + "email": "oskarstark@googlemail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "PHP library for interacting with AI platform provider.", + "keywords": [ + "Gemini", + "OpenRouter", + "ai", + "aimlapi", + "albert", + "amazeeai", + "anthropic", + "azure", + "bedrock", + "cerebras", + "dockermodelrunner", + "elevenlabs", + "huggingface", + "inference", + "litellm", + "llama", + "lmstudio", + "meta", + "mistral", + "nova", + "ollama", + "openai", + "ovh", + "perplexity", + "replicate", + "speech", + "transformers", + "vertexai", + "voyage" + ], + "support": { + "source": "https://github.com/symfony/ai-platform/tree/v0.8.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-20T21:28:38+00:00" + }, { "name": "symfony/apache-pack", "version": "v1.0.1", @@ -10442,16 +11374,16 @@ }, { "name": "symfony/cache", - "version": "v7.4.8", + "version": "v7.4.9", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "467464da294734b0fb17e853e5712abc8470f819" + "reference": "3860fa12a5013b48d445909c6ea07f870e10ba7c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/467464da294734b0fb17e853e5712abc8470f819", - "reference": "467464da294734b0fb17e853e5712abc8470f819", + "url": "https://api.github.com/repos/symfony/cache/zipball/3860fa12a5013b48d445909c6ea07f870e10ba7c", + "reference": "3860fa12a5013b48d445909c6ea07f870e10ba7c", "shasum": "" }, "require": { @@ -10522,7 +11454,7 @@ "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/v7.4.8" + "source": "https://github.com/symfony/cache/tree/v7.4.9" }, "funding": [ { @@ -10542,7 +11474,7 @@ "type": "tidelift" } ], - "time": "2026-03-30T15:15:47+00:00" + "time": "2026-04-29T13:21:53+00:00" }, { "name": "symfony/cache-contracts", @@ -10700,16 +11632,16 @@ }, { "name": "symfony/config", - "version": "v7.4.8", + "version": "v7.4.9", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "2d19dde43fa2ff720b9a40763ace7226594f503b" + "reference": "d4a277b7a0f26487db16b264d935c617b7d994ea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/2d19dde43fa2ff720b9a40763ace7226594f503b", - "reference": "2d19dde43fa2ff720b9a40763ace7226594f503b", + "url": "https://api.github.com/repos/symfony/config/zipball/d4a277b7a0f26487db16b264d935c617b7d994ea", + "reference": "d4a277b7a0f26487db16b264d935c617b7d994ea", "shasum": "" }, "require": { @@ -10755,7 +11687,7 @@ "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/config/tree/v7.4.8" + "source": "https://github.com/symfony/config/tree/v7.4.9" }, "funding": [ { @@ -10775,20 +11707,20 @@ "type": "tidelift" } ], - "time": "2026-03-24T13:12:05+00:00" + "time": "2026-04-29T14:25:20+00:00" }, { "name": "symfony/console", - "version": "v7.4.8", + "version": "v7.4.9", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "1e92e39c51f95b88e3d66fa2d9f06d1fb45dd707" + "reference": "d7d2b64a45a89d607865927b176fa51c33ddbb58" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/1e92e39c51f95b88e3d66fa2d9f06d1fb45dd707", - "reference": "1e92e39c51f95b88e3d66fa2d9f06d1fb45dd707", + "url": "https://api.github.com/repos/symfony/console/zipball/d7d2b64a45a89d607865927b176fa51c33ddbb58", + "reference": "d7d2b64a45a89d607865927b176fa51c33ddbb58", "shasum": "" }, "require": { @@ -10853,7 +11785,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.4.8" + "source": "https://github.com/symfony/console/tree/v7.4.9" }, "funding": [ { @@ -10873,20 +11805,20 @@ "type": "tidelift" } ], - "time": "2026-03-30T13:54:39+00:00" + "time": "2026-04-22T15:21:55+00:00" }, { "name": "symfony/css-selector", - "version": "v7.4.8", + "version": "v7.4.9", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "b055f228a4178a1d6774909903905e3475f3eac8" + "reference": "b75663ed96cf4756e28e3105476f220f92886cc4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/b055f228a4178a1d6774909903905e3475f3eac8", - "reference": "b055f228a4178a1d6774909903905e3475f3eac8", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/b75663ed96cf4756e28e3105476f220f92886cc4", + "reference": "b75663ed96cf4756e28e3105476f220f92886cc4", "shasum": "" }, "require": { @@ -10922,7 +11854,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v7.4.8" + "source": "https://github.com/symfony/css-selector/tree/v7.4.9" }, "funding": [ { @@ -10942,20 +11874,20 @@ "type": "tidelift" } ], - "time": "2026-03-24T13:12:05+00:00" + "time": "2026-04-18T13:18:21+00:00" }, { "name": "symfony/dependency-injection", - "version": "v7.4.8", + "version": "v7.4.9", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "f7025fd7b687c240426562f86ada06a93b1e771d" + "reference": "27cd9f912438d07ced76008bc66cf8b0cf4de622" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/f7025fd7b687c240426562f86ada06a93b1e771d", - "reference": "f7025fd7b687c240426562f86ada06a93b1e771d", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/27cd9f912438d07ced76008bc66cf8b0cf4de622", + "reference": "27cd9f912438d07ced76008bc66cf8b0cf4de622", "shasum": "" }, "require": { @@ -11006,7 +11938,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v7.4.8" + "source": "https://github.com/symfony/dependency-injection/tree/v7.4.9" }, "funding": [ { @@ -11026,7 +11958,7 @@ "type": "tidelift" } ], - "time": "2026-03-31T06:50:29+00:00" + "time": "2026-04-30T18:38:49+00:00" }, { "name": "symfony/deprecation-contracts", @@ -11097,16 +12029,16 @@ }, { "name": "symfony/doctrine-bridge", - "version": "v7.4.8", + "version": "v7.4.9", "source": { "type": "git", "url": "https://github.com/symfony/doctrine-bridge.git", - "reference": "3f8f805e54ecb5cbd487b1eff8837a8bbd278669" + "reference": "7a87c85853f3069e3657a823c62b02952de46b0a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/3f8f805e54ecb5cbd487b1eff8837a8bbd278669", - "reference": "3f8f805e54ecb5cbd487b1eff8837a8bbd278669", + "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/7a87c85853f3069e3657a823c62b02952de46b0a", + "reference": "7a87c85853f3069e3657a823c62b02952de46b0a", "shasum": "" }, "require": { @@ -11186,7 +12118,7 @@ "description": "Provides integration for Doctrine with various Symfony components", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/doctrine-bridge/tree/v7.4.8" + "source": "https://github.com/symfony/doctrine-bridge/tree/v7.4.9" }, "funding": [ { @@ -11206,7 +12138,7 @@ "type": "tidelift" } ], - "time": "2026-03-24T13:12:05+00:00" + "time": "2026-04-29T14:19:39+00:00" }, { "name": "symfony/dom-crawler", @@ -11282,16 +12214,16 @@ }, { "name": "symfony/dotenv", - "version": "v7.4.8", + "version": "v7.4.9", "source": { "type": "git", "url": "https://github.com/symfony/dotenv.git", - "reference": "5df79f11350166125fe754c85b87f7e13d735314" + "reference": "ba757a8564a0ccac1a26a859b83295645020ea68" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dotenv/zipball/5df79f11350166125fe754c85b87f7e13d735314", - "reference": "5df79f11350166125fe754c85b87f7e13d735314", + "url": "https://api.github.com/repos/symfony/dotenv/zipball/ba757a8564a0ccac1a26a859b83295645020ea68", + "reference": "ba757a8564a0ccac1a26a859b83295645020ea68", "shasum": "" }, "require": { @@ -11336,7 +12268,7 @@ "environment" ], "support": { - "source": "https://github.com/symfony/dotenv/tree/v7.4.8" + "source": "https://github.com/symfony/dotenv/tree/v7.4.9" }, "funding": [ { @@ -11356,7 +12288,7 @@ "type": "tidelift" } ], - "time": "2026-03-30T12:55:43+00:00" + "time": "2026-04-29T13:21:53+00:00" }, { "name": "symfony/error-handler", @@ -11442,16 +12374,16 @@ }, { "name": "symfony/event-dispatcher", - "version": "v7.4.8", + "version": "v7.4.9", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "f57b899fa736fd71121168ef268f23c206083f0a" + "reference": "e4a2e29753c7801f7a8340e066cfa788f3bc8101" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/f57b899fa736fd71121168ef268f23c206083f0a", - "reference": "f57b899fa736fd71121168ef268f23c206083f0a", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/e4a2e29753c7801f7a8340e066cfa788f3bc8101", + "reference": "e4a2e29753c7801f7a8340e066cfa788f3bc8101", "shasum": "" }, "require": { @@ -11503,7 +12435,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.8" + "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.9" }, "funding": [ { @@ -11523,7 +12455,7 @@ "type": "tidelift" } ], - "time": "2026-03-30T13:54:39+00:00" + "time": "2026-04-18T13:18:21+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -11671,16 +12603,16 @@ }, { "name": "symfony/filesystem", - "version": "v7.4.8", + "version": "v7.4.9", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "58b9790d12f9670b7f53a1c1738febd3108970a5" + "reference": "dcd8f96bcdc0f128ec406c765cc066c6035d1be3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/58b9790d12f9670b7f53a1c1738febd3108970a5", - "reference": "58b9790d12f9670b7f53a1c1738febd3108970a5", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/dcd8f96bcdc0f128ec406c765cc066c6035d1be3", + "reference": "dcd8f96bcdc0f128ec406c765cc066c6035d1be3", "shasum": "" }, "require": { @@ -11717,7 +12649,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.4.8" + "source": "https://github.com/symfony/filesystem/tree/v7.4.9" }, "funding": [ { @@ -11737,7 +12669,7 @@ "type": "tidelift" } ], - "time": "2026-03-24T13:12:05+00:00" + "time": "2026-04-18T13:18:21+00:00" }, { "name": "symfony/finder", @@ -11882,16 +12814,16 @@ }, { "name": "symfony/form", - "version": "v7.4.8", + "version": "v7.4.9", "source": { "type": "git", "url": "https://github.com/symfony/form.git", - "reference": "fbb79fc4de32f091ec697276824331f5de3a87b4" + "reference": "b6c107af659106abec1771d9d7d22da528644d3a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/form/zipball/fbb79fc4de32f091ec697276824331f5de3a87b4", - "reference": "fbb79fc4de32f091ec697276824331f5de3a87b4", + "url": "https://api.github.com/repos/symfony/form/zipball/b6c107af659106abec1771d9d7d22da528644d3a", + "reference": "b6c107af659106abec1771d9d7d22da528644d3a", "shasum": "" }, "require": { @@ -11961,7 +12893,7 @@ "description": "Allows to easily create, process and reuse HTML forms", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/form/tree/v7.4.8" + "source": "https://github.com/symfony/form/tree/v7.4.9" }, "funding": [ { @@ -11981,20 +12913,20 @@ "type": "tidelift" } ], - "time": "2026-03-24T13:12:05+00:00" + "time": "2026-04-29T14:39:19+00:00" }, { "name": "symfony/framework-bundle", - "version": "v7.4.8", + "version": "v7.4.9", "source": { "type": "git", "url": "https://github.com/symfony/framework-bundle.git", - "reference": "180533cfbac2144349044267db31d5d3df9957cb" + "reference": "601423cc0af2eb5e8c4acdf21fed553d456ff802" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/180533cfbac2144349044267db31d5d3df9957cb", - "reference": "180533cfbac2144349044267db31d5d3df9957cb", + "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/601423cc0af2eb5e8c4acdf21fed553d456ff802", + "reference": "601423cc0af2eb5e8c4acdf21fed553d456ff802", "shasum": "" }, "require": { @@ -12030,7 +12962,7 @@ "symfony/lock": "<6.4", "symfony/mailer": "<6.4", "symfony/messenger": "<7.4", - "symfony/mime": "<6.4", + "symfony/mime": "<6.4.37|>=7.0,<7.4.9|>=8.0,<8.0.9", "symfony/property-access": "<6.4", "symfony/property-info": "<6.4", "symfony/runtime": "<6.4.13|>=7.0,<7.1.6", @@ -12068,7 +13000,7 @@ "symfony/lock": "^6.4|^7.0|^8.0", "symfony/mailer": "^6.4|^7.0|^8.0", "symfony/messenger": "^7.4|^8.0", - "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4.37|^7.4.9|^8.0.9", "symfony/notifier": "^6.4|^7.0|^8.0", "symfony/object-mapper": "^7.3|^8.0", "symfony/polyfill-intl-icu": "~1.0", @@ -12119,7 +13051,7 @@ "description": "Provides a tight integration between Symfony components and the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/framework-bundle/tree/v7.4.8" + "source": "https://github.com/symfony/framework-bundle/tree/v7.4.9" }, "funding": [ { @@ -12139,20 +13071,20 @@ "type": "tidelift" } ], - "time": "2026-03-30T12:55:43+00:00" + "time": "2026-04-30T08:57:13+00:00" }, { "name": "symfony/http-client", - "version": "v7.4.8", + "version": "v7.4.9", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "01933e626c3de76bea1e22641e205e78f6a34342" + "reference": "7e941c6abf4e3bf7dca160bf0e11ef36a9f832f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/01933e626c3de76bea1e22641e205e78f6a34342", - "reference": "01933e626c3de76bea1e22641e205e78f6a34342", + "url": "https://api.github.com/repos/symfony/http-client/zipball/7e941c6abf4e3bf7dca160bf0e11ef36a9f832f6", + "reference": "7e941c6abf4e3bf7dca160bf0e11ef36a9f832f6", "shasum": "" }, "require": { @@ -12220,7 +13152,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v7.4.8" + "source": "https://github.com/symfony/http-client/tree/v7.4.9" }, "funding": [ { @@ -12240,7 +13172,7 @@ "type": "tidelift" } ], - "time": "2026-03-30T12:55:43+00:00" + "time": "2026-04-29T13:25:15+00:00" }, { "name": "symfony/http-client-contracts", @@ -12697,16 +13629,16 @@ }, { "name": "symfony/mime", - "version": "v7.4.8", + "version": "v7.4.9", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "6df02f99998081032da3407a8d6c4e1dcb5d4379" + "reference": "2d550c4758ba4c47519a6667c36553d535705b0c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/6df02f99998081032da3407a8d6c4e1dcb5d4379", - "reference": "6df02f99998081032da3407a8d6c4e1dcb5d4379", + "url": "https://api.github.com/repos/symfony/mime/zipball/2d550c4758ba4c47519a6667c36553d535705b0c", + "reference": "2d550c4758ba4c47519a6667c36553d535705b0c", "shasum": "" }, "require": { @@ -12762,7 +13694,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.4.8" + "source": "https://github.com/symfony/mime/tree/v7.4.9" }, "funding": [ { @@ -12782,20 +13714,20 @@ "type": "tidelift" } ], - "time": "2026-03-30T14:11:46+00:00" + "time": "2026-04-29T13:21:53+00:00" }, { "name": "symfony/monolog-bridge", - "version": "v7.4.8", + "version": "v7.4.9", "source": { "type": "git", "url": "https://github.com/symfony/monolog-bridge.git", - "reference": "b52aeb44645a9a84a1795b973cc5c77a76df0720" + "reference": "20366bceee51838144a14805204bb792cb3d09f2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/monolog-bridge/zipball/b52aeb44645a9a84a1795b973cc5c77a76df0720", - "reference": "b52aeb44645a9a84a1795b973cc5c77a76df0720", + "url": "https://api.github.com/repos/symfony/monolog-bridge/zipball/20366bceee51838144a14805204bb792cb3d09f2", + "reference": "20366bceee51838144a14805204bb792cb3d09f2", "shasum": "" }, "require": { @@ -12845,7 +13777,7 @@ "description": "Provides integration for Monolog with various Symfony components", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/monolog-bridge/tree/v7.4.8" + "source": "https://github.com/symfony/monolog-bridge/tree/v7.4.9" }, "funding": [ { @@ -12865,7 +13797,7 @@ "type": "tidelift" } ], - "time": "2026-03-30T13:54:39+00:00" + "time": "2026-04-29T13:21:53+00:00" }, { "name": "symfony/monolog-bundle", @@ -13095,7 +14027,7 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.36.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", @@ -13154,7 +14086,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.36.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0" }, "funding": [ { @@ -13178,16 +14110,16 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.36.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "ad1b7b9092976d6c948b8a187cec9faaea9ec1df" + "reference": "4864388bfbd3001ce88e234fab652acd91fdc57e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/ad1b7b9092976d6c948b8a187cec9faaea9ec1df", - "reference": "ad1b7b9092976d6c948b8a187cec9faaea9ec1df", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/4864388bfbd3001ce88e234fab652acd91fdc57e", + "reference": "4864388bfbd3001ce88e234fab652acd91fdc57e", "shasum": "" }, "require": { @@ -13236,7 +14168,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.36.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.37.0" }, "funding": [ { @@ -13256,11 +14188,11 @@ "type": "tidelift" } ], - "time": "2026-04-10T16:19:22+00:00" + "time": "2026-04-26T13:13:48+00:00" }, { "name": "symfony/polyfill-intl-icu", - "version": "v1.36.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-icu.git", @@ -13324,7 +14256,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-icu/tree/v1.36.0" + "source": "https://github.com/symfony/polyfill-intl-icu/tree/v1.37.0" }, "funding": [ { @@ -13348,7 +14280,7 @@ }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.36.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", @@ -13411,7 +14343,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.36.0" + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.37.0" }, "funding": [ { @@ -13435,7 +14367,7 @@ }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.36.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", @@ -13496,7 +14428,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.36.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.37.0" }, "funding": [ { @@ -13520,7 +14452,7 @@ }, { "name": "symfony/polyfill-php83", - "version": "v1.36.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php83.git", @@ -13576,7 +14508,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.36.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.37.0" }, "funding": [ { @@ -13600,7 +14532,7 @@ }, { "name": "symfony/polyfill-php84", - "version": "v1.36.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php84.git", @@ -13656,7 +14588,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php84/tree/v1.36.0" + "source": "https://github.com/symfony/polyfill-php84/tree/v1.37.0" }, "funding": [ { @@ -13680,16 +14612,16 @@ }, { "name": "symfony/polyfill-php85", - "version": "v1.36.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php85.git", - "reference": "2c408a6bb0313e6001a83628dc5506100474254e" + "reference": "fcfa4973a9917cef23f2e38774da74a2b7d115ee" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/2c408a6bb0313e6001a83628dc5506100474254e", - "reference": "2c408a6bb0313e6001a83628dc5506100474254e", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/fcfa4973a9917cef23f2e38774da74a2b7d115ee", + "reference": "fcfa4973a9917cef23f2e38774da74a2b7d115ee", "shasum": "" }, "require": { @@ -13736,7 +14668,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php85/tree/v1.36.0" + "source": "https://github.com/symfony/polyfill-php85/tree/v1.37.0" }, "funding": [ { @@ -13756,11 +14688,11 @@ "type": "tidelift" } ], - "time": "2026-04-10T16:50:15+00:00" + "time": "2026-04-26T13:10:57+00:00" }, { "name": "symfony/polyfill-uuid", - "version": "v1.36.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-uuid.git", @@ -13819,7 +14751,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/polyfill-uuid/tree/v1.36.0" + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.37.0" }, "funding": [ { @@ -14167,16 +15099,16 @@ }, { "name": "symfony/rate-limiter", - "version": "v7.4.8", + "version": "v7.4.9", "source": { "type": "git", "url": "https://github.com/symfony/rate-limiter.git", - "reference": "d55de9ec479418f58464e122e68d33886cf6f1fb" + "reference": "0b0078d29f6e64b8833492e9f76a853f23fb1a81" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/rate-limiter/zipball/d55de9ec479418f58464e122e68d33886cf6f1fb", - "reference": "d55de9ec479418f58464e122e68d33886cf6f1fb", + "url": "https://api.github.com/repos/symfony/rate-limiter/zipball/0b0078d29f6e64b8833492e9f76a853f23fb1a81", + "reference": "0b0078d29f6e64b8833492e9f76a853f23fb1a81", "shasum": "" }, "require": { @@ -14217,7 +15149,7 @@ "rate-limiter" ], "support": { - "source": "https://github.com/symfony/rate-limiter/tree/v7.4.8" + "source": "https://github.com/symfony/rate-limiter/tree/v7.4.9" }, "funding": [ { @@ -14237,20 +15169,20 @@ "type": "tidelift" } ], - "time": "2026-03-24T13:12:05+00:00" + "time": "2026-04-29T13:21:53+00:00" }, { "name": "symfony/routing", - "version": "v7.4.8", + "version": "v7.4.9", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "9608de9873ec86e754fb6c0a0fa7e5f1a960eb6b" + "reference": "287771d8bc86eacb30678dd10eda6c64a859951f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/9608de9873ec86e754fb6c0a0fa7e5f1a960eb6b", - "reference": "9608de9873ec86e754fb6c0a0fa7e5f1a960eb6b", + "url": "https://api.github.com/repos/symfony/routing/zipball/287771d8bc86eacb30678dd10eda6c64a859951f", + "reference": "287771d8bc86eacb30678dd10eda6c64a859951f", "shasum": "" }, "require": { @@ -14302,7 +15234,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v7.4.8" + "source": "https://github.com/symfony/routing/tree/v7.4.9" }, "funding": [ { @@ -14322,7 +15254,7 @@ "type": "tidelift" } ], - "time": "2026-03-24T13:12:05+00:00" + "time": "2026-04-22T15:21:55+00:00" }, { "name": "symfony/runtime", @@ -14686,16 +15618,16 @@ }, { "name": "symfony/security-http", - "version": "v7.4.8", + "version": "v7.4.9", "source": { "type": "git", "url": "https://github.com/symfony/security-http.git", - "reference": "1b07d7d472ba967fd66697067e6274084d2d1d7c" + "reference": "a34991b13899de1f953df245395aa2196f9bc113" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security-http/zipball/1b07d7d472ba967fd66697067e6274084d2d1d7c", - "reference": "1b07d7d472ba967fd66697067e6274084d2d1d7c", + "url": "https://api.github.com/repos/symfony/security-http/zipball/a34991b13899de1f953df245395aa2196f9bc113", + "reference": "a34991b13899de1f953df245395aa2196f9bc113", "shasum": "" }, "require": { @@ -14754,7 +15686,7 @@ "description": "Symfony Security Component - HTTP Integration", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/security-http/tree/v7.4.8" + "source": "https://github.com/symfony/security-http/tree/v7.4.9" }, "funding": [ { @@ -14774,7 +15706,7 @@ "type": "tidelift" } ], - "time": "2026-03-24T13:12:05+00:00" + "time": "2026-04-22T15:21:55+00:00" }, { "name": "symfony/serializer", @@ -15586,16 +16518,16 @@ }, { "name": "symfony/type-info", - "version": "v7.4.8", + "version": "v7.4.9", "source": { "type": "git", "url": "https://github.com/symfony/type-info.git", - "reference": "6bf34da885ff5143a3dfd8f1b863bb8ab95f50bd" + "reference": "cafeedbf157b890e94ac5b83eaed85595106d5d6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/type-info/zipball/6bf34da885ff5143a3dfd8f1b863bb8ab95f50bd", - "reference": "6bf34da885ff5143a3dfd8f1b863bb8ab95f50bd", + "url": "https://api.github.com/repos/symfony/type-info/zipball/cafeedbf157b890e94ac5b83eaed85595106d5d6", + "reference": "cafeedbf157b890e94ac5b83eaed85595106d5d6", "shasum": "" }, "require": { @@ -15645,7 +16577,7 @@ "type" ], "support": { - "source": "https://github.com/symfony/type-info/tree/v7.4.8" + "source": "https://github.com/symfony/type-info/tree/v7.4.9" }, "funding": [ { @@ -15665,20 +16597,20 @@ "type": "tidelift" } ], - "time": "2026-03-24T13:12:05+00:00" + "time": "2026-04-22T15:21:55+00:00" }, { "name": "symfony/uid", - "version": "v7.4.8", + "version": "v7.4.9", "source": { "type": "git", "url": "https://github.com/symfony/uid.git", - "reference": "6883ebdf7bf6a12b37519dbc0df62b0222401b56" + "reference": "2676b524340abcfe4d6151ec698463cebafee439" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/uid/zipball/6883ebdf7bf6a12b37519dbc0df62b0222401b56", - "reference": "6883ebdf7bf6a12b37519dbc0df62b0222401b56", + "url": "https://api.github.com/repos/symfony/uid/zipball/2676b524340abcfe4d6151ec698463cebafee439", + "reference": "2676b524340abcfe4d6151ec698463cebafee439", "shasum": "" }, "require": { @@ -15723,7 +16655,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/uid/tree/v7.4.8" + "source": "https://github.com/symfony/uid/tree/v7.4.9" }, "funding": [ { @@ -15743,7 +16675,7 @@ "type": "tidelift" } ], - "time": "2026-03-24T13:12:05+00:00" + "time": "2026-04-30T15:19:22+00:00" }, { "name": "symfony/ux-translator", @@ -15931,16 +16863,16 @@ }, { "name": "symfony/validator", - "version": "v7.4.8", + "version": "v7.4.9", "source": { "type": "git", "url": "https://github.com/symfony/validator.git", - "reference": "8f73cbddae916756f319b3e195088da216f0f12f" + "reference": "d3bb3dcfbaa26b5782196819dac2e1097d5fae2c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/validator/zipball/8f73cbddae916756f319b3e195088da216f0f12f", - "reference": "8f73cbddae916756f319b3e195088da216f0f12f", + "url": "https://api.github.com/repos/symfony/validator/zipball/d3bb3dcfbaa26b5782196819dac2e1097d5fae2c", + "reference": "d3bb3dcfbaa26b5782196819dac2e1097d5fae2c", "shasum": "" }, "require": { @@ -16011,7 +16943,7 @@ "description": "Provides tools to validate values", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/validator/tree/v7.4.8" + "source": "https://github.com/symfony/validator/tree/v7.4.9" }, "funding": [ { @@ -16031,7 +16963,7 @@ "type": "tidelift" } ], - "time": "2026-03-30T12:55:43+00:00" + "time": "2026-04-30T15:35:16+00:00" }, { "name": "symfony/var-dumper", @@ -16122,16 +17054,16 @@ }, { "name": "symfony/var-exporter", - "version": "v7.4.8", + "version": "v7.4.9", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "398907e89a2a56fe426f7955c6fa943ec0c77225" + "reference": "22e03a49c95ef054a43601cd159b222bfab1c701" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/398907e89a2a56fe426f7955c6fa943ec0c77225", - "reference": "398907e89a2a56fe426f7955c6fa943ec0c77225", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/22e03a49c95ef054a43601cd159b222bfab1c701", + "reference": "22e03a49c95ef054a43601cd159b222bfab1c701", "shasum": "" }, "require": { @@ -16179,7 +17111,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v7.4.8" + "source": "https://github.com/symfony/var-exporter/tree/v7.4.9" }, "funding": [ { @@ -16199,7 +17131,7 @@ "type": "tidelift" } ], - "time": "2026-03-24T13:12:05+00:00" + "time": "2026-04-18T13:18:21+00:00" }, { "name": "symfony/web-link", @@ -16503,16 +17435,16 @@ }, { "name": "tecnickcom/tc-lib-barcode", - "version": "2.4.34", + "version": "2.4.37", "source": { "type": "git", "url": "https://github.com/tecnickcom/tc-lib-barcode.git", - "reference": "4057b35150e383f0425221efaea312e5cb968902" + "reference": "86b89b0399797706a07c1fd4eec3158460b13f7f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/tecnickcom/tc-lib-barcode/zipball/4057b35150e383f0425221efaea312e5cb968902", - "reference": "4057b35150e383f0425221efaea312e5cb968902", + "url": "https://api.github.com/repos/tecnickcom/tc-lib-barcode/zipball/86b89b0399797706a07c1fd4eec3158460b13f7f", + "reference": "86b89b0399797706a07c1fd4eec3158460b13f7f", "shasum": "" }, "require": { @@ -16521,7 +17453,7 @@ "ext-gd": "*", "ext-pcre": "*", "php": ">=8.1", - "tecnickcom/tc-lib-color": "^2.3" + "tecnickcom/tc-lib-color": "^2.5" }, "require-dev": { "pdepend/pdepend": "^2.16", @@ -16600,20 +17532,20 @@ "type": "custom" } ], - "time": "2026-04-20T07:15:41+00:00" + "time": "2026-04-30T19:47:34+00:00" }, { "name": "tecnickcom/tc-lib-color", - "version": "2.3.14", + "version": "2.5.0", "source": { "type": "git", "url": "https://github.com/tecnickcom/tc-lib-color.git", - "reference": "6fbb7daabf4f15b38d264ad9091f0745502459b9" + "reference": "ebb76dfb334b7a0b470e765a7b46c5635dce2fe0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/tecnickcom/tc-lib-color/zipball/6fbb7daabf4f15b38d264ad9091f0745502459b9", - "reference": "6fbb7daabf4f15b38d264ad9091f0745502459b9", + "url": "https://api.github.com/repos/tecnickcom/tc-lib-color/zipball/ebb76dfb334b7a0b470e765a7b46c5635dce2fe0", + "reference": "ebb76dfb334b7a0b470e765a7b46c5635dce2fe0", "shasum": "" }, "require": { @@ -16670,7 +17602,7 @@ "type": "custom" } ], - "time": "2026-04-20T07:09:08+00:00" + "time": "2026-04-30T19:21:47+00:00" }, { "name": "thecodingmachine/safe", @@ -17618,16 +18550,16 @@ }, { "name": "web-auth/webauthn-lib", - "version": "5.2.5", + "version": "5.3.0", "source": { "type": "git", "url": "https://github.com/web-auth/webauthn-lib.git", - "reference": "c28f27cb8f968d2b84db48587563f03bb451b60a" + "reference": "a272f254c056fb3d6c80a4801d3c7c5fedc6a08d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/web-auth/webauthn-lib/zipball/c28f27cb8f968d2b84db48587563f03bb451b60a", - "reference": "c28f27cb8f968d2b84db48587563f03bb451b60a", + "url": "https://api.github.com/repos/web-auth/webauthn-lib/zipball/a272f254c056fb3d6c80a4801d3c7c5fedc6a08d", + "reference": "a272f254c056fb3d6c80a4801d3c7c5fedc6a08d", "shasum": "" }, "require": { @@ -17635,18 +18567,18 @@ "ext-openssl": "*", "paragonie/constant_time_encoding": "^2.6|^3.0", "php": ">=8.2", - "phpdocumentor/reflection-docblock": "^5.3", + "phpdocumentor/reflection-docblock": "^5.3|^6.0", "psr/clock": "^1.0", "psr/event-dispatcher": "^1.0", "psr/log": "^1.0|^2.0|^3.0", "spomky-labs/cbor-php": "^3.0", "spomky-labs/pki-framework": "^1.0", - "symfony/clock": "^6.4|^7.0", + "symfony/clock": "^6.4|^7.0|^8.0", "symfony/deprecation-contracts": "^3.2", - "symfony/property-access": "^6.4|^7.0", - "symfony/property-info": "^6.4|^7.0", - "symfony/serializer": "^6.4|^7.0", - "symfony/uid": "^6.4|^7.0", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", "web-auth/cose-lib": "^4.2.3" }, "suggest": { @@ -17688,7 +18620,7 @@ "webauthn" ], "support": { - "source": "https://github.com/web-auth/webauthn-lib/tree/5.2.5" + "source": "https://github.com/web-auth/webauthn-lib/tree/5.3.0" }, "funding": [ { @@ -17700,34 +18632,35 @@ "type": "patreon" } ], - "time": "2026-03-23T21:43:02+00:00" + "time": "2026-05-01T12:14:37+00:00" }, { "name": "web-auth/webauthn-symfony-bundle", - "version": "5.2.5", + "version": "5.3.0", "source": { "type": "git", "url": "https://github.com/web-auth/webauthn-symfony-bundle.git", - "reference": "91f0aff70703e20d84251c83e238da1f8fc53b24" + "reference": "40f5ae033d4bea090aaa395b906ebfa7d7fe6055" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/web-auth/webauthn-symfony-bundle/zipball/91f0aff70703e20d84251c83e238da1f8fc53b24", - "reference": "91f0aff70703e20d84251c83e238da1f8fc53b24", + "url": "https://api.github.com/repos/web-auth/webauthn-symfony-bundle/zipball/40f5ae033d4bea090aaa395b906ebfa7d7fe6055", + "reference": "40f5ae033d4bea090aaa395b906ebfa7d7fe6055", "shasum": "" }, "require": { "php": ">=8.2", "psr/event-dispatcher": "^1.0", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/framework-bundle": "^6.4|^7.0", - "symfony/http-client": "^6.4|^7.0", - "symfony/security-bundle": "^6.4|^7.0", - "symfony/security-core": "^6.4|^7.0", - "symfony/security-http": "^6.4|^7.0", - "symfony/serializer": "^6.4|^7.0", - "symfony/validator": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4|^7.0|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/security-bundle": "^6.4|^7.0|^8.0", + "symfony/security-core": "^6.4|^7.0|^8.0", + "symfony/security-http": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", + "symfony/service-contracts": "^3.0", + "symfony/validator": "^6.4|^7.0|^8.0", "web-auth/webauthn-lib": "self.version" }, "suggest": { @@ -17770,7 +18703,7 @@ "webauthn" ], "support": { - "source": "https://github.com/web-auth/webauthn-symfony-bundle/tree/5.2.5" + "source": "https://github.com/web-auth/webauthn-symfony-bundle/tree/5.3.0" }, "funding": [ { @@ -17782,7 +18715,7 @@ "type": "patreon" } ], - "time": "2025-12-20T10:20:41+00:00" + "time": "2026-05-01T12:14:37+00:00" }, { "name": "webmozart/assert", @@ -18481,11 +19414,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.51", + "version": "2.1.54", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/dc3b523c45e714c70de2ac5113b958223b55dc59", - "reference": "dc3b523c45e714c70de2ac5113b958223b55dc59", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/8be50c3992107dc837b17da4d140fbbdf9a5c5bd", + "reference": "8be50c3992107dc837b17da4d140fbbdf9a5c5bd", "shasum": "" }, "require": { @@ -18530,7 +19463,7 @@ "type": "github" } ], - "time": "2026-04-21T18:22:01+00:00" + "time": "2026-04-29T13:31:09+00:00" }, { "name": "phpstan/phpstan-doctrine", @@ -19256,18 +20189,18 @@ "source": { "type": "git", "url": "https://github.com/Roave/SecurityAdvisories.git", - "reference": "f41f65e527608a9a76cfbafd873756ed76c5452d" + "reference": "2221f6ef09e87784e78e188aadd8f7e3a50e679a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/f41f65e527608a9a76cfbafd873756ed76c5452d", - "reference": "f41f65e527608a9a76cfbafd873756ed76c5452d", + "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/2221f6ef09e87784e78e188aadd8f7e3a50e679a", + "reference": "2221f6ef09e87784e78e188aadd8f7e3a50e679a", "shasum": "" }, "conflict": { "3f/pygmentize": "<1.2", "adaptcms/adaptcms": "<=1.3", - "admidio/admidio": "<5.0.8", + "admidio/admidio": "<=5.0.8", "adodb/adodb-php": "<=5.22.9", "aheinze/cockpit": "<2.2", "aimeos/ai-admin-graphql": ">=2022.04.1,<2022.10.10|>=2023.04.1,<2023.10.6|>=2024.04.1,<2024.07.2", @@ -19284,6 +20217,7 @@ "alextselegidis/easyappointments": "<=1.5.2", "alexusmai/laravel-file-manager": "<=3.3.1", "algolia/algoliasearch-magento-2": "<=3.16.1|>=3.17.0.0-beta1,<=3.17.1", + "almirhodzic/nova-toggle-5": "<1.3", "alt-design/alt-redirect": "<1.6.4", "altcha-org/altcha": "<1.3.1", "alterphp/easyadmin-extension-bundle": ">=1.2,<1.2.11|>=1.3,<1.3.1", @@ -19375,12 +20309,12 @@ "cesnet/simplesamlphp-module-proxystatistics": "<3.1", "chriskacerguis/codeigniter-restserver": "<=2.7.1", "chrome-php/chrome": "<1.14", - "ci4-cms-erp/ci4ms": "<=0.31.3", + "ci4-cms-erp/ci4ms": "<=0.31.6", "civicrm/civicrm-core": ">=4.2,<4.2.9|>=4.3,<4.3.3", "ckeditor/ckeditor": "<4.25", "clickstorm/cs-seo": ">=6,<6.8|>=7,<7.5|>=8,<8.4|>=9,<9.3", "co-stack/fal_sftp": "<0.2.6", - "cockpit-hq/cockpit": "<2.13.5", + "cockpit-hq/cockpit": "<2.14", "code16/sharp": "<9.20", "codeception/codeception": "<3.1.3|>=4,<4.1.22", "codeigniter/framework": "<3.1.10", @@ -19543,7 +20477,7 @@ "fisharebest/webtrees": "<=2.1.18", "fixpunkt/fp-masterquiz": "<2.2.1|>=3,<3.5.2", "fixpunkt/fp-newsletter": "<1.1.1|>=1.2,<2.1.2|>=2.2,<3.2.6", - "flarum/core": "<1.8.10", + "flarum/core": "<=1.8.15|>=2.0.0.0-beta1,<=2.0.0.0-beta8", "flarum/flarum": "<0.1.0.0-beta8", "flarum/framework": "<1.8.10", "flarum/mentions": "<1.6.3", @@ -19580,7 +20514,7 @@ "geshi/geshi": "<=1.0.9.1", "getformwork/formwork": "<=2.3.3", "getgrav/grav": "<1.11.0.0-beta1", - "getkirby/cms": "<=5.2.1", + "getkirby/cms": "<4.9|>=5,<5.4", "getkirby/kirby": "<3.9.8.3-dev|>=3.10,<3.10.1.2-dev|>=4,<4.7.1", "getkirby/panel": "<2.5.14", "getkirby/starterkit": "<=3.7.0.2", @@ -19640,7 +20574,7 @@ "intelliants/subrion": "<4.2.2", "inter-mediator/inter-mediator": "==5.5", "invoiceninja/invoiceninja": "<5.13.4", - "ipl/web": "<0.10.1", + "ipl/web": "<=0.13", "islandora/crayfish": "<4.1", "islandora/islandora": ">=2,<2.4.1", "ivankristianto/phpwhois": "<=4.3", @@ -19678,7 +20612,7 @@ "kelvinmo/simplexrd": "<3.1.1", "kevinpapst/kimai2": "<1.16.7", "khodakhah/nodcms": "<=3.4.1", - "kimai/kimai": "<=2.53", + "kimai/kimai": "<2.54", "kitodo/presentation": "<3.2.3|>=3.3,<3.3.4", "klaviyo/magento2-extension": ">=1,<3", "knplabs/knp-snappy": "<=1.4.2", @@ -19876,7 +20810,7 @@ "phpoffice/common": "<0.2.9", "phpoffice/math": "<=0.2", "phpoffice/phpexcel": "<=1.8.2", - "phpoffice/phpspreadsheet": "<1.30|>=2,<2.1.12|>=2.2,<2.4|>=3,<3.10|>=4,<5", + "phpoffice/phpspreadsheet": "<=1.30.3|>=2,<=2.1.15|>=2.2,<=2.4.4|>=3,<=3.10.4|>=4,<=5.6", "phppgadmin/phppgadmin": "<=7.13", "phpseclib/phpseclib": "<2.0.53|>=3,<3.0.51", "phpservermon/phpservermon": "<3.6", @@ -19910,7 +20844,7 @@ "prestashop/gamification": "<2.3.2", "prestashop/prestashop": "<8.2.5|>=9.0.0.0-alpha1,<9.1", "prestashop/productcomments": "<5.0.2", - "prestashop/ps_checkout": "<4.4.1|>=5,<5.0.5", + "prestashop/ps_checkout": "<5.3", "prestashop/ps_contactinfo": "<=3.3.2", "prestashop/ps_emailsubscription": "<2.6.1", "prestashop/ps_facetedsearch": "<3.4.1", @@ -19947,6 +20881,7 @@ "rhukster/dom-sanitizer": "<1.0.10", "rmccue/requests": ">=1.6,<1.8", "roadiz/documents": "<2.3.42|>=2.4,<2.5.44|>=2.6,<2.6.28|>=2.7,<2.7.9", + "roadiz/openid": "<2.3.43|>=2.5,<2.5.45|>=2.6,<2.6.31|>=2.7,<2.7.18", "robrichards/xmlseclibs": "<3.1.5", "roots/soil": "<4.1", "roundcube/roundcubemail": "<1.5.10|>=1.6,<1.6.11|>=1.7.0.0-beta,<1.7.0.0-RC5-dev", @@ -20122,7 +21057,7 @@ "twig/twig": "<3.11.2|>=3.12,<3.14.1|>=3.16,<3.19", "typicms/core": "<16.1.7", "typo3/cms": "<9.5.29|>=10,<10.4.35|>=11,<11.5.23|>=12,<12.2", - "typo3/cms-backend": "<4.1.14|>=4.2,<4.2.15|>=4.3,<4.3.7|>=4.4,<4.4.4|>=7,<=7.6.50|>=8,<=8.7.39|>=9,<9.5.55|>=10,<=10.4.54|>=11,<=11.5.48|>=12,<=12.4.40|>=13,<=13.4.22|>=14,<=14.0.1", + "typo3/cms-backend": "<4.1.14|>=4.2,<4.2.15|>=4.3,<4.3.7|>=4.4,<4.4.4|>=7,<=7.6.50|>=8,<=8.7.39|>=9,<9.5.55|>=10,<=10.4.54|>=11,<=11.5.48|>=12,<=12.4.40|>=13,<=13.4.22|>=14,<=14.0.1|==14.2", "typo3/cms-belog": ">=10,<=10.4.47|>=11,<=11.5.41|>=12,<=12.4.24|>=13,<=13.4.2", "typo3/cms-beuser": ">=9,<9.5.55|>=10,<10.4.54|>=11,<11.5.48|>=12,<12.4.37|>=13,<13.4.18", "typo3/cms-core": "<=8.7.56|>=9,<9.5.55|>=10,<=10.4.54|>=11,<=11.5.48|>=12,<=12.4.40|>=13,<=13.4.22|>=14,<=14.0.1", @@ -20304,7 +21239,7 @@ "type": "tidelift" } ], - "time": "2026-04-21T17:25:01+00:00" + "time": "2026-04-30T21:24:12+00:00" }, { "name": "sebastian/cli-parser", @@ -21678,16 +22613,16 @@ }, { "name": "symfony/web-profiler-bundle", - "version": "v7.4.8", + "version": "v7.4.9", "source": { "type": "git", "url": "https://github.com/symfony/web-profiler-bundle.git", - "reference": "79f039096c67cc1cc3f607d2ba72af86cd27e6a4" + "reference": "36dd8b8c05da059925c5804641aad9159e5b73e8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/web-profiler-bundle/zipball/79f039096c67cc1cc3f607d2ba72af86cd27e6a4", - "reference": "79f039096c67cc1cc3f607d2ba72af86cd27e6a4", + "url": "https://api.github.com/repos/symfony/web-profiler-bundle/zipball/36dd8b8c05da059925c5804641aad9159e5b73e8", + "reference": "36dd8b8c05da059925c5804641aad9159e5b73e8", "shasum": "" }, "require": { @@ -21744,7 +22679,7 @@ "dev" ], "support": { - "source": "https://github.com/symfony/web-profiler-bundle/tree/v7.4.8" + "source": "https://github.com/symfony/web-profiler-bundle/tree/v7.4.9" }, "funding": [ { @@ -21764,7 +22699,7 @@ "type": "tidelift" } ], - "time": "2026-03-24T13:12:05+00:00" + "time": "2026-04-22T15:21:55+00:00" }, { "name": "theseer/tokenizer", diff --git a/config/bundles.php b/config/bundles.php index ae7dc9cc..000c58a1 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -33,4 +33,5 @@ return [ Jbtronics\SettingsBundle\JbtronicsSettingsBundle::class => ['all' => true], Jbtronics\TranslationEditorBundle\JbtronicsTranslationEditorBundle::class => ['dev' => true], ApiPlatform\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true], + Symfony\AI\AiBundle\AiBundle::class => ['all' => true], ]; diff --git a/config/packages/ai.yaml b/config/packages/ai.yaml new file mode 100644 index 00000000..89f8e7ae --- /dev/null +++ b/config/packages/ai.yaml @@ -0,0 +1,27 @@ +ai: + platform: + # Inference Platform configuration + # see https://github.com/symfony/ai/tree/main/src/platform#platform-bridges + + # openai: + # api_key: '%env(OPENAI_API_KEY)%' + + agent: + # Agent configuration + # see https://symfony.com/doc/current/ai/bundles/ai-bundle.html + + # default: + # platform: 'ai.platform.openai' + # model: 'gpt-5-mini' + # prompt: | + # You are a pirate and you write funny. + # tools: + # - 'Symfony\AI\Agent\Bridge\Clock\Clock' + + store: + # Store configuration + + # chromadb: + # default: + # client: 'client.service.id' + # collection: 'my_collection' diff --git a/config/packages/ai_generic_platform.yaml b/config/packages/ai_generic_platform.yaml new file mode 100644 index 00000000..c2c2e133 --- /dev/null +++ b/config/packages/ai_generic_platform.yaml @@ -0,0 +1,5 @@ +ai: + platform: + generic: + default: + base_url: '%env(GENERIC_BASE_URL)%' diff --git a/config/packages/ai_lm_studio_platform.yaml b/config/packages/ai_lm_studio_platform.yaml new file mode 100644 index 00000000..0e4287e0 --- /dev/null +++ b/config/packages/ai_lm_studio_platform.yaml @@ -0,0 +1,4 @@ +ai: + platform: + lmstudio: + host_url: '%env(string:settings:ai_lmstudio:hostURL)%' diff --git a/config/packages/ai_open_router_platform.yaml b/config/packages/ai_open_router_platform.yaml new file mode 100644 index 00000000..d34de592 --- /dev/null +++ b/config/packages/ai_open_router_platform.yaml @@ -0,0 +1,4 @@ +ai: + platform: + openrouter: + api_key: '%env(string:settings:ai_openrouter:apiKey)%' diff --git a/config/reference.php b/config/reference.php index 77a5d432..e1304d43 100644 --- a/config/reference.php +++ b/config/reference.php @@ -128,7 +128,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * @psalm-type FrameworkConfig = array{ * secret?: scalar|Param|null, * http_method_override?: bool|Param, // Set true to enable support for the '_method' request parameter to determine the intended HTTP method on POST requests. // Default: false - * allowed_http_method_override?: list|null, + * allowed_http_method_override?: null|list, * trust_x_sendfile_type_header?: scalar|Param|null, // Set true to enable support for xsendfile in binary file responses. // Default: "%env(bool:default::SYMFONY_TRUST_X_SENDFILE_TYPE_HEADER)%" * ide?: scalar|Param|null, // Default: "%env(default::SYMFONY_IDE)%" * test?: bool|Param, @@ -136,9 +136,9 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * set_locale_from_accept_language?: bool|Param, // Whether to use the Accept-Language HTTP header to set the Request locale (only when the "_locale" request attribute is not passed). // Default: false * set_content_language_from_locale?: bool|Param, // Whether to set the Content-Language HTTP header on the Response using the Request locale. // Default: false * enabled_locales?: list, - * trusted_hosts?: list, + * trusted_hosts?: string|list, * trusted_proxies?: mixed, // Default: ["%env(default::SYMFONY_TRUSTED_PROXIES)%"] - * trusted_headers?: list, + * trusted_headers?: string|list, * error_controller?: scalar|Param|null, // Default: "error_controller" * handle_all_throwables?: bool|Param, // HttpKernel will handle all kinds of \Throwable. // Default: true * csrf_protection?: bool|array{ @@ -193,40 +193,40 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * workflows?: bool|array{ * enabled?: bool|Param, // Default: false * workflows?: array, - * definition_validators?: list, - * support_strategy?: scalar|Param|null, - * initial_marking?: list, - * events_to_dispatch?: list|null, - * places?: list, + * definition_validators?: list, + * support_strategy?: scalar|Param|null, + * initial_marking?: backed-enum|string|list, + * events_to_dispatch?: null|list, + * places?: string|list, + * }>, + * transitions?: list, + * to?: backed-enum|string|list, + * weight?: int|Param, // Default: 1 + * metadata?: array, + * }>, * metadata?: array, * }>, - * transitions?: list, - * to?: list, - * weight?: int|Param, // Default: 1 - * metadata?: array, - * }>, - * metadata?: array, - * }>, * }, * router?: bool|array{ // Router configuration * enabled?: bool|Param, // Default: false @@ -271,20 +271,20 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * version_format?: scalar|Param|null, // Default: "%%s?%%s" * json_manifest_path?: scalar|Param|null, // Default: null * base_path?: scalar|Param|null, // Default: "" - * base_urls?: list, + * base_urls?: string|list, * packages?: array, - * }>, + * strict_mode?: bool|Param, // Throw an exception if an entry is missing from the manifest.json. // Default: false + * version_strategy?: scalar|Param|null, // Default: null + * version?: scalar|Param|null, + * version_format?: scalar|Param|null, // Default: null + * json_manifest_path?: scalar|Param|null, // Default: null + * base_path?: scalar|Param|null, // Default: "" + * base_urls?: string|list, + * }>, * }, * asset_mapper?: bool|array{ // Asset Mapper configuration * enabled?: bool|Param, // Default: false - * paths?: array, + * paths?: string|array, * excluded_patterns?: list, * exclude_dotfiles?: bool|Param, // If true, any files starting with "." will be excluded from the asset mapper. // Default: true * server?: bool|Param, // If true, a "dev server" will return the assets from the public directory (true in "debug" mode only by default). // Default: true @@ -303,7 +303,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * }, * translator?: bool|array{ // Translator configuration * enabled?: bool|Param, // Default: true - * fallbacks?: list, + * fallbacks?: string|list, * logging?: bool|Param, // Default: false * formatter?: scalar|Param|null, // Default: "translator.formatter.default" * cache_dir?: scalar|Param|null, // Default: "%kernel.cache_dir%/translations" @@ -318,22 +318,22 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * localizable_html_attributes?: list, * }, * providers?: array, - * locales?: list, - * }>, + * dsn?: scalar|Param|null, + * domains?: list, + * locales?: list, + * }>, * globals?: array, - * domain?: string|Param, - * }>, + * value?: mixed, + * message?: string|Param, + * parameters?: array, + * domain?: string|Param, + * }>, * }, * validation?: bool|array{ // Validation configuration * enabled?: bool|Param, // Default: true * cache?: scalar|Param|null, // Deprecated: Setting the "framework.validation.cache.cache" configuration option is deprecated. It will be removed in version 8.0. * enable_attributes?: bool|Param, // Default: true - * static_method?: list, + * static_method?: string|list, * translation_domain?: scalar|Param|null, // Default: "validators" * email_validation_mode?: "html5"|"html5-allow-no-tld"|"strict"|"loose"|Param, // Default: "html5" * mapping?: array{ @@ -345,8 +345,8 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * }, * disable_translation?: bool|Param, // Default: false * auto_mapping?: array, - * }>, + * services?: list, + * }>, * }, * annotations?: bool|array{ * enabled?: bool|Param, // Default: false @@ -362,11 +362,11 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * }, * default_context?: array, * named_serializers?: array, - * include_built_in_normalizers?: bool|Param, // Whether to include the built-in normalizers // Default: true - * include_built_in_encoders?: bool|Param, // Whether to include the built-in encoders // Default: true - * }>, + * name_converter?: scalar|Param|null, + * default_context?: array, + * include_built_in_normalizers?: bool|Param, // Whether to include the built-in normalizers // Default: true + * include_built_in_encoders?: bool|Param, // Whether to include the built-in encoders // Default: true + * }>, * }, * property_access?: bool|array{ // Property access configuration * enabled?: bool|Param, // Default: true @@ -396,40 +396,40 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * default_doctrine_dbal_provider?: scalar|Param|null, // Default: "database_connection" * default_pdo_provider?: scalar|Param|null, // Default: null * pools?: array, - * tags?: scalar|Param|null, // Default: null - * public?: bool|Param, // Default: false - * default_lifetime?: scalar|Param|null, // Default lifetime of the pool. - * provider?: scalar|Param|null, // Overwrite the setting from the default provider for this adapter. - * early_expiration_message_bus?: scalar|Param|null, - * clearer?: scalar|Param|null, - * }>, + * adapters?: string|list, + * tags?: scalar|Param|null, // Default: null + * public?: bool|Param, // Default: false + * default_lifetime?: scalar|Param|null, // Default lifetime of the pool. + * provider?: scalar|Param|null, // Overwrite the setting from the default provider for this adapter. + * early_expiration_message_bus?: scalar|Param|null, + * clearer?: scalar|Param|null, + * }>, * }, * php_errors?: array{ // PHP errors handling configuration * log?: mixed, // Use the application logger instead of the PHP logger for logging PHP errors. // Default: true * throw?: bool|Param, // Throw PHP errors as \ErrorException instances. // Default: true * }, * exceptions?: array, + * log_level?: scalar|Param|null, // The level of log message. Null to let Symfony decide. // Default: null + * status_code?: scalar|Param|null, // The status code of the response. Null or 0 to let Symfony decide. // Default: null + * log_channel?: scalar|Param|null, // The channel of log message. Null to let Symfony decide. // Default: null + * }>, * web_link?: bool|array{ // Web links configuration * enabled?: bool|Param, // Default: true * }, * lock?: bool|string|array{ // Lock configuration * enabled?: bool|Param, // Default: false - * resources?: array>, + * resources?: string|array>, * }, * semaphore?: bool|string|array{ // Semaphore configuration * enabled?: bool|Param, // Default: false - * resources?: array, + * resources?: string|array, * }, * messenger?: bool|array{ // Messenger configuration * enabled?: bool|Param, // Default: false * routing?: array, - * }>, + * senders?: list, + * }>, * serializer?: array{ * default_serializer?: scalar|Param|null, // Service id to use as the default serializer for the transports. // Default: "messenger.transport.native_php_serializer" * symfony_serializer?: array{ @@ -438,34 +438,34 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * }, * }, * transports?: array, - * failure_transport?: scalar|Param|null, // Transport name to send failed messages to (after all retries have failed). // Default: null - * retry_strategy?: string|array{ - * service?: scalar|Param|null, // Service id to override the retry strategy entirely. // Default: null - * max_retries?: int|Param, // Default: 3 - * delay?: int|Param, // Time in ms to delay (or the initial value when multiplier is used). // Default: 1000 - * multiplier?: float|Param, // If greater than 1, delay will grow exponentially for each retry: this delay = (delay * (multiple ^ retries)). // Default: 2 - * max_delay?: int|Param, // Max time in ms that a retry should ever be delayed (0 = infinite). // Default: 0 - * jitter?: float|Param, // Randomness to apply to the delay (between 0 and 1). // Default: 0.1 - * }, - * rate_limiter?: scalar|Param|null, // Rate limiter name to use when processing messages. // Default: null - * }>, + * dsn?: scalar|Param|null, + * serializer?: scalar|Param|null, // Service id of a custom serializer to use. // Default: null + * options?: array, + * failure_transport?: scalar|Param|null, // Transport name to send failed messages to (after all retries have failed). // Default: null + * retry_strategy?: string|array{ + * service?: scalar|Param|null, // Service id to override the retry strategy entirely. // Default: null + * max_retries?: int|Param, // Default: 3 + * delay?: int|Param, // Time in ms to delay (or the initial value when multiplier is used). // Default: 1000 + * multiplier?: float|Param, // If greater than 1, delay will grow exponentially for each retry: this delay = (delay * (multiple ^ retries)). // Default: 2 + * max_delay?: int|Param, // Max time in ms that a retry should ever be delayed (0 = infinite). // Default: 0 + * jitter?: float|Param, // Randomness to apply to the delay (between 0 and 1). // Default: 0.1 + * }, + * rate_limiter?: scalar|Param|null, // Rate limiter name to use when processing messages. // Default: null + * }>, * failure_transport?: scalar|Param|null, // Transport name to send failed messages to (after all retries have failed). // Default: null - * stop_worker_on_signals?: list, + * stop_worker_on_signals?: int|string|list, * default_bus?: scalar|Param|null, // Default: null * buses?: array, + * default_middleware?: bool|string|array{ + * enabled?: bool|Param, // Default: true + * allow_no_handlers?: bool|Param, // Default: false + * allow_no_senders?: bool|Param, // Default: true + * }, + * middleware?: string|list, + * }>, * }>, - * }>, * }, * scheduler?: bool|array{ // Scheduler configuration * enabled?: bool|Param, // Default: false @@ -510,10 +510,10 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * retry_failed?: bool|array{ * enabled?: bool|Param, // Default: false * retry_strategy?: scalar|Param|null, // service id to override the retry strategy. // Default: null - * http_codes?: array, - * }>, + * http_codes?: int|string|array, + * }>, * max_retries?: int|Param, // Default: 3 * delay?: int|Param, // Time in ms to delay (or the initial value when multiplier is used). // Default: 1000 * multiplier?: float|Param, // If greater than 1, delay will grow exponentially for each retry: delay * (multiple ^ retries). // Default: 2 @@ -523,57 +523,57 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * }, * mock_response_factory?: scalar|Param|null, // The id of the service that should generate mock responses. It should be either an invokable or an iterable. * scoped_clients?: array, - * headers?: array, - * max_redirects?: int|Param, // The maximum number of redirects to follow. - * http_version?: scalar|Param|null, // The default HTTP version, typically 1.1 or 2.0, leave to null for the best version. - * resolve?: array, - * proxy?: scalar|Param|null, // The URL of the proxy to pass requests through or null for automatic detection. - * no_proxy?: scalar|Param|null, // A comma separated list of hosts that do not require a proxy to be reached. - * timeout?: float|Param, // The idle timeout, defaults to the "default_socket_timeout" ini parameter. - * max_duration?: float|Param, // The maximum execution time for the request+response as a whole. - * bindto?: scalar|Param|null, // A network interface name, IP address, a host name or a UNIX socket to bind to. - * verify_peer?: bool|Param, // Indicates if the peer should be verified in a TLS context. - * verify_host?: bool|Param, // Indicates if the host should exist as a certificate common name. - * cafile?: scalar|Param|null, // A certificate authority file. - * capath?: scalar|Param|null, // A directory that contains multiple certificate authority files. - * local_cert?: scalar|Param|null, // A PEM formatted certificate file. - * local_pk?: scalar|Param|null, // A private key file. - * passphrase?: scalar|Param|null, // The passphrase used to encrypt the "local_pk" file. - * ciphers?: scalar|Param|null, // A list of TLS ciphers separated by colons, commas or spaces (e.g. "RC3-SHA:TLS13-AES-128-GCM-SHA256"...). - * peer_fingerprint?: array{ // Associative array: hashing algorithm => hash(es). - * sha1?: mixed, - * pin-sha256?: mixed, - * md5?: mixed, - * }, - * crypto_method?: scalar|Param|null, // The minimum version of TLS to accept; must be one of STREAM_CRYPTO_METHOD_TLSv*_CLIENT constants. - * extra?: array, - * rate_limiter?: scalar|Param|null, // Rate limiter name to use for throttling requests. // Default: null - * caching?: bool|array{ // Caching configuration. - * enabled?: bool|Param, // Default: false - * cache_pool?: string|Param, // The taggable cache pool to use for storing the responses. // Default: "cache.http_client" - * shared?: bool|Param, // Indicates whether the cache is shared (public) or private. // Default: true - * max_ttl?: int|Param, // The maximum TTL (in seconds) allowed for cached responses. Null means no cap. // Default: null - * }, - * retry_failed?: bool|array{ - * enabled?: bool|Param, // Default: false - * retry_strategy?: scalar|Param|null, // service id to override the retry strategy. // Default: null - * http_codes?: array, - * }>, - * max_retries?: int|Param, // Default: 3 - * delay?: int|Param, // Time in ms to delay (or the initial value when multiplier is used). // Default: 1000 - * multiplier?: float|Param, // If greater than 1, delay will grow exponentially for each retry: delay * (multiple ^ retries). // Default: 2 - * max_delay?: int|Param, // Max time in ms that a retry should ever be delayed (0 = infinite). // Default: 0 - * jitter?: float|Param, // Randomness in percent (between 0 and 1) to apply to the delay. // Default: 0.1 - * }, - * }>, + * scope?: scalar|Param|null, // The regular expression that the request URL must match before adding the other options. When none is provided, the base URI is used instead. + * base_uri?: scalar|Param|null, // The URI to resolve relative URLs, following rules in RFC 3985, section 2. + * auth_basic?: scalar|Param|null, // An HTTP Basic authentication "username:password". + * auth_bearer?: scalar|Param|null, // A token enabling HTTP Bearer authorization. + * auth_ntlm?: scalar|Param|null, // A "username:password" pair to use Microsoft NTLM authentication (requires the cURL extension). + * query?: array, + * headers?: array, + * max_redirects?: int|Param, // The maximum number of redirects to follow. + * http_version?: scalar|Param|null, // The default HTTP version, typically 1.1 or 2.0, leave to null for the best version. + * resolve?: array, + * proxy?: scalar|Param|null, // The URL of the proxy to pass requests through or null for automatic detection. + * no_proxy?: scalar|Param|null, // A comma separated list of hosts that do not require a proxy to be reached. + * timeout?: float|Param, // The idle timeout, defaults to the "default_socket_timeout" ini parameter. + * max_duration?: float|Param, // The maximum execution time for the request+response as a whole. + * bindto?: scalar|Param|null, // A network interface name, IP address, a host name or a UNIX socket to bind to. + * verify_peer?: bool|Param, // Indicates if the peer should be verified in a TLS context. + * verify_host?: bool|Param, // Indicates if the host should exist as a certificate common name. + * cafile?: scalar|Param|null, // A certificate authority file. + * capath?: scalar|Param|null, // A directory that contains multiple certificate authority files. + * local_cert?: scalar|Param|null, // A PEM formatted certificate file. + * local_pk?: scalar|Param|null, // A private key file. + * passphrase?: scalar|Param|null, // The passphrase used to encrypt the "local_pk" file. + * ciphers?: scalar|Param|null, // A list of TLS ciphers separated by colons, commas or spaces (e.g. "RC3-SHA:TLS13-AES-128-GCM-SHA256"...). + * peer_fingerprint?: array{ // Associative array: hashing algorithm => hash(es). + * sha1?: mixed, + * pin-sha256?: mixed, + * md5?: mixed, + * }, + * crypto_method?: scalar|Param|null, // The minimum version of TLS to accept; must be one of STREAM_CRYPTO_METHOD_TLSv*_CLIENT constants. + * extra?: array, + * rate_limiter?: scalar|Param|null, // Rate limiter name to use for throttling requests. // Default: null + * caching?: bool|array{ // Caching configuration. + * enabled?: bool|Param, // Default: false + * cache_pool?: string|Param, // The taggable cache pool to use for storing the responses. // Default: "cache.http_client" + * shared?: bool|Param, // Indicates whether the cache is shared (public) or private. // Default: true + * max_ttl?: int|Param, // The maximum TTL (in seconds) allowed for cached responses. Null means no cap. // Default: null + * }, + * retry_failed?: bool|array{ + * enabled?: bool|Param, // Default: false + * retry_strategy?: scalar|Param|null, // service id to override the retry strategy. // Default: null + * http_codes?: int|string|array, + * }>, + * max_retries?: int|Param, // Default: 3 + * delay?: int|Param, // Time in ms to delay (or the initial value when multiplier is used). // Default: 1000 + * multiplier?: float|Param, // If greater than 1, delay will grow exponentially for each retry: delay * (multiple ^ retries). // Default: 2 + * max_delay?: int|Param, // Max time in ms that a retry should ever be delayed (0 = infinite). // Default: 0 + * jitter?: float|Param, // Randomness in percent (between 0 and 1) to apply to the delay. // Default: 0.1 + * }, + * }>, * }, * mailer?: bool|array{ // Mailer configuration * enabled?: bool|Param, // Default: true @@ -582,12 +582,12 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * transports?: array, * envelope?: array{ // Mailer Envelope configuration * sender?: scalar|Param|null, - * recipients?: list, - * allowed_recipients?: list, + * recipients?: string|list, + * allowed_recipients?: string|list, * }, * headers?: array, + * value?: mixed, + * }>, * dkim_signer?: bool|array{ // DKIM signer configuration * enabled?: bool|Param, // Default: false * key?: scalar|Param|null, // Key content, or path to key (in PEM format with the `file://` prefix) // Default: "" @@ -624,25 +624,25 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * notification_on_failed_messages?: bool|Param, // Default: false * channel_policy?: array>, * admin_recipients?: list, + * email?: scalar|Param|null, + * phone?: scalar|Param|null, // Default: "" + * }>, * }, * rate_limiter?: bool|array{ // Rate limiter configuration * enabled?: bool|Param, // Default: true * limiters?: array, - * limit?: int|Param, // The maximum allowed hits in a fixed interval or burst. - * interval?: scalar|Param|null, // Configures the fixed interval if "policy" is set to "fixed_window" or "sliding_window". The value must be a number followed by "second", "minute", "hour", "day", "week" or "month" (or their plural equivalent). - * rate?: array{ // Configures the fill rate if "policy" is set to "token_bucket". - * interval?: scalar|Param|null, // Configures the rate interval. The value must be a number followed by "second", "minute", "hour", "day", "week" or "month" (or their plural equivalent). - * amount?: int|Param, // Amount of tokens to add each interval. // Default: 1 - * }, - * }>, + * lock_factory?: scalar|Param|null, // The service ID of the lock factory used by this limiter (or null to disable locking). // Default: "auto" + * cache_pool?: scalar|Param|null, // The cache pool to use for storing the current limiter state. // Default: "cache.rate_limiter" + * storage_service?: scalar|Param|null, // The service ID of a custom storage implementation, this precedes any configured "cache_pool". // Default: null + * policy?: "fixed_window"|"token_bucket"|"sliding_window"|"compound"|"no_limit"|Param, // The algorithm to be used by this limiter. + * limiters?: string|list, + * limit?: int|Param, // The maximum allowed hits in a fixed interval or burst. + * interval?: scalar|Param|null, // Configures the fixed interval if "policy" is set to "fixed_window" or "sliding_window". The value must be a number followed by "second", "minute", "hour", "day", "week" or "month" (or their plural equivalent). + * rate?: array{ // Configures the fill rate if "policy" is set to "token_bucket". + * interval?: scalar|Param|null, // Configures the rate interval. The value must be a number followed by "second", "minute", "hour", "day", "week" or "month" (or their plural equivalent). + * amount?: int|Param, // Amount of tokens to add each interval. // Default: 1 + * }, + * }>, * }, * uid?: bool|array{ // Uid configuration * enabled?: bool|Param, // Default: true @@ -655,33 +655,33 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * html_sanitizer?: bool|array{ // HtmlSanitizer configuration * enabled?: bool|Param, // Default: false * sanitizers?: array, - * block_elements?: list, - * drop_elements?: list, - * allow_attributes?: array, - * drop_attributes?: array, - * force_attributes?: array>, - * force_https_urls?: bool|Param, // Transforms URLs using the HTTP scheme to use the HTTPS scheme instead. // Default: false - * allowed_link_schemes?: list, - * allowed_link_hosts?: list|null, - * allow_relative_links?: bool|Param, // Allows relative URLs to be used in links href attributes. // Default: false - * allowed_media_schemes?: list, - * allowed_media_hosts?: list|null, - * allow_relative_medias?: bool|Param, // Allows relative URLs to be used in media source attributes (img, audio, video, ...). // Default: false - * with_attribute_sanitizers?: list, - * without_attribute_sanitizers?: list, - * max_input_length?: int|Param, // The maximum length allowed for the sanitized input. // Default: 0 - * }>, + * allow_safe_elements?: bool|Param, // Allows "safe" elements and attributes. // Default: false + * allow_static_elements?: bool|Param, // Allows all static elements and attributes from the W3C Sanitizer API standard. // Default: false + * allow_elements?: array, + * block_elements?: string|list, + * drop_elements?: string|list, + * allow_attributes?: array, + * drop_attributes?: array, + * force_attributes?: array>, + * force_https_urls?: bool|Param, // Transforms URLs using the HTTP scheme to use the HTTPS scheme instead. // Default: false + * allowed_link_schemes?: string|list, + * allowed_link_hosts?: null|string|list, + * allow_relative_links?: bool|Param, // Allows relative URLs to be used in links href attributes. // Default: false + * allowed_media_schemes?: string|list, + * allowed_media_hosts?: null|string|list, + * allow_relative_medias?: bool|Param, // Allows relative URLs to be used in media source attributes (img, audio, video, ...). // Default: false + * with_attribute_sanitizers?: string|list, + * without_attribute_sanitizers?: string|list, + * max_input_length?: int|Param, // The maximum length allowed for the sanitized input. // Default: 0 + * }>, * }, * webhook?: bool|array{ // Webhook configuration * enabled?: bool|Param, // Default: false * message_bus?: scalar|Param|null, // The message bus to use. // Default: "messenger.default_bus" * routing?: array, + * service?: scalar|Param|null, + * secret?: scalar|Param|null, // Default: "" + * }>, * }, * remote-event?: bool|array{ // RemoteEvent configuration * enabled?: bool|Param, // Default: false @@ -694,62 +694,11 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * dbal?: array{ * default_connection?: scalar|Param|null, * types?: array, + * class?: scalar|Param|null, + * commented?: bool|Param, // Deprecated: The doctrine-bundle type commenting features were removed; the corresponding config parameter was deprecated in 2.0 and will be dropped in 3.0. + * }>, * driver_schemes?: array, * connections?: array, - * mapping_types?: array, - * default_table_options?: array, - * schema_manager_factory?: scalar|Param|null, // Default: "doctrine.dbal.default_schema_manager_factory" - * result_cache?: scalar|Param|null, - * slaves?: array, + * mapping_types?: array, + * default_table_options?: array, + * schema_manager_factory?: scalar|Param|null, // Default: "doctrine.dbal.default_schema_manager_factory" + * result_cache?: scalar|Param|null, + * slaves?: array, + * replicas?: array, * }>, - * replicas?: array, - * }>, * }, * orm?: array{ * default_entity_manager?: scalar|Param|null, @@ -828,94 +828,94 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * 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|Param|null, - * class_metadata_factory_name?: scalar|Param|null, // Default: "Doctrine\\ORM\\Mapping\\ClassMetadataFactory" - * default_repository_class?: scalar|Param|null, // Default: "Doctrine\\ORM\\EntityRepository" - * auto_mapping?: scalar|Param|null, // Default: false - * naming_strategy?: scalar|Param|null, // Default: "doctrine.orm.naming_strategy.default" - * quote_strategy?: scalar|Param|null, // Default: "doctrine.orm.quote_strategy.default" - * typed_field_mapper?: scalar|Param|null, // Default: "doctrine.orm.typed_field_mapper.default" - * entity_listener_resolver?: scalar|Param|null, // Default: null - * fetch_mode_subselect_batch_size?: scalar|Param|null, - * repository_factory?: scalar|Param|null, // 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{ + * query_cache_driver?: string|array{ * type?: scalar|Param|null, // Default: null * id?: scalar|Param|null, * pool?: scalar|Param|null, * }, - * region_lock_lifetime?: scalar|Param|null, // Default: 60 - * log_enabled?: bool|Param, // Default: true - * region_lifetime?: scalar|Param|null, // Default: 3600 - * enabled?: bool|Param, // Default: true - * factory?: scalar|Param|null, - * regions?: array, + * }>, + * }>, + * }, + * connection?: scalar|Param|null, + * class_metadata_factory_name?: scalar|Param|null, // Default: "Doctrine\\ORM\\Mapping\\ClassMetadataFactory" + * default_repository_class?: scalar|Param|null, // Default: "Doctrine\\ORM\\EntityRepository" + * auto_mapping?: scalar|Param|null, // Default: false + * naming_strategy?: scalar|Param|null, // Default: "doctrine.orm.naming_strategy.default" + * quote_strategy?: scalar|Param|null, // Default: "doctrine.orm.quote_strategy.default" + * typed_field_mapper?: scalar|Param|null, // Default: "doctrine.orm.typed_field_mapper.default" + * entity_listener_resolver?: scalar|Param|null, // Default: null + * fetch_mode_subselect_batch_size?: scalar|Param|null, + * repository_factory?: scalar|Param|null, // 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|Param|null, // Default: null * id?: scalar|Param|null, * pool?: scalar|Param|null, * }, - * lock_path?: scalar|Param|null, // Default: "%kernel.cache_dir%/doctrine/orm/slc/filelock" - * lock_lifetime?: scalar|Param|null, // Default: 60 - * type?: scalar|Param|null, // Default: "default" - * lifetime?: scalar|Param|null, // Default: 0 - * service?: scalar|Param|null, - * name?: scalar|Param|null, - * }>, - * loggers?: array, - * }, - * hydrators?: array, - * mappings?: array, + * loggers?: array, + * }, + * hydrators?: array, + * mappings?: array, + * dql?: array{ + * string_functions?: array, + * numeric_functions?: array, + * datetime_functions?: array, + * }, + * filters?: array, + * }>, + * identity_generation_preferences?: array, * }>, - * dql?: array{ - * string_functions?: array, - * numeric_functions?: array, - * datetime_functions?: array, - * }, - * filters?: array, - * }>, - * identity_generation_preferences?: array, - * }>, * resolve_target_entities?: array, * }, * } @@ -957,390 +957,391 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * allow_if_equal_granted_denied?: bool|Param, // Default: true * }, * password_hashers?: array, - * hash_algorithm?: scalar|Param|null, // 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|Param|null, // Default: 40 - * ignore_case?: bool|Param, // Default: false - * encode_as_base64?: bool|Param, // Default: true - * iterations?: scalar|Param|null, // Default: 5000 - * cost?: int|Param, // Default: null - * memory_cost?: scalar|Param|null, // Default: null - * time_cost?: scalar|Param|null, // Default: null - * id?: scalar|Param|null, - * }>, + * algorithm?: scalar|Param|null, + * migrate_from?: string|list, + * hash_algorithm?: scalar|Param|null, // 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|Param|null, // Default: 40 + * ignore_case?: bool|Param, // Default: false + * encode_as_base64?: bool|Param, // Default: true + * iterations?: scalar|Param|null, // Default: 5000 + * cost?: int|Param, // Default: null + * memory_cost?: scalar|Param|null, // Default: null + * time_cost?: scalar|Param|null, // Default: null + * id?: scalar|Param|null, + * }>, * providers?: array, - * }, - * entity?: array{ - * class?: scalar|Param|null, // The full entity class name of your user class. - * property?: scalar|Param|null, // Default: null - * manager_name?: scalar|Param|null, // Default: null - * }, - * memory?: array{ - * users?: array, - * }>, - * }, - * ldap?: array{ - * service?: scalar|Param|null, - * base_dn?: scalar|Param|null, - * search_dn?: scalar|Param|null, // Default: null - * search_password?: scalar|Param|null, // Default: null - * extra_fields?: list, - * default_roles?: list, - * role_fetcher?: scalar|Param|null, // Default: null - * uid_key?: scalar|Param|null, // Default: "sAMAccountName" - * filter?: scalar|Param|null, // Default: "({uid_key}={user_identifier})" - * password_attribute?: scalar|Param|null, // Default: null - * }, - * saml?: array{ - * user_class?: scalar|Param|null, - * default_roles?: list, - * }, - * }>, + * id?: scalar|Param|null, + * chain?: array{ + * providers?: string|list, + * }, + * entity?: array{ + * class?: scalar|Param|null, // The full entity class name of your user class. + * property?: scalar|Param|null, // Default: null + * manager_name?: scalar|Param|null, // Default: null + * }, + * memory?: array{ + * users?: array, + * }>, + * }, + * ldap?: array{ + * service?: scalar|Param|null, + * base_dn?: scalar|Param|null, + * search_dn?: scalar|Param|null, // Default: null + * search_password?: scalar|Param|null, // Default: null + * extra_fields?: list, + * default_roles?: string|list, + * role_fetcher?: scalar|Param|null, // Default: null + * uid_key?: scalar|Param|null, // Default: "sAMAccountName" + * filter?: scalar|Param|null, // Default: "({uid_key}={user_identifier})" + * password_attribute?: scalar|Param|null, // Default: null + * }, + * saml?: array{ + * user_class?: scalar|Param|null, + * default_roles?: list, + * }, + * }>, * firewalls?: array, - * security?: bool|Param, // Default: true - * user_checker?: scalar|Param|null, // The UserChecker to use when authenticating users in this firewall. // Default: "security.user_checker" - * request_matcher?: scalar|Param|null, - * access_denied_url?: scalar|Param|null, - * access_denied_handler?: scalar|Param|null, - * entry_point?: scalar|Param|null, // An enabled authenticator name or a service id that implements "Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface". - * provider?: scalar|Param|null, - * stateless?: bool|Param, // Default: false - * lazy?: bool|Param, // Default: false - * context?: scalar|Param|null, - * logout?: array{ - * enable_csrf?: bool|Param|null, // Default: null - * csrf_token_id?: scalar|Param|null, // Default: "logout" - * csrf_parameter?: scalar|Param|null, // Default: "_csrf_token" - * csrf_token_manager?: scalar|Param|null, - * path?: scalar|Param|null, // Default: "/logout" - * target?: scalar|Param|null, // Default: "/" - * invalidate_session?: bool|Param, // Default: true - * clear_site_data?: list<"*"|"cache"|"cookies"|"storage"|"executionContexts"|Param>, - * delete_cookies?: array, - * }, - * switch_user?: array{ + * pattern?: scalar|Param|null, + * host?: scalar|Param|null, + * methods?: string|list, + * security?: bool|Param, // Default: true + * user_checker?: scalar|Param|null, // The UserChecker to use when authenticating users in this firewall. // Default: "security.user_checker" + * request_matcher?: scalar|Param|null, + * access_denied_url?: scalar|Param|null, + * access_denied_handler?: scalar|Param|null, + * entry_point?: scalar|Param|null, // An enabled authenticator name or a service id that implements "Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface". * provider?: scalar|Param|null, - * parameter?: scalar|Param|null, // Default: "_switch_user" - * role?: scalar|Param|null, // Default: "ROLE_ALLOWED_TO_SWITCH" - * target_route?: scalar|Param|null, // Default: null - * }, - * required_badges?: list, - * custom_authenticators?: list, - * login_throttling?: array{ - * limiter?: scalar|Param|null, // A service id implementing "Symfony\Component\HttpFoundation\RateLimiter\RequestRateLimiterInterface". - * max_attempts?: int|Param, // Default: 5 - * interval?: scalar|Param|null, // Default: "1 minute" - * lock_factory?: scalar|Param|null, // 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 - * }, - * two_factor?: array{ - * check_path?: scalar|Param|null, // Default: "/2fa_check" - * post_only?: bool|Param, // Default: true - * auth_form_path?: scalar|Param|null, // Default: "/2fa" - * always_use_default_target_path?: bool|Param, // Default: false - * default_target_path?: scalar|Param|null, // Default: "/" - * success_handler?: scalar|Param|null, // Default: null - * failure_handler?: scalar|Param|null, // Default: null - * authentication_required_handler?: scalar|Param|null, // Default: null - * auth_code_parameter_name?: scalar|Param|null, // Default: "_auth_code" - * trusted_parameter_name?: scalar|Param|null, // Default: "_trusted" - * remember_me_sets_trusted?: scalar|Param|null, // Default: false - * multi_factor?: bool|Param, // Default: false - * prepare_on_login?: bool|Param, // Default: false - * prepare_on_access_denied?: bool|Param, // Default: false - * enable_csrf?: scalar|Param|null, // Default: false - * csrf_parameter?: scalar|Param|null, // Default: "_csrf_token" - * csrf_token_id?: scalar|Param|null, // Default: "two_factor" - * csrf_header?: scalar|Param|null, // Default: null - * csrf_token_manager?: scalar|Param|null, // Default: "scheb_two_factor.csrf_token_manager" - * provider?: scalar|Param|null, // Default: null - * }, - * webauthn?: array{ - * user_provider?: scalar|Param|null, // Default: null - * options_storage?: scalar|Param|null, // Deprecated: The child node "options_storage" at path "security.firewalls..webauthn.options_storage" is deprecated. Please use the root option "options_storage" instead. // Default: null - * success_handler?: scalar|Param|null, // Default: "Webauthn\\Bundle\\Security\\Handler\\DefaultSuccessHandler" - * failure_handler?: scalar|Param|null, // Default: "Webauthn\\Bundle\\Security\\Handler\\DefaultFailureHandler" - * secured_rp_ids?: array, - * authentication?: bool|array{ - * enabled?: bool|Param, // Default: true - * profile?: scalar|Param|null, // Default: "default" - * options_builder?: scalar|Param|null, // Default: null - * routes?: array{ - * host?: scalar|Param|null, // Default: null - * options_method?: scalar|Param|null, // Default: "POST" - * options_path?: scalar|Param|null, // Default: "/login/options" - * result_method?: scalar|Param|null, // Default: "POST" - * result_path?: scalar|Param|null, // Default: "/login" - * }, - * options_handler?: scalar|Param|null, // Default: "Webauthn\\Bundle\\Security\\Handler\\DefaultRequestOptionsHandler" + * stateless?: bool|Param, // Default: false + * lazy?: bool|Param, // Default: false + * context?: scalar|Param|null, + * logout?: array{ + * enable_csrf?: bool|Param|null, // Default: null + * csrf_token_id?: scalar|Param|null, // Default: "logout" + * csrf_parameter?: scalar|Param|null, // Default: "_csrf_token" + * csrf_token_manager?: scalar|Param|null, + * path?: scalar|Param|null, // Default: "/logout" + * target?: scalar|Param|null, // Default: "/" + * invalidate_session?: bool|Param, // Default: true + * clear_site_data?: string|list<"*"|"cache"|"cookies"|"storage"|"executionContexts"|Param>, + * delete_cookies?: string|array, * }, - * registration?: bool|array{ - * enabled?: bool|Param, // Default: false - * profile?: scalar|Param|null, // Default: "default" - * options_builder?: scalar|Param|null, // Default: null - * routes?: array{ - * host?: scalar|Param|null, // Default: null - * options_method?: scalar|Param|null, // Default: "POST" - * options_path?: scalar|Param|null, // Default: "/register/options" - * result_method?: scalar|Param|null, // Default: "POST" - * result_path?: scalar|Param|null, // Default: "/register" - * }, - * options_handler?: scalar|Param|null, // Default: "Webauthn\\Bundle\\Security\\Handler\\DefaultCreationOptionsHandler" + * switch_user?: array{ + * provider?: scalar|Param|null, + * parameter?: scalar|Param|null, // Default: "_switch_user" + * role?: scalar|Param|null, // Default: "ROLE_ALLOWED_TO_SWITCH" + * target_route?: scalar|Param|null, // Default: null * }, - * }, - * x509?: array{ - * provider?: scalar|Param|null, - * user?: scalar|Param|null, // Default: "SSL_CLIENT_S_DN_Email" - * credentials?: scalar|Param|null, // Default: "SSL_CLIENT_S_DN" - * user_identifier?: scalar|Param|null, // Default: "emailAddress" - * }, - * remote_user?: array{ - * provider?: scalar|Param|null, - * user?: scalar|Param|null, // Default: "REMOTE_USER" - * }, - * saml?: array{ - * provider?: scalar|Param|null, - * remember_me?: bool|Param, // Default: true - * success_handler?: scalar|Param|null, // Default: "Nbgrp\\OneloginSamlBundle\\Security\\Http\\Authentication\\SamlAuthenticationSuccessHandler" - * failure_handler?: scalar|Param|null, - * check_path?: scalar|Param|null, // Default: "/login_check" - * use_forward?: bool|Param, // Default: false - * login_path?: scalar|Param|null, // Default: "/login" - * identifier_attribute?: scalar|Param|null, // Default: null - * use_attribute_friendly_name?: bool|Param, // Default: false - * user_factory?: scalar|Param|null, // Default: null - * token_factory?: scalar|Param|null, // Default: null - * persist_user?: bool|Param, // Default: false - * always_use_default_target_path?: bool|Param, // Default: false - * default_target_path?: scalar|Param|null, // Default: "/" - * target_path_parameter?: scalar|Param|null, // Default: "_target_path" - * use_referer?: bool|Param, // Default: false - * failure_path?: scalar|Param|null, // Default: null - * failure_forward?: bool|Param, // Default: false - * failure_path_parameter?: scalar|Param|null, // Default: "_failure_path" - * }, - * login_link?: array{ - * check_route?: scalar|Param|null, // Route that will validate the login link - e.g. "app_login_link_verify". - * check_post_only?: scalar|Param|null, // 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|Param|null, // Cache service id used to expired links of max_uses is set. - * success_handler?: scalar|Param|null, // A service id that implements Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface. - * failure_handler?: scalar|Param|null, // A service id that implements Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface. - * provider?: scalar|Param|null, // The user provider to load users from. - * secret?: scalar|Param|null, // Default: "%kernel.secret%" - * always_use_default_target_path?: bool|Param, // Default: false - * default_target_path?: scalar|Param|null, // Default: "/" - * login_path?: scalar|Param|null, // Default: "/login" - * target_path_parameter?: scalar|Param|null, // Default: "_target_path" - * use_referer?: bool|Param, // Default: false - * failure_path?: scalar|Param|null, // Default: null - * failure_forward?: bool|Param, // Default: false - * failure_path_parameter?: scalar|Param|null, // Default: "_failure_path" - * }, - * form_login?: array{ - * provider?: scalar|Param|null, - * remember_me?: bool|Param, // Default: true - * success_handler?: scalar|Param|null, - * failure_handler?: scalar|Param|null, - * check_path?: scalar|Param|null, // Default: "/login_check" - * use_forward?: bool|Param, // Default: false - * login_path?: scalar|Param|null, // Default: "/login" - * username_parameter?: scalar|Param|null, // Default: "_username" - * password_parameter?: scalar|Param|null, // Default: "_password" - * csrf_parameter?: scalar|Param|null, // Default: "_csrf_token" - * csrf_token_id?: scalar|Param|null, // 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|Param|null, // Default: "/" - * target_path_parameter?: scalar|Param|null, // Default: "_target_path" - * use_referer?: bool|Param, // Default: false - * failure_path?: scalar|Param|null, // Default: null - * failure_forward?: bool|Param, // Default: false - * failure_path_parameter?: scalar|Param|null, // Default: "_failure_path" - * }, - * form_login_ldap?: array{ - * provider?: scalar|Param|null, - * remember_me?: bool|Param, // Default: true - * success_handler?: scalar|Param|null, - * failure_handler?: scalar|Param|null, - * check_path?: scalar|Param|null, // Default: "/login_check" - * use_forward?: bool|Param, // Default: false - * login_path?: scalar|Param|null, // Default: "/login" - * username_parameter?: scalar|Param|null, // Default: "_username" - * password_parameter?: scalar|Param|null, // Default: "_password" - * csrf_parameter?: scalar|Param|null, // Default: "_csrf_token" - * csrf_token_id?: scalar|Param|null, // 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|Param|null, // Default: "/" - * target_path_parameter?: scalar|Param|null, // Default: "_target_path" - * use_referer?: bool|Param, // Default: false - * failure_path?: scalar|Param|null, // Default: null - * failure_forward?: bool|Param, // Default: false - * failure_path_parameter?: scalar|Param|null, // Default: "_failure_path" - * service?: scalar|Param|null, // Default: "ldap" - * dn_string?: scalar|Param|null, // Default: "{user_identifier}" - * query_string?: scalar|Param|null, - * search_dn?: scalar|Param|null, // Default: "" - * search_password?: scalar|Param|null, // Default: "" - * }, - * json_login?: array{ - * provider?: scalar|Param|null, - * remember_me?: bool|Param, // Default: true - * success_handler?: scalar|Param|null, - * failure_handler?: scalar|Param|null, - * check_path?: scalar|Param|null, // Default: "/login_check" - * use_forward?: bool|Param, // Default: false - * login_path?: scalar|Param|null, // Default: "/login" - * username_path?: scalar|Param|null, // Default: "username" - * password_path?: scalar|Param|null, // Default: "password" - * }, - * json_login_ldap?: array{ - * provider?: scalar|Param|null, - * remember_me?: bool|Param, // Default: true - * success_handler?: scalar|Param|null, - * failure_handler?: scalar|Param|null, - * check_path?: scalar|Param|null, // Default: "/login_check" - * use_forward?: bool|Param, // Default: false - * login_path?: scalar|Param|null, // Default: "/login" - * username_path?: scalar|Param|null, // Default: "username" - * password_path?: scalar|Param|null, // Default: "password" - * service?: scalar|Param|null, // Default: "ldap" - * dn_string?: scalar|Param|null, // Default: "{user_identifier}" - * query_string?: scalar|Param|null, - * search_dn?: scalar|Param|null, // Default: "" - * search_password?: scalar|Param|null, // Default: "" - * }, - * access_token?: array{ - * provider?: scalar|Param|null, - * remember_me?: bool|Param, // Default: true - * success_handler?: scalar|Param|null, - * failure_handler?: scalar|Param|null, - * realm?: scalar|Param|null, // Default: null - * token_extractors?: list, - * token_handler?: string|array{ - * id?: scalar|Param|null, - * oidc_user_info?: string|array{ - * base_uri?: scalar|Param|null, // 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|Param|null, // Cache service id to use to cache the OIDC discovery configuration. - * }, - * }, - * claim?: scalar|Param|null, // Claim which contains the user identifier (e.g. sub, email, etc.). // Default: "sub" - * client?: scalar|Param|null, // 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|Param|null, // Cache service id to use to cache the OIDC discovery configuration. - * }, - * }, - * claim?: scalar|Param|null, // Claim which contains the user identifier (e.g.: sub, email..). // Default: "sub" - * audience?: scalar|Param|null, // Audience set in the token, for validation purpose. - * issuers?: list, - * algorithm?: array, - * algorithms?: list, - * key?: scalar|Param|null, // 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|Param|null, // 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|Param|null, // JSON-encoded JWKSet used to decrypt the token (must contain a list of valid private keys). - * }, - * }, - * cas?: array{ - * validation_url?: scalar|Param|null, // CAS server validation URL - * prefix?: scalar|Param|null, // CAS prefix // Default: "cas" - * http_client?: scalar|Param|null, // HTTP Client service // Default: null - * }, - * oauth2?: scalar|Param|null, + * required_badges?: list, + * custom_authenticators?: list, + * login_throttling?: array{ + * limiter?: scalar|Param|null, // A service id implementing "Symfony\Component\HttpFoundation\RateLimiter\RequestRateLimiterInterface". + * max_attempts?: int|Param, // Default: 5 + * interval?: scalar|Param|null, // Default: "1 minute" + * lock_factory?: scalar|Param|null, // 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 * }, - * }, - * http_basic?: array{ - * provider?: scalar|Param|null, - * realm?: scalar|Param|null, // Default: "Secured Area" - * }, - * http_basic_ldap?: array{ - * provider?: scalar|Param|null, - * realm?: scalar|Param|null, // Default: "Secured Area" - * service?: scalar|Param|null, // Default: "ldap" - * dn_string?: scalar|Param|null, // Default: "{user_identifier}" - * query_string?: scalar|Param|null, - * search_dn?: scalar|Param|null, // Default: "" - * search_password?: scalar|Param|null, // Default: "" - * }, - * remember_me?: array{ - * secret?: scalar|Param|null, // Default: "%kernel.secret%" - * service?: scalar|Param|null, - * user_providers?: list, - * catch_exceptions?: bool|Param, // Default: true - * signature_properties?: list, - * token_provider?: string|array{ - * service?: scalar|Param|null, // The service ID of a custom remember-me token provider. - * doctrine?: bool|array{ + * two_factor?: array{ + * check_path?: scalar|Param|null, // Default: "/2fa_check" + * post_only?: bool|Param, // Default: true + * auth_form_path?: scalar|Param|null, // Default: "/2fa" + * always_use_default_target_path?: bool|Param, // Default: false + * default_target_path?: scalar|Param|null, // Default: "/" + * success_handler?: scalar|Param|null, // Default: null + * failure_handler?: scalar|Param|null, // Default: null + * authentication_required_handler?: scalar|Param|null, // Default: null + * auth_code_parameter_name?: scalar|Param|null, // Default: "_auth_code" + * trusted_parameter_name?: scalar|Param|null, // Default: "_trusted" + * remember_me_sets_trusted?: scalar|Param|null, // Default: false + * multi_factor?: bool|Param, // Default: false + * prepare_on_login?: bool|Param, // Default: false + * prepare_on_access_denied?: bool|Param, // Default: false + * enable_csrf?: scalar|Param|null, // Default: false + * csrf_parameter?: scalar|Param|null, // Default: "_csrf_token" + * csrf_token_id?: scalar|Param|null, // Default: "two_factor" + * csrf_header?: scalar|Param|null, // Default: null + * csrf_token_manager?: scalar|Param|null, // Default: "scheb_two_factor.csrf_token_manager" + * provider?: scalar|Param|null, // Default: null + * }, + * webauthn?: array{ + * user_provider?: scalar|Param|null, // Default: null + * options_storage?: scalar|Param|null, // Deprecated: The child node "options_storage" at path "security.firewalls..webauthn.options_storage" is deprecated. Please use the root option "options_storage" instead. // Default: null + * success_handler?: scalar|Param|null, // Default: "Webauthn\\Bundle\\Security\\Handler\\DefaultSuccessHandler" + * failure_handler?: scalar|Param|null, // Default: "Webauthn\\Bundle\\Security\\Handler\\DefaultFailureHandler" + * secured_rp_ids?: array, + * authentication?: bool|array{ + * enabled?: bool|Param, // Default: true + * profile?: scalar|Param|null, // Default: "default" + * options_builder?: scalar|Param|null, // Default: null + * routes?: array{ + * host?: scalar|Param|null, // Default: null + * options_method?: scalar|Param|null, // Default: "POST" + * options_path?: scalar|Param|null, // Default: "/login/options" + * result_method?: scalar|Param|null, // Default: "POST" + * result_path?: scalar|Param|null, // Default: "/login" + * }, + * options_handler?: scalar|Param|null, // Default: "Webauthn\\Bundle\\Security\\Handler\\DefaultRequestOptionsHandler" + * }, + * registration?: bool|array{ * enabled?: bool|Param, // Default: false - * connection?: scalar|Param|null, // Default: null + * hide_existing_credentials?: bool|Param, // Default: true + * profile?: scalar|Param|null, // Default: "default" + * options_builder?: scalar|Param|null, // Default: null + * routes?: array{ + * host?: scalar|Param|null, // Default: null + * options_method?: scalar|Param|null, // Default: "POST" + * options_path?: scalar|Param|null, // Default: "/register/options" + * result_method?: scalar|Param|null, // Default: "POST" + * result_path?: scalar|Param|null, // Default: "/register" + * }, + * options_handler?: scalar|Param|null, // Default: "Webauthn\\Bundle\\Security\\Handler\\DefaultCreationOptionsHandler" * }, * }, - * token_verifier?: scalar|Param|null, // The service ID of a custom rememberme token verifier. - * name?: scalar|Param|null, // Default: "REMEMBERME" - * lifetime?: int|Param, // Default: 31536000 - * path?: scalar|Param|null, // Default: "/" - * domain?: scalar|Param|null, // 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|Param|null, // Default: "_remember_me" - * }, - * }>, + * x509?: array{ + * provider?: scalar|Param|null, + * user?: scalar|Param|null, // Default: "SSL_CLIENT_S_DN_Email" + * credentials?: scalar|Param|null, // Default: "SSL_CLIENT_S_DN" + * user_identifier?: scalar|Param|null, // Default: "emailAddress" + * }, + * remote_user?: array{ + * provider?: scalar|Param|null, + * user?: scalar|Param|null, // Default: "REMOTE_USER" + * }, + * saml?: array{ + * provider?: scalar|Param|null, + * remember_me?: bool|Param, // Default: true + * success_handler?: scalar|Param|null, // Default: "Nbgrp\\OneloginSamlBundle\\Security\\Http\\Authentication\\SamlAuthenticationSuccessHandler" + * failure_handler?: scalar|Param|null, + * check_path?: scalar|Param|null, // Default: "/login_check" + * use_forward?: bool|Param, // Default: false + * login_path?: scalar|Param|null, // Default: "/login" + * identifier_attribute?: scalar|Param|null, // Default: null + * use_attribute_friendly_name?: bool|Param, // Default: false + * user_factory?: scalar|Param|null, // Default: null + * token_factory?: scalar|Param|null, // Default: null + * persist_user?: bool|Param, // Default: false + * always_use_default_target_path?: bool|Param, // Default: false + * default_target_path?: scalar|Param|null, // Default: "/" + * target_path_parameter?: scalar|Param|null, // Default: "_target_path" + * use_referer?: bool|Param, // Default: false + * failure_path?: scalar|Param|null, // Default: null + * failure_forward?: bool|Param, // Default: false + * failure_path_parameter?: scalar|Param|null, // Default: "_failure_path" + * }, + * login_link?: array{ + * check_route?: scalar|Param|null, // Route that will validate the login link - e.g. "app_login_link_verify". + * check_post_only?: scalar|Param|null, // 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|Param|null, // Cache service id used to expired links of max_uses is set. + * success_handler?: scalar|Param|null, // A service id that implements Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface. + * failure_handler?: scalar|Param|null, // A service id that implements Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface. + * provider?: scalar|Param|null, // The user provider to load users from. + * secret?: scalar|Param|null, // Default: "%kernel.secret%" + * always_use_default_target_path?: bool|Param, // Default: false + * default_target_path?: scalar|Param|null, // Default: "/" + * login_path?: scalar|Param|null, // Default: "/login" + * target_path_parameter?: scalar|Param|null, // Default: "_target_path" + * use_referer?: bool|Param, // Default: false + * failure_path?: scalar|Param|null, // Default: null + * failure_forward?: bool|Param, // Default: false + * failure_path_parameter?: scalar|Param|null, // Default: "_failure_path" + * }, + * form_login?: array{ + * provider?: scalar|Param|null, + * remember_me?: bool|Param, // Default: true + * success_handler?: scalar|Param|null, + * failure_handler?: scalar|Param|null, + * check_path?: scalar|Param|null, // Default: "/login_check" + * use_forward?: bool|Param, // Default: false + * login_path?: scalar|Param|null, // Default: "/login" + * username_parameter?: scalar|Param|null, // Default: "_username" + * password_parameter?: scalar|Param|null, // Default: "_password" + * csrf_parameter?: scalar|Param|null, // Default: "_csrf_token" + * csrf_token_id?: scalar|Param|null, // 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|Param|null, // Default: "/" + * target_path_parameter?: scalar|Param|null, // Default: "_target_path" + * use_referer?: bool|Param, // Default: false + * failure_path?: scalar|Param|null, // Default: null + * failure_forward?: bool|Param, // Default: false + * failure_path_parameter?: scalar|Param|null, // Default: "_failure_path" + * }, + * form_login_ldap?: array{ + * provider?: scalar|Param|null, + * remember_me?: bool|Param, // Default: true + * success_handler?: scalar|Param|null, + * failure_handler?: scalar|Param|null, + * check_path?: scalar|Param|null, // Default: "/login_check" + * use_forward?: bool|Param, // Default: false + * login_path?: scalar|Param|null, // Default: "/login" + * username_parameter?: scalar|Param|null, // Default: "_username" + * password_parameter?: scalar|Param|null, // Default: "_password" + * csrf_parameter?: scalar|Param|null, // Default: "_csrf_token" + * csrf_token_id?: scalar|Param|null, // 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|Param|null, // Default: "/" + * target_path_parameter?: scalar|Param|null, // Default: "_target_path" + * use_referer?: bool|Param, // Default: false + * failure_path?: scalar|Param|null, // Default: null + * failure_forward?: bool|Param, // Default: false + * failure_path_parameter?: scalar|Param|null, // Default: "_failure_path" + * service?: scalar|Param|null, // Default: "ldap" + * dn_string?: scalar|Param|null, // Default: "{user_identifier}" + * query_string?: scalar|Param|null, + * search_dn?: scalar|Param|null, // Default: "" + * search_password?: scalar|Param|null, // Default: "" + * }, + * json_login?: array{ + * provider?: scalar|Param|null, + * remember_me?: bool|Param, // Default: true + * success_handler?: scalar|Param|null, + * failure_handler?: scalar|Param|null, + * check_path?: scalar|Param|null, // Default: "/login_check" + * use_forward?: bool|Param, // Default: false + * login_path?: scalar|Param|null, // Default: "/login" + * username_path?: scalar|Param|null, // Default: "username" + * password_path?: scalar|Param|null, // Default: "password" + * }, + * json_login_ldap?: array{ + * provider?: scalar|Param|null, + * remember_me?: bool|Param, // Default: true + * success_handler?: scalar|Param|null, + * failure_handler?: scalar|Param|null, + * check_path?: scalar|Param|null, // Default: "/login_check" + * use_forward?: bool|Param, // Default: false + * login_path?: scalar|Param|null, // Default: "/login" + * username_path?: scalar|Param|null, // Default: "username" + * password_path?: scalar|Param|null, // Default: "password" + * service?: scalar|Param|null, // Default: "ldap" + * dn_string?: scalar|Param|null, // Default: "{user_identifier}" + * query_string?: scalar|Param|null, + * search_dn?: scalar|Param|null, // Default: "" + * search_password?: scalar|Param|null, // Default: "" + * }, + * access_token?: array{ + * provider?: scalar|Param|null, + * remember_me?: bool|Param, // Default: true + * success_handler?: scalar|Param|null, + * failure_handler?: scalar|Param|null, + * realm?: scalar|Param|null, // Default: null + * token_extractors?: string|list, + * token_handler?: string|array{ + * id?: scalar|Param|null, + * oidc_user_info?: string|array{ + * base_uri?: scalar|Param|null, // 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|Param|null, // Cache service id to use to cache the OIDC discovery configuration. + * }, + * }, + * claim?: scalar|Param|null, // Claim which contains the user identifier (e.g. sub, email, etc.). // Default: "sub" + * client?: scalar|Param|null, // HttpClient service id to use to call the OIDC server. + * }, + * oidc?: array{ + * discovery?: array{ // Enable the OIDC discovery. + * base_uri?: string|list, + * cache?: array{ + * id?: scalar|Param|null, // Cache service id to use to cache the OIDC discovery configuration. + * }, + * }, + * claim?: scalar|Param|null, // Claim which contains the user identifier (e.g.: sub, email..). // Default: "sub" + * audience?: scalar|Param|null, // Audience set in the token, for validation purpose. + * issuers?: list, + * algorithm?: array, + * algorithms?: list, + * key?: scalar|Param|null, // 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|Param|null, // 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|Param|null, // JSON-encoded JWKSet used to decrypt the token (must contain a list of valid private keys). + * }, + * }, + * cas?: array{ + * validation_url?: scalar|Param|null, // CAS server validation URL + * prefix?: scalar|Param|null, // CAS prefix // Default: "cas" + * http_client?: scalar|Param|null, // HTTP Client service // Default: null + * }, + * oauth2?: scalar|Param|null, + * }, + * }, + * http_basic?: array{ + * provider?: scalar|Param|null, + * realm?: scalar|Param|null, // Default: "Secured Area" + * }, + * http_basic_ldap?: array{ + * provider?: scalar|Param|null, + * realm?: scalar|Param|null, // Default: "Secured Area" + * service?: scalar|Param|null, // Default: "ldap" + * dn_string?: scalar|Param|null, // Default: "{user_identifier}" + * query_string?: scalar|Param|null, + * search_dn?: scalar|Param|null, // Default: "" + * search_password?: scalar|Param|null, // Default: "" + * }, + * remember_me?: array{ + * secret?: scalar|Param|null, // Default: "%kernel.secret%" + * service?: scalar|Param|null, + * user_providers?: string|list, + * catch_exceptions?: bool|Param, // Default: true + * signature_properties?: list, + * token_provider?: string|array{ + * service?: scalar|Param|null, // The service ID of a custom remember-me token provider. + * doctrine?: bool|array{ + * enabled?: bool|Param, // Default: false + * connection?: scalar|Param|null, // Default: null + * }, + * }, + * token_verifier?: scalar|Param|null, // The service ID of a custom rememberme token verifier. + * name?: scalar|Param|null, // Default: "REMEMBERME" + * lifetime?: int|Param, // Default: 31536000 + * path?: scalar|Param|null, // Default: "/" + * domain?: scalar|Param|null, // 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|Param|null, // Default: "_remember_me" + * }, + * }>, * access_control?: list, - * attributes?: array, - * route?: scalar|Param|null, // Default: null - * methods?: list, - * allow_if?: scalar|Param|null, // Default: null - * roles?: list, - * }>, + * request_matcher?: scalar|Param|null, // Default: null + * requires_channel?: scalar|Param|null, // Default: null + * path?: scalar|Param|null, // Use the urldecoded format. // Default: null + * host?: scalar|Param|null, // Default: null + * port?: int|Param, // Default: null + * ips?: string|list, + * attributes?: array, + * route?: scalar|Param|null, // Default: null + * methods?: string|list, + * allow_if?: scalar|Param|null, // Default: null + * roles?: string|list, + * }>, * role_hierarchy?: array>, * } * @psalm-type TwigConfig = array{ * form_themes?: list, * globals?: array, + * id?: scalar|Param|null, + * type?: scalar|Param|null, + * value?: mixed, + * }>, * autoescape_service?: scalar|Param|null, // Default: null * autoescape_service_method?: scalar|Param|null, // Default: null * base_template_class?: scalar|Param|null, // Deprecated: The child node "base_template_class" at path "twig.base_template_class" is deprecated. @@ -1351,7 +1352,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * auto_reload?: scalar|Param|null, * optimizations?: int|Param, * default_path?: scalar|Param|null, // The default path used to load templates. // Default: "%kernel.project_dir%/templates" - * file_name_pattern?: list, + * file_name_pattern?: string|list, * paths?: array, * date?: array{ // The default format options used by the date filter. * format?: scalar|Param|null, // Default: "F j, Y H:i" @@ -1379,144 +1380,144 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * use_microseconds?: scalar|Param|null, // Default: true * channels?: list, * handlers?: array, - * }>, - * accepted_levels?: list, - * min_level?: scalar|Param|null, // Default: "DEBUG" - * max_level?: scalar|Param|null, // Default: "EMERGENCY" - * buffer_size?: scalar|Param|null, // Default: 0 - * flush_on_overflow?: bool|Param, // Default: false - * handler?: scalar|Param|null, - * url?: scalar|Param|null, - * exchange?: scalar|Param|null, - * exchange_name?: scalar|Param|null, // Default: "log" - * channel?: scalar|Param|null, // Default: null - * bot_name?: scalar|Param|null, // Default: "Monolog" - * use_attachment?: scalar|Param|null, // Default: true - * use_short_attachment?: scalar|Param|null, // Default: false - * include_extra?: scalar|Param|null, // Default: false - * icon_emoji?: scalar|Param|null, // Default: null - * webhook_url?: scalar|Param|null, - * exclude_fields?: list, - * token?: scalar|Param|null, - * region?: scalar|Param|null, - * source?: scalar|Param|null, - * use_ssl?: bool|Param, // Default: true - * user?: mixed, - * title?: scalar|Param|null, // Default: null - * host?: scalar|Param|null, // Default: null - * port?: scalar|Param|null, // Default: 514 - * config?: list, - * members?: list, - * connection_string?: scalar|Param|null, - * timeout?: scalar|Param|null, - * time?: scalar|Param|null, // Default: 60 - * deduplication_level?: scalar|Param|null, // Default: 400 - * store?: scalar|Param|null, // Default: null - * connection_timeout?: scalar|Param|null, - * persistent?: bool|Param, - * message_type?: scalar|Param|null, // Default: 0 - * parse_mode?: scalar|Param|null, // Default: null - * disable_webpage_preview?: bool|Param|null, // Default: null - * disable_notification?: bool|Param|null, // Default: null - * split_long_messages?: bool|Param, // Default: false - * delay_between_messages?: bool|Param, // Default: false - * topic?: int|Param, // Default: null - * factor?: int|Param, // Default: 1 - * tags?: list, - * console_formatter_options?: mixed, // Default: [] - * formatter?: scalar|Param|null, - * nested?: bool|Param, // Default: false - * publisher?: string|array{ - * id?: scalar|Param|null, - * hostname?: scalar|Param|null, - * port?: scalar|Param|null, // Default: 12201 - * chunk_size?: scalar|Param|null, // Default: 1420 - * encoder?: "json"|"compressed_json"|Param, - * }, - * mongodb?: string|array{ - * id?: scalar|Param|null, // ID of a MongoDB\Client service - * uri?: scalar|Param|null, - * username?: scalar|Param|null, - * password?: scalar|Param|null, - * database?: scalar|Param|null, // Default: "monolog" - * collection?: scalar|Param|null, // Default: "logs" - * }, - * elasticsearch?: string|array{ - * id?: scalar|Param|null, - * hosts?: list, - * host?: scalar|Param|null, - * port?: scalar|Param|null, // Default: 9200 - * transport?: scalar|Param|null, // Default: "Http" - * user?: scalar|Param|null, // Default: null - * password?: scalar|Param|null, // Default: null - * }, - * index?: scalar|Param|null, // Default: "monolog" - * document_type?: scalar|Param|null, // Default: "logs" - * ignore_error?: scalar|Param|null, // Default: false - * redis?: string|array{ - * id?: scalar|Param|null, - * host?: scalar|Param|null, - * password?: scalar|Param|null, // Default: null - * port?: scalar|Param|null, // Default: 6379 - * database?: scalar|Param|null, // Default: 0 - * key_name?: scalar|Param|null, // Default: "monolog_redis" - * }, - * predis?: string|array{ - * id?: scalar|Param|null, - * host?: scalar|Param|null, - * }, - * from_email?: scalar|Param|null, - * to_email?: list, - * subject?: scalar|Param|null, - * content_type?: scalar|Param|null, // Default: null - * headers?: list, - * mailer?: scalar|Param|null, // Default: null - * email_prototype?: string|array{ - * id?: scalar|Param|null, - * method?: scalar|Param|null, // Default: null - * }, - * verbosity_levels?: array{ - * VERBOSITY_QUIET?: scalar|Param|null, // Default: "ERROR" - * VERBOSITY_NORMAL?: scalar|Param|null, // Default: "WARNING" - * VERBOSITY_VERBOSE?: scalar|Param|null, // Default: "NOTICE" - * VERBOSITY_VERY_VERBOSE?: scalar|Param|null, // Default: "INFO" - * VERBOSITY_DEBUG?: scalar|Param|null, // Default: "DEBUG" - * }, - * channels?: string|array{ * type?: scalar|Param|null, - * elements?: list, - * }, - * }>, + * id?: scalar|Param|null, + * enabled?: bool|Param, // Default: true + * priority?: scalar|Param|null, // Default: 0 + * level?: scalar|Param|null, // Default: "DEBUG" + * bubble?: bool|Param, // Default: true + * interactive_only?: bool|Param, // Default: false + * app_name?: scalar|Param|null, // Default: null + * include_stacktraces?: bool|Param, // Default: false + * process_psr_3_messages?: array{ + * enabled?: bool|Param|null, // Default: null + * date_format?: scalar|Param|null, + * remove_used_context_fields?: bool|Param, + * }, + * path?: scalar|Param|null, // Default: "%kernel.logs_dir%/%kernel.environment%.log" + * file_permission?: scalar|Param|null, // Default: null + * use_locking?: bool|Param, // Default: false + * filename_format?: scalar|Param|null, // Default: "{filename}-{date}" + * date_format?: scalar|Param|null, // Default: "Y-m-d" + * ident?: scalar|Param|null, // Default: false + * logopts?: scalar|Param|null, // Default: 1 + * facility?: scalar|Param|null, // Default: "user" + * max_files?: scalar|Param|null, // Default: 0 + * action_level?: scalar|Param|null, // Default: "WARNING" + * activation_strategy?: scalar|Param|null, // Default: null + * stop_buffering?: bool|Param, // Default: true + * passthru_level?: scalar|Param|null, // Default: null + * excluded_http_codes?: list, + * }>, + * accepted_levels?: list, + * min_level?: scalar|Param|null, // Default: "DEBUG" + * max_level?: scalar|Param|null, // Default: "EMERGENCY" + * buffer_size?: scalar|Param|null, // Default: 0 + * flush_on_overflow?: bool|Param, // Default: false + * handler?: scalar|Param|null, + * url?: scalar|Param|null, + * exchange?: scalar|Param|null, + * exchange_name?: scalar|Param|null, // Default: "log" + * channel?: scalar|Param|null, // Default: null + * bot_name?: scalar|Param|null, // Default: "Monolog" + * use_attachment?: scalar|Param|null, // Default: true + * use_short_attachment?: scalar|Param|null, // Default: false + * include_extra?: scalar|Param|null, // Default: false + * icon_emoji?: scalar|Param|null, // Default: null + * webhook_url?: scalar|Param|null, + * exclude_fields?: list, + * token?: scalar|Param|null, + * region?: scalar|Param|null, + * source?: scalar|Param|null, + * use_ssl?: bool|Param, // Default: true + * user?: mixed, + * title?: scalar|Param|null, // Default: null + * host?: scalar|Param|null, // Default: null + * port?: scalar|Param|null, // Default: 514 + * config?: list, + * members?: list, + * connection_string?: scalar|Param|null, + * timeout?: scalar|Param|null, + * time?: scalar|Param|null, // Default: 60 + * deduplication_level?: scalar|Param|null, // Default: 400 + * store?: scalar|Param|null, // Default: null + * connection_timeout?: scalar|Param|null, + * persistent?: bool|Param, + * message_type?: scalar|Param|null, // Default: 0 + * parse_mode?: scalar|Param|null, // Default: null + * disable_webpage_preview?: bool|Param|null, // Default: null + * disable_notification?: bool|Param|null, // Default: null + * split_long_messages?: bool|Param, // Default: false + * delay_between_messages?: bool|Param, // Default: false + * topic?: int|Param, // Default: null + * factor?: int|Param, // Default: 1 + * tags?: string|list, + * console_formatter_options?: mixed, // Default: [] + * formatter?: scalar|Param|null, + * nested?: bool|Param, // Default: false + * publisher?: string|array{ + * id?: scalar|Param|null, + * hostname?: scalar|Param|null, + * port?: scalar|Param|null, // Default: 12201 + * chunk_size?: scalar|Param|null, // Default: 1420 + * encoder?: "json"|"compressed_json"|Param, + * }, + * mongodb?: string|array{ + * id?: scalar|Param|null, // ID of a MongoDB\Client service + * uri?: scalar|Param|null, + * username?: scalar|Param|null, + * password?: scalar|Param|null, + * database?: scalar|Param|null, // Default: "monolog" + * collection?: scalar|Param|null, // Default: "logs" + * }, + * elasticsearch?: string|array{ + * id?: scalar|Param|null, + * hosts?: list, + * host?: scalar|Param|null, + * port?: scalar|Param|null, // Default: 9200 + * transport?: scalar|Param|null, // Default: "Http" + * user?: scalar|Param|null, // Default: null + * password?: scalar|Param|null, // Default: null + * }, + * index?: scalar|Param|null, // Default: "monolog" + * document_type?: scalar|Param|null, // Default: "logs" + * ignore_error?: scalar|Param|null, // Default: false + * redis?: string|array{ + * id?: scalar|Param|null, + * host?: scalar|Param|null, + * password?: scalar|Param|null, // Default: null + * port?: scalar|Param|null, // Default: 6379 + * database?: scalar|Param|null, // Default: 0 + * key_name?: scalar|Param|null, // Default: "monolog_redis" + * }, + * predis?: string|array{ + * id?: scalar|Param|null, + * host?: scalar|Param|null, + * }, + * from_email?: scalar|Param|null, + * to_email?: string|list, + * subject?: scalar|Param|null, + * content_type?: scalar|Param|null, // Default: null + * headers?: list, + * mailer?: scalar|Param|null, // Default: null + * email_prototype?: string|array{ + * id?: scalar|Param|null, + * method?: scalar|Param|null, // Default: null + * }, + * verbosity_levels?: array{ + * VERBOSITY_QUIET?: scalar|Param|null, // Default: "ERROR" + * VERBOSITY_NORMAL?: scalar|Param|null, // Default: "WARNING" + * VERBOSITY_VERBOSE?: scalar|Param|null, // Default: "NOTICE" + * VERBOSITY_VERY_VERBOSE?: scalar|Param|null, // Default: "INFO" + * VERBOSITY_DEBUG?: scalar|Param|null, // Default: "DEBUG" + * }, + * channels?: string|array{ + * type?: scalar|Param|null, + * elements?: list, + * }, + * }>, * } * @psalm-type DebugConfig = array{ * max_items?: int|Param, // Max number of displayed items past the first level, -1 means no limit. // Default: 2500 @@ -1556,52 +1557,52 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * } * @psalm-type LiipImagineConfig = array{ * resolvers?: array, - * get_options?: array, - * put_options?: array, - * proxies?: array, - * }, - * flysystem?: array{ - * filesystem_service?: scalar|Param|null, - * cache_prefix?: scalar|Param|null, // Default: "" - * root_url?: scalar|Param|null, - * visibility?: "public"|"private"|"noPredefinedVisibility"|Param, // Default: "public" - * }, - * }>, - * loaders?: array, - * allow_unresolvable_data_roots?: bool|Param, // Default: false - * bundle_resources?: array{ - * enabled?: bool|Param, // Default: false - * access_control_type?: "blacklist"|"whitelist"|Param, // Sets the access control method applied to bundle names in "access_control_list" into a blacklist or whitelist. // Default: "blacklist" - * access_control_list?: list, + * web_path?: array{ + * web_root?: scalar|Param|null, // Default: "%kernel.project_dir%/public" + * cache_prefix?: scalar|Param|null, // Default: "media/cache" * }, - * }, - * flysystem?: array{ - * filesystem_service?: scalar|Param|null, - * }, - * asset_mapper?: array, - * chain?: array{ - * loaders?: list, - * }, - * }>, + * aws_s3?: array{ + * bucket?: scalar|Param|null, + * cache?: scalar|Param|null, // Default: false + * use_psr_cache?: bool|Param, // Default: false + * acl?: scalar|Param|null, // Default: "public-read" + * cache_prefix?: scalar|Param|null, // Default: "" + * client_id?: scalar|Param|null, // Default: null + * client_config?: list, + * get_options?: array, + * put_options?: array, + * proxies?: array, + * }, + * flysystem?: array{ + * filesystem_service?: scalar|Param|null, + * cache_prefix?: scalar|Param|null, // Default: "" + * root_url?: scalar|Param|null, + * visibility?: "public"|"private"|"noPredefinedVisibility"|Param, // Default: "public" + * }, + * }>, + * loaders?: array, + * allow_unresolvable_data_roots?: bool|Param, // Default: false + * bundle_resources?: array{ + * enabled?: bool|Param, // Default: false + * access_control_type?: "blacklist"|"whitelist"|Param, // Sets the access control method applied to bundle names in "access_control_list" into a blacklist or whitelist. // Default: "blacklist" + * access_control_list?: list, + * }, + * }, + * flysystem?: array{ + * filesystem_service?: scalar|Param|null, + * }, + * asset_mapper?: array, + * chain?: array{ + * loaders?: list, + * }, + * }>, * driver?: scalar|Param|null, // Default: "gd" * cache?: scalar|Param|null, // Default: "default" * cache_base_path?: scalar|Param|null, // Default: "" @@ -1626,18 +1627,18 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * redirect_response_code?: int|Param, // Default: 302 * }, * filter_sets?: array>, - * post_processors?: array>, - * }>, + * quality?: scalar|Param|null, + * jpeg_quality?: scalar|Param|null, + * png_compression_level?: scalar|Param|null, + * png_compression_filter?: scalar|Param|null, + * format?: scalar|Param|null, + * animated?: bool|Param, + * cache?: scalar|Param|null, + * data_loader?: scalar|Param|null, + * default_image?: scalar|Param|null, + * filters?: array>, + * post_processors?: array>, + * }>, * twig?: array{ * mode?: "none"|"lazy"|"legacy"|Param, // Twig mode: none/lazy/legacy (default) // Default: "legacy" * assets_version?: scalar|Param|null, // Default: null @@ -1852,8 +1853,8 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * clickjacking?: array{ * hosts?: list, * paths?: array, + * header?: scalar|Param|null, // Default: "DENY" + * }>, * content_types?: list, * }, * external_redirects?: array{ @@ -1930,7 +1931,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * script-src?: list, * style-src?: list, * upgrade-insecure-requests?: bool|Param, // Default: false - * report-uri?: list, + * report-uri?: string|list, * worker-src?: list, * prefetch-src?: list, * report-to?: scalar|Param|null, @@ -1958,7 +1959,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * script-src?: list, * style-src?: list, * upgrade-insecure-requests?: bool|Param, // Default: false - * report-uri?: list, + * report-uri?: string|list, * worker-src?: list, * prefetch-src?: list, * report-to?: scalar|Param|null, @@ -1966,7 +1967,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * }, * referrer_policy?: bool|array{ * enabled?: bool|Param, // Default: false - * policies?: list, + * policies?: string|list, * }, * permissions_policy?: bool|array{ * enabled?: bool|Param, // Default: false @@ -2018,12 +2019,12 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * cross_origin_isolation?: bool|array{ * enabled?: bool|Param, // Default: false * paths?: array, + * coep?: "unsafe-none"|"require-corp"|"credentialless"|Param, // Cross-Origin-Embedder-Policy (COEP) header value + * coop?: "unsafe-none"|"same-origin-allow-popups"|"same-origin"|"noopener-allow-popups"|Param, // Cross-Origin-Opener-Policy (COOP) header value + * corp?: "same-site"|"same-origin"|"cross-origin"|Param, // Cross-Origin-Resource-Policy (CORP) header value + * report_only?: bool|Param, // Use Report-Only headers instead of enforcing (applies to COEP and COOP only) // Default: false + * report_to?: scalar|Param|null, // Reporting endpoint name for violations (requires Reporting API configuration, applies to COEP and COOP only) // Default: null + * }>, * }, * } * @psalm-type TurboConfig = array{ @@ -2097,31 +2098,57 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * secured_rp_ids?: array, * counter_checker?: scalar|Param|null, // This service will check if the counter is valid. By default it throws an exception (recommended). // Default: "Webauthn\\Counter\\ThrowExceptionIfInvalid" * top_origin_validator?: scalar|Param|null, // For cross origin (e.g. iframe), this service will be in charge of verifying the top origin. // Default: null - * creation_profiles?: array, + * public_key_credential_parameters?: list, + * attestation_conveyance?: scalar|Param|null, // Default: "none" + * conditional_create?: bool|Param, // Enable Conditional Create (auto-register) for this profile. When true, user presence can be false after password authentication. See https://github.com/w3c/webauthn/wiki/Explainer:-Conditional-Create // Default: false + * }>, + * request_profiles?: bool|array, + * }>, + * client_override_policy?: array{ // Configuration for allowing client request values to override profile configuration + * user_verification?: array{ + * enabled?: bool|Param, // Whether to allow client requests to override the user verification requirement // Default: false + * allowed_values?: list, * }, - * extensions?: array, - * public_key_credential_parameters?: list, - * attestation_conveyance?: scalar|Param|null, // Default: "none" - * }>, - * request_profiles?: array, - * }>, + * authenticator_attachment?: array{ + * enabled?: bool|Param, // Whether to allow client requests to override the authenticator attachment // Default: true + * allowed_values?: list, + * }, + * resident_key?: array{ + * enabled?: bool|Param, // Whether to allow client requests to override the resident key requirement // Default: true + * allowed_values?: list, + * }, + * attestation_conveyance?: array{ + * enabled?: bool|Param, // Whether to allow client requests to override the attestation conveyance preference // Default: true + * allowed_values?: list, + * }, + * extensions?: array{ + * enabled?: bool|Param, // Whether to allow client requests to override extensions // Default: true + * }, + * mediation?: array{ + * enabled?: bool|Param, // Whether to allow client requests to request the conditional mediation flow (auto-register). // Default: false + * allowed_values?: list, + * }, + * }, * metadata?: bool|array{ // Enable the support of the Metadata Statements. Please read the documentation for this feature. * enabled?: bool|Param, // Default: false * mds_repository?: scalar|Param|null, // The Metadata Statement repository. @@ -2131,146 +2158,161 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * controllers?: bool|array{ * enabled?: bool|Param, // Default: false * creation?: array, - * allow_subdomains?: bool|Param, // Default: false - * secured_rp_ids?: array, - * }>, + * options_method?: scalar|Param|null, // Default: "POST" + * options_path?: scalar|Param|null, + * result_method?: scalar|Param|null, // Default: "POST" + * result_path?: scalar|Param|null, // Default: null + * host?: scalar|Param|null, // Default: null + * profile?: scalar|Param|null, // Default: "default" + * options_builder?: scalar|Param|null, // When set, corresponds to the ID of the Public Key Credential Creation Builder. The profile-based ebuilder is ignored. // Default: null + * user_entity_guesser?: scalar|Param|null, + * hide_existing_credentials?: scalar|Param|null, // In order to prevent username enumeration, the existing credentials can be hidden. This is highly recommended when the attestation ceremony is performed by anonymous users. // Default: false + * options_storage?: scalar|Param|null, // Deprecated: The child node "options_storage" at path "webauthn.controllers.creation..options_storage" is deprecated. Please use the root option "options_storage" instead. // Service responsible of the options/user entity storage during the ceremony // Default: null + * success_handler?: scalar|Param|null, // Default: "Webauthn\\Bundle\\Service\\DefaultSuccessHandler" + * failure_handler?: scalar|Param|null, // Default: "Webauthn\\Bundle\\Service\\DefaultFailureHandler" + * options_handler?: scalar|Param|null, // Default: "Webauthn\\Bundle\\Security\\Handler\\DefaultCreationOptionsHandler" + * allowed_origins?: array, + * allow_subdomains?: bool|Param, // Default: false + * secured_rp_ids?: array, + * }>, * request?: array, - * allow_subdomains?: bool|Param, // Default: false - * secured_rp_ids?: array, - * }>, + * options_method?: scalar|Param|null, // Default: "POST" + * options_path?: scalar|Param|null, + * result_method?: scalar|Param|null, // Default: "POST" + * result_path?: scalar|Param|null, // Default: null + * host?: scalar|Param|null, // Default: null + * profile?: scalar|Param|null, // Default: "default" + * options_builder?: scalar|Param|null, // When set, corresponds to the ID of the Public Key Credential Creation Builder. The profile-based ebuilder is ignored. // Default: null + * options_storage?: scalar|Param|null, // Deprecated: The child node "options_storage" at path "webauthn.controllers.request..options_storage" is deprecated. Please use the root option "options_storage" instead. // Service responsible of the options/user entity storage during the ceremony // Default: null + * success_handler?: scalar|Param|null, // Default: "Webauthn\\Bundle\\Service\\DefaultSuccessHandler" + * failure_handler?: scalar|Param|null, // Default: "Webauthn\\Bundle\\Service\\DefaultFailureHandler" + * options_handler?: scalar|Param|null, // Default: "Webauthn\\Bundle\\Security\\Handler\\DefaultRequestOptionsHandler" + * allowed_origins?: array, + * allow_subdomains?: bool|Param, // Default: false + * secured_rp_ids?: array, + * }>, + * }, + * passkey_endpoints?: bool|array{ // Enable the .well-known/passkey-endpoints discovery endpoint as defined in the W3C Passkey Endpoints specification. + * enabled?: bool|Param, // Default: false + * enroll?: string|array{ // URL to the passkey enrollment/creation interface. + * path?: scalar|Param|null, // The absolute HTTPS URL or Symfony route name. + * params?: list, + * }, + * manage?: string|array{ // URL to the passkey management interface. + * path?: scalar|Param|null, // The absolute HTTPS URL or Symfony route name. + * params?: list, + * }, + * prf_usage_details?: string|array{ // URL to informational page about PRF (Pseudo-Random Function) extension usage. + * path?: scalar|Param|null, // The absolute HTTPS URL or Symfony route name. + * params?: list, + * }, * }, * } * @psalm-type NbgrpOneloginSamlConfig = array{ // nb:group OneLogin PHP Symfony Bundle configuration * onelogin_settings?: array/saml/" - * strict?: bool|Param, - * debug?: bool|Param, - * idp?: array{ - * entityId?: scalar|Param|null, - * singleSignOnService?: array{ - * url?: scalar|Param|null, - * binding?: scalar|Param|null, + * baseurl?: scalar|Param|null, // Default: "/saml/" + * strict?: bool|Param, + * debug?: bool|Param, + * idp?: array{ + * entityId?: scalar|Param|null, + * singleSignOnService?: array{ + * url?: scalar|Param|null, + * binding?: scalar|Param|null, + * }, + * singleLogoutService?: array{ + * url?: scalar|Param|null, + * responseUrl?: scalar|Param|null, + * binding?: scalar|Param|null, + * }, + * x509cert?: scalar|Param|null, + * certFingerprint?: scalar|Param|null, + * certFingerprintAlgorithm?: "sha1"|"sha256"|"sha384"|"sha512"|Param, + * x509certMulti?: array{ + * signing?: list, + * encryption?: list, + * }, * }, - * singleLogoutService?: array{ - * url?: scalar|Param|null, - * responseUrl?: scalar|Param|null, - * binding?: scalar|Param|null, + * sp?: array{ + * entityId?: scalar|Param|null, // Default: "/saml/metadata" + * assertionConsumerService?: array{ + * url?: scalar|Param|null, // Default: "/saml/acs" + * binding?: scalar|Param|null, + * }, + * attributeConsumingService?: array{ + * serviceName?: scalar|Param|null, + * serviceDescription?: scalar|Param|null, + * requestedAttributes?: list, + * }>, + * }, + * singleLogoutService?: array{ + * url?: scalar|Param|null, // Default: "/saml/logout" + * binding?: scalar|Param|null, + * }, + * NameIDFormat?: scalar|Param|null, + * x509cert?: scalar|Param|null, + * privateKey?: scalar|Param|null, + * x509certNew?: scalar|Param|null, * }, - * x509cert?: scalar|Param|null, - * certFingerprint?: scalar|Param|null, - * certFingerprintAlgorithm?: "sha1"|"sha256"|"sha384"|"sha512"|Param, - * x509certMulti?: array{ - * signing?: list, - * encryption?: list, + * compress?: array{ + * requests?: bool|Param, + * responses?: bool|Param, * }, - * }, - * sp?: array{ - * entityId?: scalar|Param|null, // Default: "/saml/metadata" - * assertionConsumerService?: array{ - * url?: scalar|Param|null, // Default: "/saml/acs" - * binding?: scalar|Param|null, + * security?: array{ + * nameIdEncrypted?: bool|Param, + * authnRequestsSigned?: bool|Param, + * logoutRequestSigned?: bool|Param, + * logoutResponseSigned?: bool|Param, + * signMetadata?: bool|Param, + * wantMessagesSigned?: bool|Param, + * wantAssertionsEncrypted?: bool|Param, + * wantAssertionsSigned?: bool|Param, + * wantNameId?: bool|Param, + * wantNameIdEncrypted?: bool|Param, + * requestedAuthnContext?: mixed, + * requestedAuthnContextComparison?: "exact"|"minimum"|"maximum"|"better"|Param, + * wantXMLValidation?: bool|Param, + * relaxDestinationValidation?: bool|Param, + * destinationStrictlyMatches?: bool|Param, + * allowRepeatAttributeName?: bool|Param, + * rejectUnsolicitedResponsesWithInResponseTo?: bool|Param, + * signatureAlgorithm?: "http://www.w3.org/2000/09/xmldsig#rsa-sha1"|"http://www.w3.org/2000/09/xmldsig#dsa-sha1"|"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"|"http://www.w3.org/2001/04/xmldsig-more#rsa-sha384"|"http://www.w3.org/2001/04/xmldsig-more#rsa-sha512"|Param, + * digestAlgorithm?: "http://www.w3.org/2000/09/xmldsig#sha1"|"http://www.w3.org/2001/04/xmlenc#sha256"|"http://www.w3.org/2001/04/xmldsig-more#sha384"|"http://www.w3.org/2001/04/xmlenc#sha512"|Param, + * encryption_algorithm?: "http://www.w3.org/2001/04/xmlenc#tripledes-cbc"|"http://www.w3.org/2001/04/xmlenc#aes128-cbc"|"http://www.w3.org/2001/04/xmlenc#aes192-cbc"|"http://www.w3.org/2001/04/xmlenc#aes256-cbc"|"http://www.w3.org/2009/xmlenc11#aes128-gcm"|"http://www.w3.org/2009/xmlenc11#aes192-gcm"|"http://www.w3.org/2009/xmlenc11#aes256-gcm"|Param, + * lowercaseUrlencoding?: bool|Param, * }, - * attributeConsumingService?: array{ - * serviceName?: scalar|Param|null, - * serviceDescription?: scalar|Param|null, - * requestedAttributes?: list, + * displayname?: scalar|Param|null, + * url?: scalar|Param|null, * }>, - * }, - * singleLogoutService?: array{ - * url?: scalar|Param|null, // Default: "/saml/logout" - * binding?: scalar|Param|null, - * }, - * NameIDFormat?: scalar|Param|null, - * x509cert?: scalar|Param|null, - * privateKey?: scalar|Param|null, - * x509certNew?: scalar|Param|null, - * }, - * compress?: array{ - * requests?: bool|Param, - * responses?: bool|Param, - * }, - * security?: array{ - * nameIdEncrypted?: bool|Param, - * authnRequestsSigned?: bool|Param, - * logoutRequestSigned?: bool|Param, - * logoutResponseSigned?: bool|Param, - * signMetadata?: bool|Param, - * wantMessagesSigned?: bool|Param, - * wantAssertionsEncrypted?: bool|Param, - * wantAssertionsSigned?: bool|Param, - * wantNameId?: bool|Param, - * wantNameIdEncrypted?: bool|Param, - * requestedAuthnContext?: mixed, - * requestedAuthnContextComparison?: "exact"|"minimum"|"maximum"|"better"|Param, - * wantXMLValidation?: bool|Param, - * relaxDestinationValidation?: bool|Param, - * destinationStrictlyMatches?: bool|Param, - * allowRepeatAttributeName?: bool|Param, - * rejectUnsolicitedResponsesWithInResponseTo?: bool|Param, - * signatureAlgorithm?: "http://www.w3.org/2000/09/xmldsig#rsa-sha1"|"http://www.w3.org/2000/09/xmldsig#dsa-sha1"|"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"|"http://www.w3.org/2001/04/xmldsig-more#rsa-sha384"|"http://www.w3.org/2001/04/xmldsig-more#rsa-sha512"|Param, - * digestAlgorithm?: "http://www.w3.org/2000/09/xmldsig#sha1"|"http://www.w3.org/2001/04/xmlenc#sha256"|"http://www.w3.org/2001/04/xmldsig-more#sha384"|"http://www.w3.org/2001/04/xmlenc#sha512"|Param, - * encryption_algorithm?: "http://www.w3.org/2001/04/xmlenc#tripledes-cbc"|"http://www.w3.org/2001/04/xmlenc#aes128-cbc"|"http://www.w3.org/2001/04/xmlenc#aes192-cbc"|"http://www.w3.org/2001/04/xmlenc#aes256-cbc"|"http://www.w3.org/2009/xmlenc11#aes128-gcm"|"http://www.w3.org/2009/xmlenc11#aes192-gcm"|"http://www.w3.org/2009/xmlenc11#aes256-gcm"|Param, - * lowercaseUrlencoding?: bool|Param, - * }, - * contactPerson?: array{ - * technical?: array{ - * givenName?: scalar|Param|null, - * emailAddress?: scalar|Param|null, - * }, - * support?: array{ - * givenName?: scalar|Param|null, - * emailAddress?: scalar|Param|null, - * }, - * administrative?: array{ - * givenName?: scalar|Param|null, - * emailAddress?: scalar|Param|null, - * }, - * billing?: array{ - * givenName?: scalar|Param|null, - * emailAddress?: scalar|Param|null, - * }, - * other?: array{ - * givenName?: scalar|Param|null, - * emailAddress?: scalar|Param|null, - * }, - * }, - * organization?: list, - * }>, * use_proxy_vars?: bool|Param, // Default: false * idp_parameter_name?: scalar|Param|null, // Default: "idp" * entity_manager_name?: scalar|Param|null, @@ -2293,7 +2335,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * type?: scalar|Param|null, * elements?: list, * }, - * keys_patterns?: list, + * keys_patterns?: string|list, * } * @psalm-type DompdfFontLoaderConfig = array{ * autodiscovery?: bool|array{ @@ -2304,11 +2346,11 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * }, * auto_install?: bool|Param, // Default: false * fonts?: list, + * normal?: scalar|Param|null, + * bold?: scalar|Param|null, + * italic?: scalar|Param|null, + * bold_italic?: scalar|Param|null, + * }>, * } * @psalm-type KnpuOauth2ClientConfig = array{ * http_client?: scalar|Param|null, // Service id of HTTP client to use (must implement GuzzleHttp\ClientInterface) // Default: null @@ -2334,18 +2376,18 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * skip_same_as_origin?: bool|Param, // Default: true * }, * paths?: array, - * allow_headers?: list, - * allow_methods?: list, - * allow_private_network?: bool|Param, - * expose_headers?: list, - * max_age?: scalar|Param|null, // Default: 0 - * hosts?: list, - * origin_regex?: bool|Param, - * forced_allow_origin_value?: scalar|Param|null, // Default: null - * skip_same_as_origin?: bool|Param, - * }>, + * allow_credentials?: bool|Param, + * allow_origin?: list, + * allow_headers?: list, + * allow_methods?: list, + * allow_private_network?: bool|Param, + * expose_headers?: list, + * max_age?: scalar|Param|null, // Default: 0 + * hosts?: list, + * origin_regex?: bool|Param, + * forced_allow_origin_value?: scalar|Param|null, // Default: null + * skip_same_as_origin?: bool|Param, + * }>, * } * @psalm-type JbtronicsSettingsConfig = array{ * search_paths?: list, @@ -2476,13 +2518,13 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * persist_authorization?: bool|Param, // Persist the SwaggerUI Authorization in the localStorage. // Default: false * versions?: list, * api_keys?: array, + * name?: scalar|Param|null, // The name of the header or query parameter containing the api key. + * type?: "query"|"header"|Param, // Whether the api key should be a query parameter or a header. + * }>, * http_auth?: array, + * scheme?: scalar|Param|null, // The OpenAPI HTTP auth scheme, for example "bearer" + * bearerFormat?: scalar|Param|null, // The OpenAPI HTTP bearer format + * }>, * swagger_ui_extra_configuration?: mixed, // To pass extra configuration to Swagger UI, like docExpansion or filter. // Default: [] * }, * http_cache?: array{ @@ -2523,9 +2565,9 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * }, * termsOfService?: scalar|Param|null, // A URL to the Terms of Service for the API. MUST be in the format of a URL. // Default: null * tags?: list, + * name?: scalar|Param|null, + * description?: scalar|Param|null, // Default: null + * }>, * license?: array{ * name?: scalar|Param|null, // The license name used for the API. // Default: null * url?: scalar|Param|null, // URL to the license used for the API. MUST be in the format of a URL. // Default: null @@ -2547,17 +2589,17 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * }, * exception_to_status?: array, * formats?: array, - * }>, + * mime_types?: list, + * }>, * patch_formats?: array, - * }>, + * mime_types?: list, + * }>, * docs_formats?: array, - * }>, + * mime_types?: list, + * }>, * error_formats?: array, - * }>, + * mime_types?: list, + * }>, * jsonschema_formats?: list, * defaults?: array{ * uri_template?: mixed, @@ -2629,30 +2671,30 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * policy?: mixed, * middleware?: mixed, * parameters?: array - * }>, + * key?: mixed, + * schema?: mixed, + * open_api?: mixed, + * provider?: mixed, + * filter?: mixed, + * property?: mixed, + * description?: mixed, + * properties?: mixed, + * required?: mixed, + * priority?: mixed, + * hydra?: mixed, + * constraints?: mixed, + * security?: mixed, + * security_message?: mixed, + * extra_properties?: mixed, + * filter_context?: mixed, + * native_type?: mixed, + * cast_to_array?: mixed, + * cast_to_native_type?: mixed, + * cast_fn?: mixed, + * default?: mixed, + * filter_class?: mixed, + * ... + * }>, * strict_query_parameter_validation?: mixed, * hide_hydra_operation?: mixed, * json_stream?: mixed, @@ -2674,6 +2716,465 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * ... * }, * } + * @psalm-type AiConfig = array{ + * platform?: array{ + * albert?: array{ + * api_key?: string|Param, + * base_url?: string|Param, + * http_client?: string|Param, // Service ID of the HTTP client to use // Default: "http_client" + * }, + * amazeeai?: array{ + * base_url?: string|Param, + * api_key?: string|Param, + * http_client?: string|Param, // Service ID of the HTTP client to use // Default: "http_client" + * }, + * anthropic?: array{ + * api_key?: string|Param, + * version?: string|Param, // Default: null + * http_client?: string|Param, // Service ID of the HTTP client to use // Default: "http_client" + * cache_retention?: "none"|"short"|"long"|Param, // Prompt cache retention policy for Anthropic models // Default: "short" + * }, + * azure?: array, + * bedrock?: array, + * cache?: array, + * cartesia?: array{ + * api_key?: string|Param, + * version?: string|Param, + * http_client?: string|Param, // Service ID of the HTTP client to use // Default: "http_client" + * }, + * cerebras?: array{ + * api_key?: string|Param, + * http_client?: string|Param, // Service ID of the HTTP client to use // Default: "http_client" + * }, + * cohere?: array{ + * api_key?: string|Param, + * http_client?: string|Param, // Service ID of the HTTP client to use // Default: "http_client" + * }, + * decart?: array{ + * api_key?: string|Param, + * host?: string|Param, // Default: "https://api.decart.ai/v1" + * http_client?: string|Param, // Service ID of the HTTP client to use // Default: "http_client" + * }, + * deepseek?: array{ + * api_key?: string|Param, + * http_client?: string|Param, // Service ID of the HTTP client to use // Default: "http_client" + * }, + * dockermodelrunner?: array{ + * host_url?: string|Param, // Default: "http://127.0.0.1:12434" + * http_client?: string|Param, // Service ID of the HTTP client to use // Default: "http_client" + * }, + * elevenlabs?: array{ + * api_key?: string|Param, + * endpoint?: string|Param, // Default: "https://api.elevenlabs.io/v1/" + * http_client?: string|Param, // Service ID of the HTTP client to use // Default: "http_client" + * }, + * failover?: array, + * rate_limiter?: string|Param, + * }>, + * gemini?: array{ + * api_key?: string|Param, + * http_client?: string|Param, // Service ID of the HTTP client to use // Default: "http_client" + * }, + * generic?: array, + * huggingface?: array{ + * api_key?: string|Param, + * provider?: string|Param, // Default: "hf-inference" + * http_client?: string|Param, // Service ID of the HTTP client to use // Default: "http_client" + * }, + * lmstudio?: array{ + * host_url?: string|Param, // Default: "http://127.0.0.1:1234" + * http_client?: string|Param, // Service ID of the HTTP client to use // Default: "http_client" + * }, + * mistral?: array{ + * api_key?: string|Param, + * http_client?: string|Param, // Service ID of the HTTP client to use // Default: "http_client" + * }, + * ollama?: array{ + * endpoint?: string|Param, // Endpoint for Ollama (e.g. "http://127.0.0.1:11434" for local, or a cloud endpoint). If null, the http_client is used as-is and must already be configured with a base URI. + * api_key?: string|Param, // API key for Ollama Cloud authentication (optional for local usage) + * http_client?: string|Param, // Service ID of the HTTP client to use. When "endpoint" is null, this client must be pre-configured (e.g. with a base_uri). // Default: "http_client" + * }, + * openai?: array{ + * api_key?: string|Param, + * region?: scalar|Param|null, // The region for OpenAI API (EU, US, or null for default) // Default: null + * http_client?: string|Param, // Service ID of the HTTP client to use // Default: "http_client" + * }, + * openrouter?: array{ + * api_key?: string|Param, + * http_client?: string|Param, // Service ID of the HTTP client to use // Default: "http_client" + * }, + * ovh?: array{ + * api_key?: scalar|Param|null, + * http_client?: string|Param, // Service ID of the HTTP client to use // Default: "http_client" + * }, + * perplexity?: array{ + * api_key?: string|Param, + * http_client?: string|Param, // Service ID of the HTTP client to use // Default: "http_client" + * }, + * scaleway?: array{ + * api_key?: scalar|Param|null, + * http_client?: string|Param, // Service ID of the HTTP client to use // Default: "http_client" + * }, + * transformersphp?: array, + * vertexai?: array{ + * location?: string|Param, // Required for the project-scoped endpoint. Must be set together with "project_id". // Default: null + * project_id?: string|Param, // Required for the project-scoped endpoint. Must be set together with "location". // Default: null + * api_key?: string|Param, // When set without "location" and "project_id", uses the global endpoint. Note: API keys only identify the project for billing and do not provide identity-based access control. For production use with IAM, audit logging, or data residency, prefer the project-scoped endpoint with service account authentication. // Default: null + * http_client?: string|Param, // Service ID of the HTTP client to use // Default: "http_client" + * }, + * voyage?: array{ + * api_key?: string|Param, + * http_client?: string|Param, // Service ID of the HTTP client to use // Default: "http_client" + * }, + * }, + * model?: array|\Symfony\AI\Platform\Capability|Param>, + * }>>, + * agent?: array, + * }, + * keep_tool_messages?: bool|Param, // Keep tool messages in the conversation history // Default: false + * include_sources?: bool|Param, // Include sources exposed by tools as part of the tool result metadata // Default: false + * fault_tolerant_toolbox?: bool|Param, // Continue the agent run even if a tool call fails // Default: true + * speech?: bool|array{ // Speech (TTS/STT) decorator configuration + * enabled?: bool|Param, // Default: true + * text_to_speech_platform?: string|Param, // Service name of the TTS platform (e.g. ai.platform.elevenlabs). // Default: null + * speech_to_text_platform?: string|Param, // Service name of the STT platform (e.g. ai.platform.openai). // Default: null + * tts_model?: string|Param, // Text-to-speech model name // Default: null + * tts_options?: mixed, // Provider-specific TTS options // Default: [] + * stt_model?: string|Param, // Speech-to-text model name // Default: null + * stt_options?: mixed, // Provider-specific STT options // Default: [] + * }, + * }>, + * multi_agent?: array>, + * fallback?: string|Param, // Service ID of the fallback agent for unmatched requests + * }>, + * store?: array{ + * azuresearch?: array, + * cache?: array, + * chromadb?: array, + * clickhouse?: array, + * cloudflare?: array, + * elasticsearch?: array, + * manticoresearch?: array, + * mariadb?: array, + * meilisearch?: array, + * memory?: array, + * milvus?: array, + * mongodb?: array, + * neo4j?: array, + * opensearch?: array, + * pinecone?: array, + * top_k?: int|Param, + * }>, + * postgres?: array, + * qdrant?: array, + * redis?: array, + * s3vectors?: array, + * vector_bucket_name?: string|Param, + * index_name?: string|Param, + * filter?: array, + * top_k?: int|Param, // Default number of results to return // Default: 3 + * }>, + * sqlite?: array, + * supabase?: array, + * surrealdb?: array, + * typesense?: array, + * weaviate?: array, + * vektor?: array, + * }, + * message_store?: array{ + * cache?: array, + * cloudflare?: array, + * doctrine?: array{ + * dbal?: array, + * }, + * meilisearch?: array, + * memory?: array, + * mongodb?: array, + * pogocache?: array, + * redis?: array, + * session?: array, + * surrealdb?: array, + * }, + * chat?: array, + * vectorizer?: array, + * indexer?: array, + * filters?: list, + * vectorizer?: scalar|Param|null, // Service name of vectorizer // Default: "Symfony\\AI\\Store\\Document\\VectorizerInterface" + * store?: string|Param, // Service name of store // Default: "Symfony\\AI\\Store\\StoreInterface" + * }>, + * retriever?: array, + * } * @psalm-type ConfigType = array{ * imports?: ImportsConfig, * parameters?: ParametersConfig, @@ -2703,6 +3204,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * nelmio_cors?: NelmioCorsConfig, * jbtronics_settings?: JbtronicsSettingsConfig, * api_platform?: ApiPlatformConfig, + * ai?: AiConfig, * "when@dev"?: array{ * imports?: ImportsConfig, * parameters?: ParametersConfig, @@ -2736,6 +3238,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * jbtronics_settings?: JbtronicsSettingsConfig, * jbtronics_translation_editor?: JbtronicsTranslationEditorConfig, * api_platform?: ApiPlatformConfig, + * ai?: AiConfig, * }, * "when@docker"?: array{ * imports?: ImportsConfig, @@ -2766,6 +3269,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * nelmio_cors?: NelmioCorsConfig, * jbtronics_settings?: JbtronicsSettingsConfig, * api_platform?: ApiPlatformConfig, + * ai?: AiConfig, * }, * "when@prod"?: array{ * imports?: ImportsConfig, @@ -2796,6 +3300,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * nelmio_cors?: NelmioCorsConfig, * jbtronics_settings?: JbtronicsSettingsConfig, * api_platform?: ApiPlatformConfig, + * ai?: AiConfig, * }, * "when@test"?: array{ * imports?: ImportsConfig, @@ -2829,6 +3334,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * nelmio_cors?: NelmioCorsConfig, * jbtronics_settings?: JbtronicsSettingsConfig, * api_platform?: ApiPlatformConfig, + * ai?: AiConfig, * }, * ...get('keyword')->getData(); $providers = $form->get('providers')->getData(); + $no_cache_search = $form->get('no_cache_search')->getData(); + $no_cache_details = $form->get('no_cache_details')->getData(); + $dtos = []; try { - $dtos = $this->infoRetriever->searchByKeyword(keyword: $keyword, providers: $providers); + $dtos = $this->infoRetriever->searchByKeyword(keyword: $keyword, providers: $providers, options: [ + InfoProviderInterface::OPTION_NO_CACHE => $no_cache_search + ]); } catch (ClientException $e) { $this->addFlash('error', t('info_providers.search.error.client_exception')); $this->addFlash('error',$e->getMessage()); @@ -207,40 +215,41 @@ class InfoProviderController extends AbstractController return $this->render('info_providers/search/part_search.html.twig', [ 'form' => $form, 'results' => $results, - 'update_target' => $update_target + 'update_target' => $update_target, + 'no_cache_details' => $no_cache_details ?? false, ]); } #[Route('/from_url', name: 'info_providers_from_url')] - public function fromURL(Request $request, GenericWebProvider $provider): Response + public function fromURL(Request $request, GenericWebProvider $provider, CreateFromUrlHelper $fromUrlHelper): Response { $this->denyAccessUnlessGranted('@info_providers.create_parts'); - if (!$provider->isActive()) { + if (!$fromUrlHelper->canCreateFromUrl()) { $this->addFlash('error', "Generic Web Provider is not active. Please enable it in the provider settings."); return $this->redirectToRoute('info_providers_list'); } - $formBuilder = $this->createFormBuilder(); - $formBuilder->add('url', UrlType::class, [ - 'label' => 'info_providers.from_url.url.label', - 'required' => true, - ]); - $formBuilder->add('submit', SubmitType::class, [ - 'label' => 'info_providers.search.submit', - ]); - - $form = $formBuilder->getForm(); + $form = $this->createForm(FromURLFormType::class); $form->handleRequest($request); $partDetail = null; if ($form->isSubmitted() && $form->isValid()) { //Try to retrieve the part detail from the given URL $url = $form->get('url')->getData(); + + $method = $form->get('method')->getData(); + $no_cache = $form->get('no_cache')->getData(); + $skip_delegation = $form->get('skip_delegation')->getData(); + try { + //It's okay if we use the cached results here, as its just for convenience $searchResult = $this->infoRetriever->searchByKeyword( keyword: $url, - providers: [$provider] + providers: [$method], + options: [ + InfoProviderInterface::OPTION_SKIP_DELEGATION => $skip_delegation, + ] ); if (count($searchResult) === 0) { @@ -251,6 +260,8 @@ class InfoProviderController extends AbstractController return $this->redirectToRoute('info_providers_create_part', [ 'providerKey' => $searchResult->provider_key, 'providerId' => $searchResult->provider_id, + 'no_cache' => $no_cache ? 1 : null, + 'skip_delegation' => $skip_delegation ? 1 : null, ]); } } catch (ExceptionInterface $e) { diff --git a/src/Controller/PartController.php b/src/Controller/PartController.php index eb80d9bc..ab424f50 100644 --- a/src/Controller/PartController.php +++ b/src/Controller/PartController.php @@ -40,6 +40,7 @@ use App\Services\Attachments\AttachmentSubmitHandler; use App\Services\Attachments\PartPreviewGenerator; use App\Services\EntityMergers\Mergers\PartMerger; use App\Services\InfoProviderSystem\PartInfoRetriever; +use App\Services\InfoProviderSystem\Providers\InfoProviderInterface; use App\Services\LogSystem\EventCommentHelper; use App\Services\LogSystem\HistoryHelper; use App\Services\LogSystem\TimeTravel; @@ -283,7 +284,14 @@ final class PartController extends AbstractController { $this->denyAccessUnlessGranted('@info_providers.create_parts'); - $dto = $infoRetriever->getDetails($providerKey, $providerId); + //Force info providers to not use cache, when retrieving part details for creating a new part, because otherwise we might end up with outdated information + $no_cache = $request->query->getBoolean('no_cache', false); + $skip_delegation = $request->query->getBoolean('skip_delegation', false); + + $dto = $infoRetriever->getDetails($providerKey, $providerId, [ + InfoProviderInterface::OPTION_NO_CACHE => $no_cache, + InfoProviderInterface::OPTION_SKIP_DELEGATION => $skip_delegation, + ]); $new_part = $infoRetriever->dtoToPart($dto); if ($new_part->getCategory() === null || $new_part->getCategory()->getID() === null) { @@ -342,10 +350,13 @@ final class PartController extends AbstractController $this->denyAccessUnlessGranted('edit', $part); $this->denyAccessUnlessGranted('@info_providers.create_parts'); + //Force info providers to not use cache, when retrieving part details for creating a new part, because otherwise we might end up with outdated information + $no_cache = $request->query->getBoolean('no_cache', false); + //Save the old name of the target part for the template $old_name = $part->getName(); - $dto = $infoRetriever->getDetails($providerKey, $providerId); + $dto = $infoRetriever->getDetails($providerKey, $providerId, [InfoProviderInterface::OPTION_NO_CACHE => $no_cache]); $provider_part = $infoRetriever->dtoToPart($dto); $part = $partMerger->merge($part, $provider_part); diff --git a/src/Controller/TypeaheadController.php b/src/Controller/TypeaheadController.php index 95480329..f7e15b6d 100644 --- a/src/Controller/TypeaheadController.php +++ b/src/Controller/TypeaheadController.php @@ -22,38 +22,43 @@ declare(strict_types=1); namespace App\Controller; -use App\Entity\Parameters\AbstractParameter; -use App\Settings\MiscSettings\IpnSuggestSettings; -use Symfony\Component\HttpFoundation\Response; use App\Entity\Attachments\Attachment; -use App\Entity\Parts\Category; -use App\Entity\Parts\Footprint; +use App\Entity\Parameters\AbstractParameter; use App\Entity\Parameters\AttachmentTypeParameter; use App\Entity\Parameters\CategoryParameter; -use App\Entity\Parameters\ProjectParameter; use App\Entity\Parameters\FootprintParameter; use App\Entity\Parameters\GroupParameter; use App\Entity\Parameters\ManufacturerParameter; use App\Entity\Parameters\MeasurementUnitParameter; use App\Entity\Parameters\PartParameter; +use App\Entity\Parameters\ProjectParameter; use App\Entity\Parameters\StorageLocationParameter; use App\Entity\Parameters\SupplierParameter; +use App\Entity\Parts\Category; +use App\Entity\Parts\Footprint; use App\Entity\Parts\Part; use App\Entity\PriceInformations\Currency; use App\Repository\ParameterRepository; +use App\Services\AI\AIPlatformRegistry; +use App\Services\AI\AIPlatforms; use App\Services\Attachments\AttachmentURLGenerator; use App\Services\Attachments\BuiltinAttachmentsFinder; use App\Services\Attachments\PartPreviewGenerator; use App\Services\Tools\TagFinder; +use App\Settings\MiscSettings\IpnSuggestSettings; use Doctrine\ORM\EntityManagerInterface; +use Symfony\AI\Platform\Capability; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Asset\Packages; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Serializer\Encoder\JsonEncoder; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Component\Serializer\Serializer; +use Symfony\Contracts\Cache\CacheInterface; +use Symfony\Contracts\Cache\ItemInterface; /** * In this controller the endpoints for the typeaheads are collected. @@ -121,9 +126,12 @@ class TypeaheadController extends AbstractController } #[Route(path: '/parts/search/{query}', name: 'typeahead_parts')] - public function parts(EntityManagerInterface $entityManager, PartPreviewGenerator $previewGenerator, - AttachmentURLGenerator $attachmentURLGenerator, string $query = ""): JsonResponse - { + public function parts( + EntityManagerInterface $entityManager, + PartPreviewGenerator $previewGenerator, + AttachmentURLGenerator $attachmentURLGenerator, + string $query = "" + ): JsonResponse { $this->denyAccessUnlessGranted('@parts.read'); $repo = $entityManager->getRepository(Part::class); @@ -134,7 +142,7 @@ class TypeaheadController extends AbstractController foreach ($parts as $part) { //Determine the picture to show: $preview_attachment = $previewGenerator->getTablePreviewAttachment($part); - if($preview_attachment instanceof Attachment) { + if ($preview_attachment instanceof Attachment) { $preview_url = $attachmentURLGenerator->getThumbnailURL($preview_attachment, 'thumbnail_sm'); } else { $preview_url = ''; @@ -148,7 +156,7 @@ class TypeaheadController extends AbstractController 'footprint' => $part->getFootprint() instanceof Footprint ? $part->getFootprint()->getName() : '', 'description' => mb_strimwidth($part->getDescription(), 0, 127, '...'), 'image' => $preview_url, - ]; + ]; } return new JsonResponse($data); @@ -219,8 +227,36 @@ class TypeaheadController extends AbstractController $partRepository = $entityManager->getRepository(Part::class); - $ipnSuggestions = $partRepository->autoCompleteIpn($clonedPart, $description, $this->ipnSuggestSettings->suggestPartDigits); + $ipnSuggestions = $partRepository->autoCompleteIpn($clonedPart, $description, + $this->ipnSuggestSettings->suggestPartDigits); return new JsonResponse($ipnSuggestions); } + + #[Route(path: '/ai/{platform}/models', name: 'typeahead_ai_models', requirements: ['platform' => '.+'])] + public function aiModels( + AIPlatforms $platform, + Request $request, + AIPlatformRegistry $platformRegistry, + CacheInterface $cache, + ): JsonResponse { + $this->denyAccessUnlessGranted('@config.change_system_settings'); + + $capability_filter = $request->query->getEnum('capability', Capability::class); + + $models = $cache->get('ai_models_'.$platform->value.'_'.($capability_filter->value ?? 'all'), + function (ItemInterface $item) use ($platformRegistry, $platform, $capability_filter) { + $item->expiresAfter(3600); //Cache for 1 hour + if ($capability_filter === null) { + return $platformRegistry->getPlatform($platform)->getModelCatalog()->getModels(); + } + + //Otherwise filter the models by the capability + return array_filter($platformRegistry->getPlatform($platform)->getModelCatalog()->getModels(), + static fn(array $model) => in_array($capability_filter, $model['capabilities'], true) + ); + }); + + return new JsonResponse($models); + } } diff --git a/src/Form/InfoProviderSystem/FromURLFormType.php b/src/Form/InfoProviderSystem/FromURLFormType.php new file mode 100644 index 00000000..39ef50f4 --- /dev/null +++ b/src/Form/InfoProviderSystem/FromURLFormType.php @@ -0,0 +1,87 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Form\InfoProviderSystem; + +use App\Services\InfoProviderSystem\ProviderRegistry; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\CheckboxType; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\Extension\Core\Type\SubmitType; +use Symfony\Component\Form\Extension\Core\Type\UrlType; +use Symfony\Component\Form\FormBuilderInterface; + +class FromURLFormType extends AbstractType +{ + public function __construct(private readonly ProviderRegistry $providerRegistry) + { + + } + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder->add('url', UrlType::class, [ + 'label' => 'info_providers.from_url.url.label', + 'required' => true, + ]); + + + $builder->add('method', ChoiceType::class, [ + 'expanded' => true, + 'data' => 'generic_web', //Default value + 'label' => 'info_providers.from_url.method', + 'choices' => [ + 'info_providers.from_url.method.generic_web' => 'generic_web', + 'info_providers.from_url.method.ai_web' => 'ai_web', + ], + 'choice_attr' => function ($choice, $key, $value) { + //Disable all providers that are not active + $provider = $this->providerRegistry->getProviderByKey($value); + if (!$provider->isActive()) { + return ['disabled' => 'disabled']; + } + + return []; + }, + + //Render the choices as inline radio buttons + 'label_attr' => [ + 'class' => 'radio-inline', + ], + ]); + + $builder->add('no_cache', CheckboxType::class, [ + 'label' => 'info_providers.from_url.no_cache', + 'required' => false, + ]); + + $builder->add('skip_delegation', CheckboxType::class, [ + 'label' => 'info_providers.from_url.skip_delegation', + 'required' => false, + ]); + + $builder->add('submit', SubmitType::class, [ + 'label' => 'info_providers.search.submit', + ]); + } +} diff --git a/src/Form/InfoProviderSystem/PartSearchType.php b/src/Form/InfoProviderSystem/PartSearchType.php index 9d582ca4..154c1aa3 100644 --- a/src/Form/InfoProviderSystem/PartSearchType.php +++ b/src/Form/InfoProviderSystem/PartSearchType.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace App\Form\InfoProviderSystem; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\SearchType; use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\FormBuilderInterface; @@ -40,8 +41,17 @@ class PartSearchType extends AbstractType 'help' => 'info_providers.search.providers.help', ]); + $builder->add('no_cache_search', CheckboxType::class, [ + 'label' => 'info_providers.no_cache_search', + 'required' => false, + ]); + $builder->add('no_cache_details', CheckboxType::class, [ + 'label' => 'info_providers.no_cache_details', + 'required' => false, + ]); + $builder->add('submit', SubmitType::class, [ 'label' => 'info_providers.search.submit' ]); } -} \ No newline at end of file +} diff --git a/src/Form/Settings/AiModelsType.php b/src/Form/Settings/AiModelsType.php new file mode 100644 index 00000000..5228bb47 --- /dev/null +++ b/src/Form/Settings/AiModelsType.php @@ -0,0 +1,72 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Form\Settings; + +use Symfony\AI\Platform\Capability; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; + +/** + * An text input with autocomplete for AI models from the given platform. + * The platform is determined by the value of another form field, which is specified by the "platform_selector" option. This allows to filter the available models based on the selected platform. + */ +final class AiModelsType extends AbstractType +{ + public function __construct(private readonly UrlGeneratorInterface $urlGenerator) + { + } + + public function getParent(): string + { + return TextType::class; + } + + public function configureOptions(OptionsResolver $resolver): void + { + //The target label of the platform select, which is used to filter the models for the selected platform. + $resolver->setRequired('platform_selector'); + $resolver->setAllowedTypes('platform_selector', 'string'); + + //Only show models, that have the given capability. This is used to only show models that support structured output for the AI extractor settings. + $resolver->setDefault('filter_capability', null); + $resolver->setAllowedTypes('filter_capability', ['null', Capability::class]); + } + + public function finishView(FormView $view, FormInterface $form, array $options): void + { + $urlOptions = ['platform' => '__PLATFORM__']; + if ($options['filter_capability'] !== null) { + $urlOptions['capability'] = $options['filter_capability']->value; + } + + $view->vars['attr']['data-url-template'] = $this->urlGenerator->generate('typeahead_ai_models', $urlOptions); + $view->vars['attr']['data-controller'] = 'elements--ai-model-autocomplete'; + + $view->vars['attr']['data-platform-selector'] = $options['platform_selector']; + } +} diff --git a/src/Form/Settings/AiPlatformChoiceType.php b/src/Form/Settings/AiPlatformChoiceType.php new file mode 100644 index 00000000..82ea66b2 --- /dev/null +++ b/src/Form/Settings/AiPlatformChoiceType.php @@ -0,0 +1,65 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Form\Settings; + +use App\Services\AI\AIPlatformRegistry; +use App\Services\AI\AIPlatforms; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\Extension\Core\Type\EnumType; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\OptionsResolver\OptionsResolver; + +/** + * Allow to choose an AI platform from the enabled platforms in the system. This is used in the settings to choose the default platform for AI features. + */ +final class AiPlatformChoiceType extends AbstractType +{ + public function __construct(private readonly AIPlatformRegistry $platformRegistry) + { + } + + public function getParent(): string + { + return EnumType::class; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $choices = array_map(static fn(string $val) => AIPlatforms::from($val), array_keys($this->platformRegistry->getEnabledPlatforms())); + + $resolver->setDefaults([ + 'class' => AIPlatforms::class, + 'choices' => $choices, + 'required' => false, + 'platform_selector_label' => null + ]); + } + + public function finishView(FormView $view, FormInterface $form, array $options): void + { + $view->vars['attr']['data-platform-selector-label'] = $options['platform_selector_label'] ?? $view->vars['id'].'_label'; + } +} diff --git a/src/Services/AI/AIPlatformRegistry.php b/src/Services/AI/AIPlatformRegistry.php new file mode 100644 index 00000000..408bb181 --- /dev/null +++ b/src/Services/AI/AIPlatformRegistry.php @@ -0,0 +1,94 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\AI; + +use Jbtronics\SettingsBundle\Manager\SettingsManagerInterface; +use Symfony\AI\Platform\PlatformInterface; +use Symfony\Component\DependencyInjection\Attribute\AutowireIterator; + +final readonly class AIPlatformRegistry +{ + /** + * All registered platforms, indexed by their service tag name (e.g. "openrouter", "lmstudio") + * @var array $allPlatforms + */ + private array $allPlatforms; + + /** + * All registered platforms, indexed by their AIPlatforms enum value (e.g. AIPlatforms::OPENROUTER->value) + * @var array $enabledPlatforms + */ + private array $enabledPlatforms; + + public function __construct( + SettingsManagerInterface $settingsManager, + #[AutowireIterator(tag: 'ai.platform', indexAttribute: 'name')] + iterable $platforms, + ) { + $this->allPlatforms = iterator_to_array($platforms); + + //Check which platforms are active based on the settings and store them in $activePlatforms + $tmp = []; + foreach (AIPlatforms::cases() as $platform) { + if (isset($this->allPlatforms[$platform->toServiceTagName()])) { + //Check if the platform is active by calling its isActive() on the settings class + $settings = $settingsManager->get($platform->toSettingsClass()); + if (!$settings->isAIPlatformEnabled()) { + continue; + } + + $tmp[$platform->value] = $this->allPlatforms[$platform->toServiceTagName()]; + } + } + $this->enabledPlatforms = $tmp; + } + + public function getPlatform(AIPlatforms $platform): PlatformInterface + { + if (!isset($this->enabledPlatforms[$platform->value])) { + throw new \InvalidArgumentException(sprintf('AI platform "%s" is not active or does not exist.', $platform->name)); + } + + return $this->enabledPlatforms[$platform->value]; + } + + /** + * Check if the given platform is active (i.e. it is registered and its settings are properly configured) + * @param AIPlatforms $platform + * @return bool + */ + public function isEnabled(AIPlatforms $platform): bool + { + return isset($this->enabledPlatforms[$platform->value]); + } + + /** + * Returns an array of all active platforms, indexed by their AIPlatforms enum value (e.g. AIPlatforms::OPENROUTER->value) + * @return PlatformInterface[] + */ + public function getEnabledPlatforms(): array + { + return $this->enabledPlatforms; + } +} diff --git a/src/Services/AI/AIPlatformSettingsInterface.php b/src/Services/AI/AIPlatformSettingsInterface.php new file mode 100644 index 00000000..c400db46 --- /dev/null +++ b/src/Services/AI/AIPlatformSettingsInterface.php @@ -0,0 +1,33 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\AI; + +interface AIPlatformSettingsInterface +{ + /** + * Returns true, if the AI platform is enabled in the settings and can be used, false otherwise. + * @return bool + */ + public function isAIPlatformEnabled(): bool; +} diff --git a/src/Services/AI/AIPlatforms.php b/src/Services/AI/AIPlatforms.php new file mode 100644 index 00000000..2f4d6317 --- /dev/null +++ b/src/Services/AI/AIPlatforms.php @@ -0,0 +1,64 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\AI; + +use App\Settings\AISettings\LMStudioSettings; +use App\Settings\AISettings\OpenRouterSettings; +use Symfony\Contracts\Translation\TranslatableInterface; +use Symfony\Contracts\Translation\TranslatorInterface; + +enum AIPlatforms: string implements TranslatableInterface +{ + case OPENROUTER = 'openrouter'; + case LMSTUDIO = 'lmstudio'; + + /** + * Returns the name attribute of the service tag for this platform, which is used to register the platform in the AIPlatformRegistry + * @return string + */ + public function toServiceTagName(): string + { + return $this->value; + } + + /** + * Returns the class name of the settings class for this platform, which implements AIPlatformSettingsInterface + * @return string + * @phpstan-return class-string + */ + public function toSettingsClass(): string + { + return match ($this) { + self::LMSTUDIO => LMStudioSettings::class, + self::OPENROUTER => OpenRouterSettings::class, + }; + } + + public function trans(TranslatorInterface $translator, ?string $locale = null): string + { + $key = 'settings.ai.' . $this->value; + + return $translator->trans($key, locale: $locale); + } +} diff --git a/src/Services/AI/AcceptAllModelsCatalog.php b/src/Services/AI/AcceptAllModelsCatalog.php new file mode 100644 index 00000000..a2f5c33a --- /dev/null +++ b/src/Services/AI/AcceptAllModelsCatalog.php @@ -0,0 +1,61 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\AI; + +use Symfony\AI\Platform\Bridge\Generic\CompletionsModel; +use Symfony\AI\Platform\Exception\ModelNotFoundException; +use Symfony\AI\Platform\Model; +use Symfony\AI\Platform\ModelCatalog\ModelCatalogInterface; +use Symfony\Component\DependencyInjection\Attribute\AsDecorator; + +/** + * This is a wrapper, to allow accepting all models, even if they are not contained in the decorated ModelCatalogInterface. + * This is a workaround for outdated/incomplete model catalogs provided by AI platforms, which do not contain all available models, or do not update their catalogs frequently enough. + */ +#[AsDecorator('ai.platform.model_catalog.lmstudio')] +#[AsDecorator('ai.platform.model_catalog.openrouter')] +final readonly class AcceptAllModelsCatalog implements ModelCatalogInterface +{ + + public function __construct(private ModelCatalogInterface $decorated) + { + } + + public function getModel(string $modelName): Model + { + //Use the actual values when its available. + try { + return $this->decorated->getModel($modelName); + } catch (ModelNotFoundException $e) { + //If the model is not found, return a generic model with the given name and no capabilities. + return new CompletionsModel($modelName, []); + } + } + + public function getModels(): array + { + //Return the actual models catalog here for correct autocompletition + return $this->decorated->getModels(); + } +} diff --git a/src/Services/InfoProviderSystem/CreateFromUrlHelper.php b/src/Services/InfoProviderSystem/CreateFromUrlHelper.php new file mode 100644 index 00000000..0291142f --- /dev/null +++ b/src/Services/InfoProviderSystem/CreateFromUrlHelper.php @@ -0,0 +1,109 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\InfoProviderSystem; + +use App\Entity\UserSystem\User; +use App\Exceptions\ProviderIDNotSupportedException; +use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; +use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; +use App\Services\InfoProviderSystem\Providers\InfoProviderInterface; +use Symfony\Bundle\SecurityBundle\Security; + +final readonly class CreateFromUrlHelper +{ + public function __construct(private Security $security, + private ProviderRegistry $providerRegistry, + private PartInfoRetriever $infoRetriever, + ) + { + } + + /** + * Checks if at least one provider can create parts from an URL and the current user is allowed to use it. + * This is used to determine if the "From URL" feature should be shown to the user. + * @return bool + */ + public function canCreateFromUrl(): bool + { + if (!$this->security->isGranted('@info_providers.create_parts')) { + return false; + } + + //Check if either the generic web provider or the ai web provider is active + $genericWebProvider = $this->providerRegistry->getProviderByKey('generic_web'); + $aiWebProvider = $this->providerRegistry->getProviderByKey('ai_web'); + + return $genericWebProvider->isActive() || $aiWebProvider->isActive(); + } + + /** + * Delegates the URL to another provider if possible, otherwise return null + * @param string $url + * @return SearchResultDTO|null + */ + public function delegateToOtherProvider(string $url, InfoProviderInterface $callingInfoProvider): ?SearchResultDTO + { + //Extract domain from url: + $host = parse_url($url, PHP_URL_HOST); + if ($host === false || $host === null) { + return null; + } + + $provider = $this->providerRegistry->getProviderHandlingDomain($host); + + if ($provider !== null && $provider->isActive() && $provider->getProviderKey() !== $callingInfoProvider->getProviderKey()) { + try { + $id = $provider->getIDFromURL($url); + if ($id !== null) { + $results = $this->infoRetriever->searchByKeyword($id, [$provider]); + if (count($results) > 0) { + return $results[0]; + } + } + return null; + } catch (ProviderIDNotSupportedException $e) { + //Ignore and continue + return null; + } + } + + return null; + } + + /** + * Delegates the URL to another provider if possible and returns the details, otherwise return null + * @param string $url + * @param InfoProviderInterface $callingInfoProvider + * @return PartDetailDTO|null + */ + public function delegateToOtherProviderDetails(string $url, InfoProviderInterface $callingInfoProvider): ?PartDetailDTO + { + $delegatedResult = $this->delegateToOtherProvider($url, $callingInfoProvider); + if ($delegatedResult !== null) { + return $this->infoRetriever->getDetailsForSearchResult($delegatedResult); + } + + return null; + } +} diff --git a/src/Services/InfoProviderSystem/DTOJsonSchemaConverter.php b/src/Services/InfoProviderSystem/DTOJsonSchemaConverter.php new file mode 100644 index 00000000..a61e7465 --- /dev/null +++ b/src/Services/InfoProviderSystem/DTOJsonSchemaConverter.php @@ -0,0 +1,252 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\InfoProviderSystem; + +use App\Entity\Parts\ManufacturingStatus; +use App\Services\InfoProviderSystem\DTOs\FileDTO; +use App\Services\InfoProviderSystem\DTOs\ParameterDTO; +use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; +use App\Services\InfoProviderSystem\DTOs\PriceDTO; +use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO; + +/** + * This class allows to convert the JSON data returned by an LLM into the DTOs used by the info provider system later. + */ +final class DTOJsonSchemaConverter +{ + /** + * Returns the JSON schema, that defines the expected structure of the JSON data returned by the LLM. + * @return array + */ + public function getJSONSchema(): array + { + return [ + 'name' => 'clock', + 'strict' => true, + 'schema' => [ + 'type' => 'object', + 'properties' => [ + 'name' => ['type' => 'string', 'description' => 'Product name'], + 'description' => ['type' => 'string', 'description' => 'A short description of the product, maybe containing the most important things. Onnly One line.'], + 'manufacturer' => ['type' => ['string', 'null'], 'description' => 'Manufacturer name'], + 'mpn' => ['type' => ['string', 'null'], 'description' => 'Manufacturer Part Number'], + 'category' => ['type' => ['string', 'null'], 'description' => 'Product category, e.g. "Passive components -> Resistors"'], + 'manufacturing_status' => ['type' => ['string', 'null'], 'enum' => ['active', 'obsolete', 'nrfnd', 'discontinued', null], 'description' => 'Manufacturing status'], + 'footprint' => ['type' => ['string', 'null'], 'description' => 'Package/footprint type, like "SOT-23", "DIP-8", "QFN-32" etc.'], + 'mass' => ['type' => ['number', 'null'], 'description' => 'Mass of the product in grams'], + 'gtin' => ['type' => ['string', 'null'], 'description' => 'Global Trade Item Number (GTIN) / EAN / UPC code for barcodes'], + 'notes' => ['type' => ['string', 'null'], 'description' => 'Optional long description of the part with more details than description. Can be markdown formatted.'], + 'parameters' => [ + 'type' => 'array', + 'items' => [ + 'type' => 'object', + 'properties' => [ + 'name' => ['type' => 'string'], + 'symbol' => ['type' => ['string', 'null'], 'description' => 'An optional quantity symbol for the parameter in latex code, like R_1'], + 'value_typical' => ['type' => ['number', 'null'], 'description' => 'The typical value of the parameter. For example, for a resistor this could be 100 for a 100 Ohm resistor. Also used if only one numeric value is given. If used an unit should be given'], + 'value_min' => ['type' => ['number', 'null'], 'description' => 'If a range is given for the parameter, this is the minimum value. Null if no range is given.'], + 'value_max' => ['type' => ['number', 'null'], 'description' => 'If a range is given for the parameter, this is the maximum value. Null if not a range.'], + 'value_text' => ['type' => ['string', 'null'], 'description' => 'When a value is not numeric it can be put here as text. Only use if it does not fit in value_min, value_typical or value_max. E.g. "Yes", "Red", etc.'], + 'group' => ['type' => ['string', 'null'], 'description' => 'An optional group name for the parameter, e.g. "Electrical parameters", "Mechanical parameters" etc.'], + 'unit' => ['type' => ['string', 'null'], 'description' => 'The unit of the parameter values, e.g. kg, Ohm, V, etc.'], + ], + 'required' => ['name', 'value_typical', 'value_min', 'value_max', 'value_text'] + ], + ], + 'datasheets' => [ + 'description' => 'A list of datasheets, manuals, or other technical documents related to the product. Not images, but actual documents, preferably PDFs.', + 'type' => 'array', + 'items' => [ + 'type' => 'object', + 'properties' => [ + 'url' => ['type' => 'string'], + 'description' => ['type' => 'string'], + ], + 'required' => ['url'], + ], + ], + 'images' => [ + 'type' => 'array', + 'items' => [ + 'type' => 'object', + 'properties' => [ + 'url' => ['type' => 'string'], + 'description' => ['type' => 'string'], + ], + 'required' => ['url'], + ], + ], + 'vendor_infos' => [ + 'type' => 'array', + 'items' => [ + 'type' => 'object', + 'properties' => [ + 'distributor_name' => ['type' => 'string', 'description' => 'Name of the distributor or vendor. Typically the shop name'], + 'order_number' => ['type' => ['string', 'null'], 'description' => 'The order number or SKU used by the distributor. Optional, but can help to find the product on the distributor website.'], + 'product_url' => ['type' => 'string'], + 'prices_include_vat' => ['type' => ['boolean', 'null'], 'description' => 'Whether the prices include VAT or not. Null if unknown.'], + 'prices' => [ + 'type' => 'array', + 'items' => [ + 'type' => 'object', + 'properties' => [ + 'minimum_quantity' => ['type' => 'integer', 'description' => 'Minimum quantity for this price tier. 1 when no tiered pricing is available.'], + 'price' => ['type' => 'number', 'description' => 'Price for the given minimum quantity.'], + 'currency' => ['type' => 'string', 'description' => 'Currency ISO code, e.g. USD'], + ], + 'required' => ['minimum_quantity', 'price', 'currency'], + ], + ], + ], + 'required' => ['distributor_name', 'product_url'], + ], + ], + 'manufacturer_product_url' => ['type' => ['string', 'null'], 'description' => 'Manufacturer product page URL'], + ], + 'required' => ['name', 'description'], + ] + ]; + } + + public function jsonToDTO(array $data, string $providerKey, string $providerId, ?string $productUrl = null, string $distributorNameFallback = '???'): PartDetailDTO + { + // Map manufacturing status + $manufacturingStatus = null; + if (!empty($data['manufacturing_status'])) { + $status = strtolower((string) $data['manufacturing_status']); + $manufacturingStatus = match ($status) { + 'active' => ManufacturingStatus::ACTIVE, + 'obsolete', 'discontinued' => ManufacturingStatus::DISCONTINUED, + 'nrfnd', 'not recommended for new designs' => ManufacturingStatus::NRFND, + 'eol' => ManufacturingStatus::EOL, + 'announced' => ManufacturingStatus::ANNOUNCED, + default => null, + }; + } + + // Build parameters + $parameters = null; + if (!empty($data['parameters']) && is_array($data['parameters'])) { + $parameters = []; + foreach ($data['parameters'] as $p) { + if (!empty($p['name'])) { + $parameters[] = new ParameterDTO( + name: $p['name'], + value_text: $p['value_text'] ?? null, + value_typ: isset($p['value_typical']) && is_numeric($p['value_typical']) ? (float) $p['value_typical'] : null, + value_min: isset($p['value_min']) && is_numeric($p['value_min']) ? (float) $p['value_min'] : null, + value_max: isset($p['value_max']) && is_numeric($p['value_max']) ? (float) $p['value_max'] : null, + unit: $p['unit'] ?? null, + symbol: $p['symbol'] ?? null, + group: $p['group'] ?? null, + ); + } + } + } + + // Build datasheets + $datasheets = null; + if (!empty($data['datasheets']) && is_array($data['datasheets'])) { + $datasheets = []; + foreach ($data['datasheets'] as $d) { + if (!empty($d['url'])) { + $datasheets[] = new FileDTO( + url: $d['url'], + name: $d['description'] ?? 'Datasheet' + ); + } + } + } + + // Build images + $images = null; + if (!empty($data['images']) && is_array($data['images'])) { + $images = []; + foreach ($data['images'] as $i) { + if (!empty($i['url'])) { + $images[] = new FileDTO( + url: $i['url'], + name: $i['description'] ?? 'Image' + ); + } + } + } + + // Build vendor infos + $vendorInfos = null; + if (!empty($data['vendor_infos']) && is_array($data['vendor_infos'])) { + $vendorInfos = []; + foreach ($data['vendor_infos'] as $v) { + $prices = []; + if (!empty($v['prices']) && is_array($v['prices'])) { + foreach ($v['prices'] as $p) { + $prices[] = new PriceDTO( + minimum_discount_amount: (int) ($p['minimum_quantity'] ?? 1), + price: (string) ($p['price'] ?? 0), + currency_iso_code: $p['currency'] ?? null, + price_related_quantity: 1, + ); + } + } + + $vendorInfos[] = new PurchaseInfoDTO( + distributor_name: $v['distributor_name'] ?? $distributorNameFallback, + order_number: $v['order_number'] ?? 'Unknown', + prices: $prices, + product_url: $v['product_url'] ?? $productUrl, + prices_include_vat: $v['prices_include_vat'] ?? null, + ); + } + } + + // Get preview image URL + $previewImageUrl = null; + if (!empty($data['images']) && is_array($data['images']) && !empty($data['images'][0]['url'])) { + $previewImageUrl = $data['images'][0]['url']; + } + + return new PartDetailDTO( + provider_key: $providerKey, + provider_id: $providerId, + name: $data['name'] ?? 'Unknown', + description: $data['description'] ?? '', + category: $data['category'] ?? null, + manufacturer: $data['manufacturer'] ?? null, + mpn: $data['mpn'] ?? null, + preview_image_url: $previewImageUrl, + manufacturing_status: $manufacturingStatus, + provider_url: $productUrl, + footprint: $data['footprint'] ?? null, + gtin: $data['gtin'] ?? null, + notes: $data['notes'] ?? null, + datasheets: $datasheets, + images: $images, + parameters: $parameters, + vendor_infos: $vendorInfos, + mass: isset($data['mass']) && is_numeric($data['mass']) ? (float) $data['mass'] : null, + manufacturer_product_url: $data['manufacturer_product_url'] ?? null, + ); + } + +} diff --git a/src/Services/InfoProviderSystem/PartInfoRetriever.php b/src/Services/InfoProviderSystem/PartInfoRetriever.php index db1895e7..6c10f10e 100644 --- a/src/Services/InfoProviderSystem/PartInfoRetriever.php +++ b/src/Services/InfoProviderSystem/PartInfoRetriever.php @@ -53,6 +53,7 @@ final class PartInfoRetriever * Search for a keyword in the given providers. The results can be cached * @param string[]|InfoProviderInterface[] $providers A list of providers to search in, either as provider keys or as provider instances * @param string $keyword The keyword to search for + * @param array $options An associative array of options which can be used to modify the search behavior. The supported options depend on the provider and should be documented in the provider's documentation. * @return SearchResultDTO[] The search results * @throws InfoProviderNotActiveException if any of the given providers is not active * @throws ClientException if any of the providers throws an exception during the search @@ -60,7 +61,7 @@ final class PartInfoRetriever * @throws TransportException if any of the providers throws an exception during the search * @throws OAuthReconnectRequiredException if any of the providers throws an exception during the search that indicates that the OAuth token needs to be refreshed */ - public function searchByKeyword(string $keyword, array $providers): array + public function searchByKeyword(string $keyword, array $providers, array $options = []): array { $results = []; @@ -79,7 +80,7 @@ final class PartInfoRetriever } /** @noinspection SlowArrayOperationsInLoopInspection */ - $results = array_merge($results, $this->searchInProvider($provider, $keyword)); + $results = array_merge($results, $this->searchInProvider($provider, $keyword, $options)); } return $results; @@ -89,15 +90,31 @@ final class PartInfoRetriever * Search for a keyword in the given provider. The result is cached for 7 days. * @return SearchResultDTO[] */ - protected function searchInProvider(InfoProviderInterface $provider, string $keyword): array + protected function searchInProvider(InfoProviderInterface $provider, string $keyword, array $options = []): array { //Generate key and escape reserved characters from the provider id $escaped_keyword = hash('xxh3', $keyword); - return $this->partInfoCache->get("search_{$provider->getProviderKey()}_{$escaped_keyword}", function (ItemInterface $item) use ($provider, $keyword) { + + $no_cache = $options[InfoProviderInterface::OPTION_NO_CACHE] ?? false; + + //Exclude the no_cache option from the options hash, since it should not affect the cache key, as it only determines whether to bypass the cache or not, but does not change the actual search results + $options_without_cache = $options; + unset($options_without_cache[InfoProviderInterface::OPTION_NO_CACHE]); + //Generate a hash for the options, to ensure that different options result in different cache entries + $options_hash = hash('xxh3', json_encode($options_without_cache, JSON_THROW_ON_ERROR)); + + $cache_key = "search_{$provider->getProviderKey()}_{$escaped_keyword}_{$options_hash}"; + + //If no_cache is set, bypass the cache and get fresh results from the provider + if ($no_cache) { + $this->partInfoCache->delete($cache_key); + } + + return $this->partInfoCache->get($cache_key, function (ItemInterface $item) use ($provider, $keyword, $options) { //Set the expiration time $item->expiresAfter(!$this->debugMode ? self::CACHE_RESULT_EXPIRATION : 10); - return $provider->searchByKeyword($keyword); + return $provider->searchByKeyword($keyword, $options); }); } @@ -106,10 +123,11 @@ final class PartInfoRetriever * The result is cached for 4 days. * @param string $provider_key * @param string $part_id + * @param array $options An associative array of options which can be used to modify the search behavior. The supported options depend on the provider and should be documented in the provider's documentation. * @return PartDetailDTO * @throws InfoProviderNotActiveException if the the given providers is not active */ - public function getDetails(string $provider_key, string $part_id): PartDetailDTO + public function getDetails(string $provider_key, string $part_id, array $options = []): PartDetailDTO { $provider = $this->provider_registry->getProviderByKey($provider_key); @@ -118,13 +136,26 @@ final class PartInfoRetriever throw InfoProviderNotActiveException::fromProvider($provider); } + //Exclude the no_cache option from the options hash, since it should not affect the cache key, as it only determines whether to bypass the cache or not, but does not change the actual search results + $options_without_cache = $options; + unset($options_without_cache[InfoProviderInterface::OPTION_NO_CACHE]); + //Generate a hash for the options, to ensure that different options result in different cache entries + $options_hash = hash('xxh3', json_encode($options_without_cache, JSON_THROW_ON_ERROR)); + //Generate key and escape reserved characters from the provider id $escaped_part_id = hash('xxh3', $part_id); - return $this->partInfoCache->get("details_{$provider_key}_{$escaped_part_id}", function (ItemInterface $item) use ($provider, $part_id) { + $cache_key = "details_{$provider_key}_{$escaped_part_id}_{$options_hash}"; + + //Delete the cache entry if no_cache is set, to ensure that the next get call will fetch fresh data from the provider, instead of returning stale data from the cache. + if ($options[InfoProviderInterface::OPTION_NO_CACHE] ?? false) { + $this->partInfoCache->delete($cache_key); + } + + return $this->partInfoCache->get($cache_key, function (ItemInterface $item) use ($provider, $part_id, $options) { //Set the expiration time $item->expiresAfter(!$this->debugMode ? self::CACHE_DETAIL_EXPIRATION : 10); - return $provider->getDetails($part_id); + return $provider->getDetails($part_id, $options); }); } diff --git a/src/Services/InfoProviderSystem/Providers/AIWebProvider.php b/src/Services/InfoProviderSystem/Providers/AIWebProvider.php new file mode 100644 index 00000000..8fb7e4ec --- /dev/null +++ b/src/Services/InfoProviderSystem/Providers/AIWebProvider.php @@ -0,0 +1,282 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\InfoProviderSystem\Providers; + +use App\Exceptions\ProviderIDNotSupportedException; +use App\Helpers\RandomizeUseragentHttpClient; +use App\Services\AI\AIPlatformRegistry; +use App\Services\InfoProviderSystem\CreateFromUrlHelper; +use App\Services\InfoProviderSystem\DTOJsonSchemaConverter; +use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; +use App\Settings\InfoProviderSystem\AIExtractorSettings; +use Brick\Schema\SchemaReader; +use Jkphl\Micrometa; +use League\HTMLToMarkdown\HtmlConverter; +use Psr\Cache\CacheItemPoolInterface; +use Symfony\AI\Platform\Message\Message; +use Symfony\AI\Platform\Message\MessageBag; +use Symfony\Component\DomCrawler\Crawler; +use Symfony\Component\HttpClient\NoPrivateNetworkHttpClient; +use Symfony\Component\Intl\Languages; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +use function Symfony\Component\String\u; + + +final class AIWebProvider implements InfoProviderInterface +{ + use FixAndValidateUrlTrait; + + private const DISTRIBUTOR_NAME = 'Website'; + + private readonly HttpClientInterface $httpClient; + + public function __construct( + HttpClientInterface $httpClient, + private readonly AIExtractorSettings $settings, + private readonly AIPlatformRegistry $AIPlatformRegistry, + private readonly DTOJsonSchemaConverter $jsonSchemaConverter, + private readonly CacheItemPoolInterface $partInfoCache, + private readonly CreateFromUrlHelper $createFromUrlHelper, + ) { + //Use NoPrivateNetworkHttpClient to prevent SSRF vulnerabilities, and RandomizeUseragentHttpClient to make it harder for servers to block us + $this->httpClient = (new RandomizeUseragentHttpClient(new NoPrivateNetworkHttpClient($httpClient)))->withOptions( + [ + 'timeout' => 15, + ] + ); + } + + public function getProviderInfo(): array + { + return [ + 'name' => 'AI Web Extractor', + 'description' => 'Extract part info from any URL using LLM', + //'url' => 'https://openrouter.ai', + 'disabled_help' => 'Configure AI settings', + 'settings_class' => AIExtractorSettings::class, + ]; + } + + public function getProviderKey(): string + { + return 'ai_web'; + } + + public function isActive(): bool + { + return $this->settings->platform !== null && $this->settings->model !== null && $this->settings->model !== ''; + } + + public function searchByKeyword(string $keyword, array $options = []): array + { + $url = $this->fixAndValidateURL($keyword); + + if (!($options[self::OPTION_SKIP_DELEGATION] ?? false)) { + //Before loading the page, try to delegate to another provider + $delegatedPart = $this->createFromUrlHelper->delegateToOtherProvider($url, $this); + if ($delegatedPart !== null) { + return [$delegatedPart]; + } + } + + try { + + $new_options = $options; + $new_options[self::OPTION_SKIP_DELEGATION] = true; //Skip delegation for the getDetails call to prevent infinite loops + + return [ + $this->getDetails($keyword, $new_options) + ]; } catch (ProviderIDNotSupportedException $e) { + return []; + } + } + + public function getDetails(string $id, array $options = []): PartDetailDTO + { + $url = $this->fixAndValidateURL($id); + + if (!($options[self::OPTION_SKIP_DELEGATION] ?? false)) { + //Before loading the page, try to delegate to another provider + $delegatedPart = $this->createFromUrlHelper->delegateToOtherProviderDetails($url, $this); + if ($delegatedPart !== null) { + return $delegatedPart; + } + } + + //Check if we have a cached result for this URL, to avoid unnecessary LLM calls, which can be slow and costly. + $cacheKey = 'ai_web_'.hash('xxh3', $url); + + //If ignore cache option is set, skip cache and fetch fresh data + if ($options[self::OPTION_NO_CACHE] ?? false) { + $this->partInfoCache->deleteItem($cacheKey); + } + + //Return cached result if available + $cacheItem = $this->partInfoCache->getItem($cacheKey); + if ($cacheItem->isHit()) { + return $cacheItem->get(); + } + + // Fetch HTML content + $response = $this->httpClient->request('GET', $url); + $html = $response->getContent(); + + //Convert html to markdown, to provide a cleaner input to the LLM. + $markdown = $this->htmlToMarkdown($html); + //Truncate markdown to max content length, if needed + $markdown = u($markdown)->truncate($this->settings->maxContentLength, '... [truncated]')->toString(); + + //Extract structured data using traditional methods, to provide additional context to the LLM. This can help improve accuracy, especially for technical specifications that might be in tables or specific formats. + $structuredData = $this->extractStructuredData($html, $url); + + // Call LLM + $llmResponse = $this->callLLM($markdown, $url, $structuredData); + + // Build and return PartDetailDTO + $result = $this->jsonSchemaConverter->jsonToDTO($llmResponse, $this->getProviderKey(), $url, $url, self::DISTRIBUTOR_NAME); + + // Cache the result for future use, to improve performance and reduce costs. + $cacheItem->set($result); + $cacheItem->expiresAfter(3600 * 2); //Cache for 2 hours, as web content can change frequently, but we still want to benefit from caching for repeated accesses. + $this->partInfoCache->save($cacheItem); + + return $result; + } + + /** + * Extracts structured data from the HTML using microformats. + * @param string $html + * @param string $url + * @return string JSON encoded structured data + */ + private function extractStructuredData(string $html, string $url): string + { + $micrometa = new Micrometa\Ports\Parser(); + $items = $micrometa($url, $html); + + return json_encode($items->toObject(), JSON_THROW_ON_ERROR); + } + + private function htmlToMarkdown(string $html): string + { + //Extract only the main content of the page to avoid overwhelming the LLM with irrelevant information. + $crawler = new Crawler($html); + $mainContent = $crawler->filter('main, article, #content'); + + // If we found a specific content area, get its HTML; otherwise, use the whole body. + //Concat the html of all matched nodes, to provide more context to the LLM, especially for pages that use multiple sections for product info. + if ($mainContent->count() > 0) { + $htmlToConvert = ''; + foreach ($mainContent as $node) { + $htmlToConvert .= $node->ownerDocument->saveHTML($node); + $htmlToConvert .= "\n\n"; // Add some spacing between sections + } + } else { + //Use the whole body content, as it might contain relevant information, especially for simpler pages that don't have a clear main/content section. + $htmlToConvert = $html; + } + + + //Concert to markdown + $converter = new HtmlConverter([ + 'strip_tags' => true, // Removes tags that aren't Markdown-compatible (like
) + 'hard_break' => true, // Preserves line breaks + 'remove_nodes' => 'nav footer script style' // Extra safety layer + ]); + + return $converter->convert($htmlToConvert); + } + + public function getCapabilities(): array + { + return [ + ProviderCapabilities::BASIC, + ProviderCapabilities::PICTURE, + ProviderCapabilities::DATASHEET, + ProviderCapabilities::PRICE, + ProviderCapabilities::PARAMETERS, + ]; + } + + private function callLLM(string $htmlContent, string $url, ?string $structuredData = null): array + { + $input = new MessageBag( + Message::forSystem($this->buildSystemPrompt()), + Message::ofUser("Extract part information from this webpage content:\n\nURL: $url\n\n$htmlContent") + ); + + if ($structuredData) { + $input->add(Message::ofUser("Following data was extracted using traditional methods, but might be incomplete or inaccurate. + Enrich it with the actual website data:\n\n".$structuredData)); + } + + try { + $aiPlatform = $this->AIPlatformRegistry->getPlatform($this->settings->platform ?? throw new \RuntimeException('No AI platform selected') ); + + //'openai/gpt-5-mini' + $result = $aiPlatform->invoke($this->settings->model ?? throw new \RuntimeException('No model selected'), $input, [ + 'response_format' => [ + 'type' => 'json_schema', + 'json_schema' => $this->jsonSchemaConverter->getJSONSchema(), + ] + ]); + } catch (\Throwable $e) { + throw new \RuntimeException('LLM invocation failed: '.$e->getMessage(), previous: $e); + } + + return $result->getResult()->getContent(); + } + + private function buildSystemPrompt(): string + { + $tmp = <<<'PROMPT' +You are an expert at extracting electronic component information from web pages. Extract structured data in JSON format, from markdown extracted from a product page. +Focus on the main content of the page, such as product descriptions, specifications, and tables. Ignore navigation menus, footers, and sidebars. + +Rules: +- manufacturing_status: Use "active", "obsolete", "nrfnd" (not recommended for new designs), "discontinued", or null +- parameters: Extract technical specs like voltage, current, temperature, etc. and put them into the fields according to the JSON schema. Include units if available. +- prices: Extract pricing tiers with minimum_quantity, price, and currency code +- URLs must be absolute (include https://...) +- If information is not found, use null +- Try to avoid duplicating parameters, if the same parameter is mentioned multiple times, or if it is already used in another field. +- Include only the 1 to 3 most relevant images, such as the main product image or important diagrams. Ignore decorative images, logos, or icons. +PROMPT; + + if ($this->settings->outputLanguage === null) { + $tmp .= "\n\nProvide the response in the same language of the webpage."; + } else { + $tmp .= "\n\nThe response must be in ". Languages::getName($this->settings->outputLanguage, 'en') ." language. Translate texts if needed."; + } + + if ($this->settings->additionalInstructions) { + $tmp .= "\n\nAdditional instructions:\n" . $this->settings->additionalInstructions; + } + + return $tmp; + } + +} diff --git a/src/Services/InfoProviderSystem/Providers/BatchInfoProviderInterface.php b/src/Services/InfoProviderSystem/Providers/BatchInfoProviderInterface.php index 549f117a..cd918439 100644 --- a/src/Services/InfoProviderSystem/Providers/BatchInfoProviderInterface.php +++ b/src/Services/InfoProviderSystem/Providers/BatchInfoProviderInterface.php @@ -34,7 +34,8 @@ interface BatchInfoProviderInterface extends InfoProviderInterface * Search for multiple keywords in a single batch operation and return the results, ordered by the keywords. * This allows for a more efficient search compared to running multiple single searches. * @param string[] $keywords + * @param array $options An associative array of options which can be used to modify the search behavior. The supported options depend on the provider and should be documented in the provider's documentation. * @return array An associative array where the key is the keyword and the value is the search results for that keyword */ - public function searchByKeywordsBatch(array $keywords): array; + public function searchByKeywordsBatch(array $keywords, array $options = []): array; } diff --git a/src/Services/InfoProviderSystem/Providers/BuerklinProvider.php b/src/Services/InfoProviderSystem/Providers/BuerklinProvider.php index c2291107..ca6e26e1 100644 --- a/src/Services/InfoProviderSystem/Providers/BuerklinProvider.php +++ b/src/Services/InfoProviderSystem/Providers/BuerklinProvider.php @@ -120,7 +120,7 @@ class BuerklinProvider implements BatchInfoProviderInterface, URLHandlerInfoProv ]; } - private function getProduct(string $code): array + private function getProduct(string $code, bool $use_cache = true): array { $code = strtoupper(trim($code)); if ($code === '') { @@ -132,6 +132,11 @@ class BuerklinProvider implements BatchInfoProviderInterface, URLHandlerInfoProv md5($code . '|' . $this->settings->language . '|' . $this->settings->currency) ); + if (!$use_cache) { + $this->partInfoCache->deleteItem($cacheKey); + unset($this->productCache[$cacheKey]); + } + if (isset($this->productCache[$cacheKey])) { return $this->productCache[$cacheKey]; } @@ -461,9 +466,11 @@ class BuerklinProvider implements BatchInfoProviderInterface, URLHandlerInfoProv } /** + * @param string $keyword + * @param array $options * @return PartDetailDTO[] */ - public function searchByKeyword(string $keyword): array + public function searchByKeyword(string $keyword, array $options = []): array { $keyword = strtoupper(trim($keyword)); if ($keyword === '') { @@ -486,17 +493,18 @@ class BuerklinProvider implements BatchInfoProviderInterface, URLHandlerInfoProv // Fallback: try direct lookup by code try { - $product = $this->getProduct($keyword); + $product = $this->getProduct($keyword, use_cache: !($options[self::OPTION_NO_CACHE] ?? false)); return [$this->getPartDetail($product)]; } catch (\Throwable $e) { return []; } } - public function getDetails(string $id): PartDetailDTO + public function getDetails(string $id, array $options = []): PartDetailDTO { // Detail endpoint is /products/{code}/ - $response = $this->getProduct($id); + //By default use cache for details, but allow bypassing cache with option (e.g. for refresh) + $response = $this->getProduct($id, use_cache: !($options[self::OPTION_NO_CACHE] ?? false)); return $this->getPartDetail($response); } @@ -588,10 +596,11 @@ class BuerklinProvider implements BatchInfoProviderInterface, URLHandlerInfoProv } /** - * @param string[] $keywords + * @param array $keywords + * @param array $options * @return array */ - public function searchByKeywordsBatch(array $keywords): array + public function searchByKeywordsBatch(array $keywords, array $options = []): array { /** @var array $results */ $results = []; @@ -643,27 +652,27 @@ class BuerklinProvider implements BatchInfoProviderInterface, URLHandlerInfoProv public function getIDFromURL(string $url): ?string { - //Inputs: - //https://www.buerklin.com/de/p/bkl-electronic/niedervoltsteckverbinder/072341-l/40F1332/ + //Inputs: + //https://www.buerklin.com/de/p/bkl-electronic/niedervoltsteckverbinder/072341-l/40F1332/ //https://www.buerklin.com/de/p/40F1332/ //https://www.buerklin.com/en/p/bkl-electronic/dc-connectors/072341-l/40F1332/ //https://www.buerklin.com/en/p/40F1332/ //The ID is the last part after the manufacturer/category/mpn segment and before the final slash //https://www.buerklin.com/de/p/bkl-electronic/niedervoltsteckverbinder/072341-l/40F1332/#download should also work - + $path = parse_url($url, PHP_URL_PATH); - + if (!$path) { return null; } - + // Ensure it's actually a product URL if (strpos($path, '/p/') === false) { return null; } - + $id = basename(rtrim($path, '/')); - + return $id !== '' && $id !== 'p' ? $id : null; } diff --git a/src/Services/InfoProviderSystem/Providers/CanopyProvider.php b/src/Services/InfoProviderSystem/Providers/CanopyProvider.php index 18864a49..aee30d6b 100644 --- a/src/Services/InfoProviderSystem/Providers/CanopyProvider.php +++ b/src/Services/InfoProviderSystem/Providers/CanopyProvider.php @@ -111,7 +111,7 @@ class CanopyProvider implements InfoProviderInterface return null; } - public function searchByKeyword(string $keyword): array + public function searchByKeyword(string $keyword, array $options = []): array { $response = $this->httpClient->request('GET', self::SEARCH_API_URL, [ 'query' => [ @@ -177,15 +177,17 @@ class CanopyProvider implements InfoProviderInterface return new PurchaseInfoDTO(self::DISTRIBUTOR_NAME, order_number: $asin, prices: $priceDtos, product_url: $this->productPageFromASIN($asin)); } - public function getDetails(string $id): PartDetailDTO + public function getDetails(string $id, array $options = []): PartDetailDTO { //Check that the id is a valid ASIN (10 characters, letters and numbers) if (!preg_match('/^[A-Z0-9]{10}$/', $id)) { throw new \InvalidArgumentException("The id must be a valid ASIN (10 characters, letters and numbers)"); } + $do_not_cache = ($options[self::OPTION_NO_CACHE] ?? false) || $this->settings->alwaysGetDetails; + //Use cached details if available and the settings allow it, to avoid unnecessary API requests, since the search results already contain most of the details - if(!$this->settings->alwaysGetDetails && ($cached = $this->getFromCache($id)) !== null) { + if(!$do_not_cache && ($cached = $this->getFromCache($id)) !== null) { return $cached; } diff --git a/src/Services/InfoProviderSystem/Providers/ConradProvider.php b/src/Services/InfoProviderSystem/Providers/ConradProvider.php index 39de1e23..2e6708be 100644 --- a/src/Services/InfoProviderSystem/Providers/ConradProvider.php +++ b/src/Services/InfoProviderSystem/Providers/ConradProvider.php @@ -88,7 +88,7 @@ readonly class ConradProvider implements InfoProviderInterface, URLHandlerInfoPr return null; } - public function searchByKeyword(string $keyword): array + public function searchByKeyword(string $keyword, array $options = []): array { $url = $this->settings->shopID->getAPIRoot() . self::SEARCH_ENDPOINT . '/' . $this->settings->shopID->getDomainEnd() . '/' . $this->settings->shopID->getLanguage() @@ -279,7 +279,7 @@ readonly class ConradProvider implements InfoProviderInterface, URLHandlerInfoPr ); } - public function getDetails(string $id): PartDetailDTO + public function getDetails(string $id, array $options = []): PartDetailDTO { $productInfoURL = $this->settings->shopID->getAPIRoot() . '/product/1/service/' . $this->settings->shopID->getShopID() . '/product/' . $id; diff --git a/src/Services/InfoProviderSystem/Providers/DigikeyProvider.php b/src/Services/InfoProviderSystem/Providers/DigikeyProvider.php index d7eb6e4f..e7a62aa4 100644 --- a/src/Services/InfoProviderSystem/Providers/DigikeyProvider.php +++ b/src/Services/InfoProviderSystem/Providers/DigikeyProvider.php @@ -106,7 +106,7 @@ class DigikeyProvider implements InfoProviderInterface return $this->settings->clientId !== null && $this->settings->clientId !== '' && $this->authTokenManager->hasToken(self::OAUTH_APP_NAME); } - public function searchByKeyword(string $keyword): array + public function searchByKeyword(string $keyword, array $options = []): array { $request = [ 'Keywords' => $keyword, @@ -159,7 +159,7 @@ class DigikeyProvider implements InfoProviderInterface return $result; } - public function getDetails(string $id): PartDetailDTO + public function getDetails(string $id, array $options = []): PartDetailDTO { try { $response = $this->digikeyClient->request('GET', '/products/v4/search/' . urlencode($id) . '/productdetails', [ diff --git a/src/Services/InfoProviderSystem/Providers/Element14Provider.php b/src/Services/InfoProviderSystem/Providers/Element14Provider.php index 9ae45728..1d9e092c 100644 --- a/src/Services/InfoProviderSystem/Providers/Element14Provider.php +++ b/src/Services/InfoProviderSystem/Providers/Element14Provider.php @@ -282,12 +282,12 @@ class Element14Provider implements InfoProviderInterface, URLHandlerInfoProvider }; } - public function searchByKeyword(string $keyword): array + public function searchByKeyword(string $keyword, array $options = []): array { return $this->queryByTerm('any:' . $keyword); } - public function getDetails(string $id): PartDetailDTO + public function getDetails(string $id, array $options = []): PartDetailDTO { $tmp = $this->queryByTerm('id:' . $id); if (count($tmp) === 0) { diff --git a/src/Services/InfoProviderSystem/Providers/EmptyProvider.php b/src/Services/InfoProviderSystem/Providers/EmptyProvider.php index e0de9772..915a118c 100644 --- a/src/Services/InfoProviderSystem/Providers/EmptyProvider.php +++ b/src/Services/InfoProviderSystem/Providers/EmptyProvider.php @@ -54,7 +54,7 @@ class EmptyProvider implements InfoProviderInterface return true; } - public function searchByKeyword(string $keyword): array + public function searchByKeyword(string $keyword, array $options = []): array { return [ @@ -69,7 +69,7 @@ class EmptyProvider implements InfoProviderInterface ]; } - public function getDetails(string $id): PartDetailDTO + public function getDetails(string $id, array $options = []): PartDetailDTO { throw new \RuntimeException('No part details available'); } diff --git a/src/Services/InfoProviderSystem/Providers/FixAndValidateUrlTrait.php b/src/Services/InfoProviderSystem/Providers/FixAndValidateUrlTrait.php new file mode 100644 index 00000000..c9395a46 --- /dev/null +++ b/src/Services/InfoProviderSystem/Providers/FixAndValidateUrlTrait.php @@ -0,0 +1,58 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\InfoProviderSystem\Providers; + +use App\Exceptions\ProviderIDNotSupportedException; + +trait FixAndValidateUrlTrait +{ + private function fixAndValidateURL(string $url): string + { + $originalUrl = $url; + + //Add scheme if missing + if (!preg_match('/^https?:\/\//', $url)) { + //Remove any leading slashes + $url = ltrim($url, '/'); + + //If the URL starts with https:/ or http:/, add the missing slash + //Traefik removes the double slash as secruity measure, so we want to be forgiving and add it back if needed + //See https://github.com/Part-DB/Part-DB-server/issues/1296 + if (preg_match('/^https?:\/[^\/]/', $url)) { + $url = preg_replace('/^(https?:)\/([^\/])/', '$1//$2', $url); + } else { + $url = 'https://'.$url; + } + } + + //If this is not a valid URL with host, domain and path, throw an exception + if (filter_var($url, FILTER_VALIDATE_URL) === false || + parse_url($url, PHP_URL_HOST) === null || + parse_url($url, PHP_URL_PATH) === null) { + throw new ProviderIDNotSupportedException("The given ID is not a valid URL: ".$originalUrl); + } + + return $url; + } +} diff --git a/src/Services/InfoProviderSystem/Providers/GenericWebProvider.php b/src/Services/InfoProviderSystem/Providers/GenericWebProvider.php index b5a150f8..06a9d4c1 100644 --- a/src/Services/InfoProviderSystem/Providers/GenericWebProvider.php +++ b/src/Services/InfoProviderSystem/Providers/GenericWebProvider.php @@ -25,6 +25,7 @@ namespace App\Services\InfoProviderSystem\Providers; use App\Exceptions\ProviderIDNotSupportedException; use App\Helpers\RandomizeUseragentHttpClient; +use App\Services\InfoProviderSystem\CreateFromUrlHelper; use App\Services\InfoProviderSystem\DTOs\ParameterDTO; use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; use App\Services\InfoProviderSystem\DTOs\PriceDTO; @@ -48,12 +49,14 @@ use Symfony\Contracts\HttpClient\HttpClientInterface; class GenericWebProvider implements InfoProviderInterface { + use FixAndValidateUrlTrait; + public const DISTRIBUTOR_NAME = 'Website'; private readonly HttpClientInterface $httpClient; public function __construct(HttpClientInterface $httpClient, private readonly GenericWebProviderSettings $settings, - private readonly ProviderRegistry $providerRegistry, private readonly PartInfoRetriever $infoRetriever, + private readonly CreateFromUrlHelper $createFromUrlHelper, ) { //Use NoPrivateNetworkHttpClient to prevent SSRF vulnerabilities, and RandomizeUseragentHttpClient to make it harder for servers to block us @@ -85,19 +88,23 @@ class GenericWebProvider implements InfoProviderInterface return $this->settings->enabled; } - public function searchByKeyword(string $keyword): array + public function searchByKeyword(string $keyword, array $options = []): array { $url = $this->fixAndValidateURL($keyword); - //Before loading the page, try to delegate to another provider - $delegatedPart = $this->delegateToOtherProvider($url); - if ($delegatedPart !== null) { - return [$delegatedPart]; + if (!($options[self::OPTION_SKIP_DELEGATION] ?? false)) { + //Before loading the page, try to delegate to another provider + $delegatedPart = $this->createFromUrlHelper->delegateToOtherProvider($url, $this); + if ($delegatedPart !== null) { + return [$delegatedPart]; + } } try { + $new_options = $options; + $new_options[self::OPTION_SKIP_DELEGATION] = true; //Skip delegation for the getDetails call to prevent infinite loops return [ - $this->getDetails($keyword, false) //We already tried delegation + $this->getDetails($keyword, $new_options) ]; } catch (ProviderIDNotSupportedException $e) { return []; } @@ -274,78 +281,16 @@ class GenericWebProvider implements InfoProviderInterface return null; } - /** - * Delegates the URL to another provider if possible, otherwise return null - * @param string $url - * @return SearchResultDTO|null - */ - private function delegateToOtherProvider(string $url): ?SearchResultDTO - { - //Extract domain from url: - $host = parse_url($url, PHP_URL_HOST); - if ($host === false || $host === null) { - return null; - } - $provider = $this->providerRegistry->getProviderHandlingDomain($host); - - if ($provider !== null && $provider->isActive() && $provider->getProviderKey() !== $this->getProviderKey()) { - try { - $id = $provider->getIDFromURL($url); - if ($id !== null) { - $results = $this->infoRetriever->searchByKeyword($id, [$provider]); - if (count($results) > 0) { - return $results[0]; - } - } - return null; - } catch (ProviderIDNotSupportedException $e) { - //Ignore and continue - return null; - } - } - - return null; - } - - private function fixAndValidateURL(string $url): string - { - $originalUrl = $url; - - //Add scheme if missing - if (!preg_match('/^https?:\/\//', $url)) { - //Remove any leading slashes - $url = ltrim($url, '/'); - - //If the URL starts with https:/ or http:/, add the missing slash - //Traefik removes the double slash as secruity measure, so we want to be forgiving and add it back if needed - //See https://github.com/Part-DB/Part-DB-server/issues/1296 - if (preg_match('/^https?:\/[^\/]/', $url)) { - $url = preg_replace('/^(https?:)\/([^\/])/', '$1//$2', $url); - } else { - $url = 'https://'.$url; - } - } - - //If this is not a valid URL with host, domain and path, throw an exception - if (filter_var($url, FILTER_VALIDATE_URL) === false || - parse_url($url, PHP_URL_HOST) === null || - parse_url($url, PHP_URL_PATH) === null) { - throw new ProviderIDNotSupportedException("The given ID is not a valid URL: ".$originalUrl); - } - - return $url; - } - - public function getDetails(string $id, bool $check_for_delegation = true): PartDetailDTO + public function getDetails(string $id, array $options = []): PartDetailDTO { $url = $this->fixAndValidateURL($id); - if ($check_for_delegation) { + if (!($options[self::OPTION_SKIP_DELEGATION] ?? false)) { //Before loading the page, try to delegate to another provider - $delegatedPart = $this->delegateToOtherProvider($url); + $delegatedPart = $this->createFromUrlHelper->delegateToOtherProviderDetails($url, $this); if ($delegatedPart !== null) { - return $this->infoRetriever->getDetailsForSearchResult($delegatedPart); + return $delegatedPart; } } diff --git a/src/Services/InfoProviderSystem/Providers/InfoProviderInterface.php b/src/Services/InfoProviderSystem/Providers/InfoProviderInterface.php index 1f787559..a6e073a5 100644 --- a/src/Services/InfoProviderSystem/Providers/InfoProviderInterface.php +++ b/src/Services/InfoProviderSystem/Providers/InfoProviderInterface.php @@ -28,6 +28,8 @@ use App\Services\InfoProviderSystem\DTOs\SearchResultDTO; interface InfoProviderInterface { + public const OPTION_NO_CACHE = 'no_cache'; // if set to true, the provider should not use any cache and retrieve fresh data from the source + public const OPTION_SKIP_DELEGATION = 'skip_delegation'; // if set to true, the provider should not delegate the request to other providers, even if it supports delegation. /** * Get information about this provider @@ -61,16 +63,18 @@ interface InfoProviderInterface /** * Searches for a keyword and returns a list of search results * @param string $keyword The keyword to search for + * @param array $options An associative array of options for the search, which can be used to pass additional parameters to the provider (e.g. filters, pagination, etc.). The content of this array is provider specific and not defined by the interface * @return SearchResultDTO[] A list of search results */ - public function searchByKeyword(string $keyword): array; + public function searchByKeyword(string $keyword, array $options = []): array; /** * Returns detailed information about the part with the given id * @param string $id + * @param array $options An associative array of options for the search, which can be used to pass additional parameters to the provider (e.g. filters, pagination, etc.). The content of this array is provider specific and not defined by the interface * @return PartDetailDTO */ - public function getDetails(string $id): PartDetailDTO; + public function getDetails(string $id, array $options = []): PartDetailDTO; /** * A list of capabilities this provider supports (which kind of data it can provide). diff --git a/src/Services/InfoProviderSystem/Providers/LCSCProvider.php b/src/Services/InfoProviderSystem/Providers/LCSCProvider.php index 1b807eff..8bdd776e 100755 --- a/src/Services/InfoProviderSystem/Providers/LCSCProvider.php +++ b/src/Services/InfoProviderSystem/Providers/LCSCProvider.php @@ -349,17 +349,18 @@ class LCSCProvider implements BatchInfoProviderInterface, URLHandlerInfoProvider return $result; } - public function searchByKeyword(string $keyword): array + public function searchByKeyword(string $keyword, array $options = []): array { return $this->queryByTerm($keyword, true); // Use lightweight mode for search } /** * Batch search multiple keywords asynchronously (like JavaScript Promise.all) - * @param array $keywords Array of keywords to search + * @param array $keywords + * @param array $options * @return array Results indexed by keyword */ - public function searchByKeywordsBatch(array $keywords): array + public function searchByKeywordsBatch(array $keywords, array $options = []): array { if (empty($keywords)) { return []; @@ -428,7 +429,7 @@ class LCSCProvider implements BatchInfoProviderInterface, URLHandlerInfoProvider return $result; } - public function getDetails(string $id): PartDetailDTO + public function getDetails(string $id, array $options = []): PartDetailDTO { $tmp = $this->queryByTerm($id, false); if (count($tmp) === 0) { diff --git a/src/Services/InfoProviderSystem/Providers/MouserProvider.php b/src/Services/InfoProviderSystem/Providers/MouserProvider.php index 3171c994..49ca2d50 100644 --- a/src/Services/InfoProviderSystem/Providers/MouserProvider.php +++ b/src/Services/InfoProviderSystem/Providers/MouserProvider.php @@ -76,7 +76,7 @@ class MouserProvider implements InfoProviderInterface return $this->settings->apiKey !== '' && $this->settings->apiKey !== null; } - public function searchByKeyword(string $keyword): array + public function searchByKeyword(string $keyword, array $options = []): array { /* SearchByKeywordRequest description: @@ -144,7 +144,7 @@ class MouserProvider implements InfoProviderInterface return $this->responseToDTOArray($response); } - public function getDetails(string $id): PartDetailDTO + public function getDetails(string $id, array $options = []): PartDetailDTO { /* SearchByPartRequest description: diff --git a/src/Services/InfoProviderSystem/Providers/OEMSecretsProvider.php b/src/Services/InfoProviderSystem/Providers/OEMSecretsProvider.php index f7048a87..9764517b 100644 --- a/src/Services/InfoProviderSystem/Providers/OEMSecretsProvider.php +++ b/src/Services/InfoProviderSystem/Providers/OEMSecretsProvider.php @@ -278,12 +278,13 @@ class OEMSecretsProvider implements InfoProviderInterface * and debugging with local JSON files. The results are processed, cached, and then sorted based * on the keyword and specified criteria. * - * @param string $keyword The part number to search for + * @param string $keyword + * @param array $options * @return array An array of processed product details, sorted by relevance and additional criteria. * * @throws \Exception If the JSON file used for debugging is not found or contains errors. */ - public function searchByKeyword(string $keyword): array + public function searchByKeyword(string $keyword, array $options = []): array { /* oemsecrets Part Search API 3.0.1 @@ -414,14 +415,20 @@ class OEMSecretsProvider implements InfoProviderInterface * found in the cache, they are returned. If not, an exception is thrown indicating that * the details could not be found. * - * @param string $id The unique identifier of the provider or part. + * @param string $id + * @param array $options * @return PartDetailDTO The detailed information about the part. * * @throws \Exception If no details are found for the given provider ID. */ - public function getDetails(string $id): PartDetailDTO + public function getDetails(string $id, array $options = []): PartDetailDTO { $cacheKey = $this->getCacheKey($id); + + if ($options[self::OPTION_NO_CACHE] ?? false) { + $this->partInfoCache->deleteItem($cacheKey); + } + $cacheItem = $this->partInfoCache->getItem($cacheKey); if ($cacheItem->isHit()) { diff --git a/src/Services/InfoProviderSystem/Providers/OctopartProvider.php b/src/Services/InfoProviderSystem/Providers/OctopartProvider.php index 1142f4ef..de404e18 100644 --- a/src/Services/InfoProviderSystem/Providers/OctopartProvider.php +++ b/src/Services/InfoProviderSystem/Providers/OctopartProvider.php @@ -326,7 +326,7 @@ class OctopartProvider implements InfoProviderInterface ); } - public function searchByKeyword(string $keyword): array + public function searchByKeyword(string $keyword, array $options = []): array { $graphQL = sprintf(<<<'GRAPHQL' query partSearch($keyword: String, $limit: Int, $currency: String!, $country: String!, $authorizedOnly: Boolean!) { @@ -367,11 +367,13 @@ class OctopartProvider implements InfoProviderInterface return $tmp; } - public function getDetails(string $id): PartDetailDTO + public function getDetails(string $id, array $options = []): PartDetailDTO { + $no_cache = $options[self::OPTION_NO_CACHE] ?? false; + //Check if we have the part cached $cached = $this->getFromCache($id); - if ($cached !== null) { + if (!$no_cache && $cached !== null) { return $cached; } diff --git a/src/Services/InfoProviderSystem/Providers/PollinProvider.php b/src/Services/InfoProviderSystem/Providers/PollinProvider.php index 6ac969d3..7acecc3a 100644 --- a/src/Services/InfoProviderSystem/Providers/PollinProvider.php +++ b/src/Services/InfoProviderSystem/Providers/PollinProvider.php @@ -66,7 +66,7 @@ class PollinProvider implements InfoProviderInterface, URLHandlerInfoProviderInt return $this->settings->enabled; } - public function searchByKeyword(string $keyword): array + public function searchByKeyword(string $keyword, array $options = []): array { $response = $this->client->request('GET', 'https://www.pollin.de/search', [ 'query' => [ @@ -110,7 +110,7 @@ class PollinProvider implements InfoProviderInterface, URLHandlerInfoProviderInt }; } - public function getDetails(string $id): PartDetailDTO + public function getDetails(string $id, array $options = []): PartDetailDTO { //Ensure that $id is numeric if (!is_numeric($id)) { diff --git a/src/Services/InfoProviderSystem/Providers/ProviderCapabilities.php b/src/Services/InfoProviderSystem/Providers/ProviderCapabilities.php index 21fba53b..3a7d03e9 100644 --- a/src/Services/InfoProviderSystem/Providers/ProviderCapabilities.php +++ b/src/Services/InfoProviderSystem/Providers/ProviderCapabilities.php @@ -46,6 +46,9 @@ enum ProviderCapabilities /** Provider can provide GTIN for a part */ case GTIN; + /** Provider can provide parameters/specifications for a part */ + case PARAMETERS; + /** * Get the order index for displaying capabilities in a stable order. * @return int @@ -59,6 +62,7 @@ enum ProviderCapabilities self::PRICE => 4, self::FOOTPRINT => 5, self::GTIN => 6, + self::PARAMETERS => 7, }; } @@ -71,6 +75,7 @@ enum ProviderCapabilities self::DATASHEET => 'datasheet', self::PRICE => 'price', self::GTIN => 'gtin', + self::PARAMETERS => 'parameters', }; } @@ -83,6 +88,7 @@ enum ProviderCapabilities self::DATASHEET => 'fa-file-alt', self::PRICE => 'fa-money-bill-wave', self::GTIN => 'fa-barcode', + self::PARAMETERS => 'fa-list-ul', }; } } diff --git a/src/Services/InfoProviderSystem/Providers/ReicheltProvider.php b/src/Services/InfoProviderSystem/Providers/ReicheltProvider.php index 81f0a449..9dfb099f 100644 --- a/src/Services/InfoProviderSystem/Providers/ReicheltProvider.php +++ b/src/Services/InfoProviderSystem/Providers/ReicheltProvider.php @@ -69,7 +69,7 @@ class ReicheltProvider implements InfoProviderInterface return $this->settings->enabled; } - public function searchByKeyword(string $keyword): array + public function searchByKeyword(string $keyword, array $options = []): array { $response = $this->client->request('GET', sprintf($this->getBaseURL() . '/shop/search/%s', $keyword)); $html = $response->getContent(); @@ -108,7 +108,7 @@ class ReicheltProvider implements InfoProviderInterface return $results; } - public function getDetails(string $id): PartDetailDTO + public function getDetails(string $id, array $options = []): PartDetailDTO { //Check that the ID is a number if (!is_numeric($id)) { diff --git a/src/Services/InfoProviderSystem/Providers/TMEProvider.php b/src/Services/InfoProviderSystem/Providers/TMEProvider.php index 93222517..24ba0ea7 100644 --- a/src/Services/InfoProviderSystem/Providers/TMEProvider.php +++ b/src/Services/InfoProviderSystem/Providers/TMEProvider.php @@ -69,7 +69,7 @@ class TMEProvider implements InfoProviderInterface, URLHandlerInfoProviderInterf return $this->tmeClient->isUsable(); } - public function searchByKeyword(string $keyword): array + public function searchByKeyword(string $keyword, array $options = []): array { $response = $this->tmeClient->makeRequest('Products/Search', [ 'Country' => $this->settings->country, @@ -99,7 +99,7 @@ class TMEProvider implements InfoProviderInterface, URLHandlerInfoProviderInterf return $result; } - public function getDetails(string $id): PartDetailDTO + public function getDetails(string $id, array $options = []): PartDetailDTO { $response = $this->tmeClient->makeRequest('Products/GetProducts', [ 'Country' => $this->settings->country, diff --git a/src/Services/InfoProviderSystem/Providers/TestProvider.php b/src/Services/InfoProviderSystem/Providers/TestProvider.php index 8b78c95a..42927abd 100644 --- a/src/Services/InfoProviderSystem/Providers/TestProvider.php +++ b/src/Services/InfoProviderSystem/Providers/TestProvider.php @@ -55,7 +55,7 @@ class TestProvider implements InfoProviderInterface return true; } - public function searchByKeyword(string $keyword): array + public function searchByKeyword(string $keyword, array $options = []): array { return [ new SearchResultDTO(provider_key: $this->getProviderKey(), provider_id: 'element1', name: 'Element 1', description: 'fd'), @@ -72,7 +72,7 @@ class TestProvider implements InfoProviderInterface ]; } - public function getDetails(string $id): PartDetailDTO + public function getDetails(string $id, array $options = []): PartDetailDTO { return new PartDetailDTO( provider_key: $this->getProviderKey(), @@ -92,4 +92,4 @@ class TestProvider implements InfoProviderInterface ] ); } -} \ No newline at end of file +} diff --git a/src/Services/System/UpdateAvailableFacade.php b/src/Services/System/UpdateAvailableFacade.php index ac3a46c0..60f66036 100644 --- a/src/Services/System/UpdateAvailableFacade.php +++ b/src/Services/System/UpdateAvailableFacade.php @@ -105,6 +105,6 @@ class UpdateAvailableFacade return $this->updateCache->get(self::CACHE_KEY, function (ItemInterface $item) { $item->expiresAfter(self::CACHE_TTL); return $this->updateChecker->getLatestVersion(); - }); + }) ?? ['version' => '0.0.1', 'url' => 'update-checking-failed']; } } diff --git a/src/Settings/AISettings/AISettings.php b/src/Settings/AISettings/AISettings.php new file mode 100644 index 00000000..732eb597 --- /dev/null +++ b/src/Settings/AISettings/AISettings.php @@ -0,0 +1,43 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Settings\AISettings; + +use App\Settings\SettingsIcon; +use Jbtronics\SettingsBundle\Settings\EmbeddedSettings; +use Jbtronics\SettingsBundle\Settings\Settings; +use Jbtronics\SettingsBundle\Settings\SettingsTrait; +use Symfony\Component\Translation\TranslatableMessage as TM; + +#[Settings(label: new TM("settings.ai"))] +#[SettingsIcon("fa-brain")] +class AISettings +{ + use SettingsTrait; + + #[EmbeddedSettings] + public ?OpenRouterSettings $openRouter = null; + + #[EmbeddedSettings] + public ?LMStudioSettings $lmstudio = null; +} diff --git a/src/Settings/AISettings/LMStudioSettings.php b/src/Settings/AISettings/LMStudioSettings.php new file mode 100644 index 00000000..2bdad06e --- /dev/null +++ b/src/Settings/AISettings/LMStudioSettings.php @@ -0,0 +1,53 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Settings\AISettings; + +use App\Form\Type\APIKeyType; +use App\Services\AI\AIPlatformSettingsInterface; +use App\Settings\SettingsIcon; +use Jbtronics\SettingsBundle\Metadata\EnvVarMode; +use Jbtronics\SettingsBundle\Settings\Settings; +use Jbtronics\SettingsBundle\Settings\SettingsParameter; +use Jbtronics\SettingsBundle\Settings\SettingsTrait; +use Symfony\Component\Form\Extension\Core\Type\UrlType; +use Symfony\Component\Translation\StaticMessage; +use Symfony\Component\Translation\TranslatableMessage as TM; + +#[Settings(name: 'ai_lmstudio', label: new TM("settings.ai.lmstudio"))] +#[SettingsIcon("fa-robot")] +class LMStudioSettings implements AIPlatformSettingsInterface +{ + use SettingsTrait; + + #[SettingsParameter(label: new TM("settings.ai.lmstudio.hosturl"), + formType: UrlType::class, + formOptions: ["attr" => ["placeholder" => new StaticMessage("http://localhost:1234")]], + envVar: "AI_LMSTUDIO_HOSTURL", envVarMode: EnvVarMode::OVERWRITE)] + public ?string $hostURL = null; + + public function isAIPlatformEnabled(): bool + { + return $this->hostURL !== null && $this->hostURL !== ""; + } +} diff --git a/src/Settings/AISettings/OpenRouterSettings.php b/src/Settings/AISettings/OpenRouterSettings.php new file mode 100644 index 00000000..e083513a --- /dev/null +++ b/src/Settings/AISettings/OpenRouterSettings.php @@ -0,0 +1,50 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Settings\AISettings; + +use App\Form\Type\APIKeyType; +use App\Services\AI\AIPlatformSettingsInterface; +use App\Settings\SettingsIcon; +use Jbtronics\SettingsBundle\Metadata\EnvVarMode; +use Jbtronics\SettingsBundle\Settings\Settings; +use Jbtronics\SettingsBundle\Settings\SettingsParameter; +use Jbtronics\SettingsBundle\Settings\SettingsTrait; +use Symfony\Component\Translation\TranslatableMessage as TM; + +#[Settings(name: 'ai_openrouter', label: new TM("settings.ai.openrouter"), description: "settings.ai.openrouter.help")] +#[SettingsIcon("fa-robot")] +class OpenRouterSettings implements AIPlatformSettingsInterface +{ + use SettingsTrait; + + #[SettingsParameter(label: new TM("settings.ips.element14.apiKey"), + formType: APIKeyType::class, + formOptions: ["help_html" => true], envVar: "AI_OPENROUTER_KEY", envVarMode: EnvVarMode::OVERWRITE)] + public ?string $apiKey = null; + + public function isAIPlatformEnabled(): bool + { + return $this->apiKey !== null && $this->apiKey !== ""; + } +} diff --git a/src/Settings/AppSettings.php b/src/Settings/AppSettings.php index 14d9395e..085f7ffe 100644 --- a/src/Settings/AppSettings.php +++ b/src/Settings/AppSettings.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace App\Settings; +use App\Settings\AISettings\AISettings; use App\Settings\BehaviorSettings\BehaviorSettings; use App\Settings\InfoProviderSystem\InfoProviderSettings; use App\Settings\MiscSettings\MiscSettings; @@ -50,6 +51,9 @@ class AppSettings #[EmbeddedSettings] public ?SynonymSettings $synonyms = null; + #[EmbeddedSettings] + public ?AISettings $ai = null; + #[EmbeddedSettings()] public ?MiscSettings $miscSettings = null; diff --git a/src/Settings/InfoProviderSystem/AIExtractorSettings.php b/src/Settings/InfoProviderSystem/AIExtractorSettings.php new file mode 100644 index 00000000..da5ef0f9 --- /dev/null +++ b/src/Settings/InfoProviderSystem/AIExtractorSettings.php @@ -0,0 +1,73 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Settings\InfoProviderSystem; + +use App\Form\Settings\AiModelsType; +use App\Form\Settings\AiPlatformChoiceType; +use App\Services\AI\AIPlatforms; +use App\Settings\SettingsIcon; +use Jbtronics\SettingsBundle\Metadata\EnvVarMode; +use Jbtronics\SettingsBundle\Settings\Settings; +use Jbtronics\SettingsBundle\Settings\SettingsParameter; +use Jbtronics\SettingsBundle\Settings\SettingsTrait; +use Symfony\AI\Platform\Capability; +use Symfony\Component\Form\Extension\Core\Type\LanguageType; +use Symfony\Component\Form\Extension\Core\Type\TextareaType; +use Symfony\Component\Translation\TranslatableMessage as TM; +use Symfony\Component\Validator\Constraints\Language; + +#[Settings(name: "ai_extractor", label: new TM("settings.ips.ai_extractor"), description: new TM("settings.ips.ai_extractor.description"))] +#[SettingsIcon("fa-plug")] +class AIExtractorSettings +{ + private const MODEL_SELECTOR_LABEL = 'ai_extractor'; + + use SettingsTrait; + + #[SettingsParameter(label: new TM("settings.ips.ai_extractor.ai_platform"), + formType: AiPlatformChoiceType::class, formOptions: ['platform_selector_label' => self::MODEL_SELECTOR_LABEL], + )] + public ?AIPlatforms $platform = null; + + #[SettingsParameter(label: new TM("settings.ips.ai_extractor.model"), description: new TM("settings.ips.ai_extractor.model.help"), + formType: AiModelsType::class, formOptions: ['platform_selector' => self::MODEL_SELECTOR_LABEL, 'filter_capability' => Capability::OUTPUT_STRUCTURED], + )] + public ?string $model = null; + + #[SettingsParameter(label: new TM("settings.ips.ai_extractor.max_content_length"), + description: new TM("settings.ips.ai_extractor.max_content_length.description"), + )] + public int $maxContentLength = 50000; + + #[Language] + #[SettingsParameter(label: new TM("settings.ips.ai_extractor.output_language"), description: new TM("settings.ips.ai_extractor.output_language.description"), + formType: LanguageType::class, + )] + public ?string $outputLanguage = null; + + #[SettingsParameter(label: new TM("settings.ips.ai_extractor.additional_instructions"), description: new TM("settings.ips.ai_extractor.additional_instructions.description"), + formType: TextareaType::class, + )] + public ?string $additionalInstructions = null; +} diff --git a/src/Settings/InfoProviderSystem/CanopySettings.php b/src/Settings/InfoProviderSystem/CanopySettings.php index 0858871b..3c97a80e 100644 --- a/src/Settings/InfoProviderSystem/CanopySettings.php +++ b/src/Settings/InfoProviderSystem/CanopySettings.php @@ -72,7 +72,7 @@ class CanopySettings /** * @var string The domain used internally for the API requests. This is not necessarily the same as the domain shown to the user, which is determined by the keys of the ALLOWED_DOMAINS constant */ - #[SettingsParameter(label: new TM("settings.ips.tme.country"), formType: ChoiceType::class, formOptions: ["choices" => self::ALLOWED_DOMAINS])] + #[SettingsParameter(label: new TM("settings.ips.tme.country"), formType: ChoiceType::class, formOptions: ["choices" => self::ALLOWED_DOMAINS, 'translation_domain' => false])] public string $domain = "DE"; /** diff --git a/src/Settings/InfoProviderSystem/InfoProviderSettings.php b/src/Settings/InfoProviderSystem/InfoProviderSettings.php index 248fcedc..3e2a27ef 100644 --- a/src/Settings/InfoProviderSystem/InfoProviderSettings.php +++ b/src/Settings/InfoProviderSystem/InfoProviderSettings.php @@ -40,6 +40,9 @@ class InfoProviderSettings #[EmbeddedSettings] public ?GenericWebProviderSettings $genericWebProvider = null; + #[EmbeddedSettings] + public ?AIExtractorSettings $aiExtractor = null; + #[EmbeddedSettings] public ?DigikeySettings $digikey = null; @@ -75,4 +78,5 @@ class InfoProviderSettings #[EmbeddedSettings] public ?CanopySettings $canopy = null; + } diff --git a/src/Twig/MiscExtension.php b/src/Twig/MiscExtension.php index 390ad084..565d56f2 100644 --- a/src/Twig/MiscExtension.php +++ b/src/Twig/MiscExtension.php @@ -22,6 +22,7 @@ declare(strict_types=1); */ namespace App\Twig; +use App\Services\InfoProviderSystem\CreateFromUrlHelper; use Twig\Attribute\AsTwigFunction; use App\Settings\SettingsIcon; use Symfony\Component\HttpFoundation\Request; @@ -34,7 +35,7 @@ use Twig\Extension\AbstractExtension; final readonly class MiscExtension { - public function __construct(private EventCommentNeededHelper $eventCommentNeededHelper) + public function __construct(private EventCommentNeededHelper $eventCommentNeededHelper, private CreateFromUrlHelper $fromUrlHelper) { } @@ -84,4 +85,14 @@ final readonly class MiscExtension return $request->getBaseUrl().$request->getPathInfo().$qs; } + + /** + * Returns true if the from url provider is active, false otherwise. + * @return bool + */ + #[AsTwigFunction(name: 'create_from_url_active')] + public function create_from_url_active(): bool + { + return $this->fromUrlHelper->canCreateFromUrl(); + } } diff --git a/symfony.lock b/symfony.lock index 68de2da7..f8f88675 100644 --- a/symfony.lock +++ b/symfony.lock @@ -375,6 +375,54 @@ "shivas/versioning-bundle": { "version": "4.0.3" }, + "symfony/ai-bundle": { + "version": "0.8", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "0.1", + "ref": "2be6ccd77335c2631fdf12d1680649b072efb8ad" + }, + "files": [ + "config/packages/ai.yaml" + ] + }, + "symfony/ai-generic-platform": { + "version": "0.8", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "0.1", + "ref": "f38913b87380322d4c40c302b41626e811516bc4" + }, + "files": [ + "config/packages/ai_generic_platform.yaml" + ] + }, + "symfony/ai-lm-studio-platform": { + "version": "0.8", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "0.1", + "ref": "e35cced28f6559fc5effccb8f22597f309fedfdf" + }, + "files": [ + "config/packages/ai_lm_studio_platform.yaml" + ] + }, + "symfony/ai-open-router-platform": { + "version": "0.8", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "0.1", + "ref": "c39a146c6ec3df8b874accf6ce1cccbda431a688" + }, + "files": [ + "config/packages/ai_open_router_platform.yaml" + ] + }, "symfony/apache-pack": { "version": "1.0", "recipe": { @@ -393,12 +441,6 @@ "symfony/browser-kit": { "version": "v4.2.3" }, - "symfony/cache": { - "version": "v4.2.3" - }, - "symfony/cache-contracts": { - "version": "v1.1.5" - }, "symfony/config": { "version": "v4.2.3" }, diff --git a/templates/_navbar.html.twig b/templates/_navbar.html.twig index 57331370..7719ab2b 100644 --- a/templates/_navbar.html.twig +++ b/templates/_navbar.html.twig @@ -52,7 +52,7 @@ {% trans %}info_providers.search.title{% endtrans %} - {% if settings_instance('generic_web_provider').enabled %} + {% if create_from_url_active() %}
  • diff --git a/templates/info_providers/from_url/from_url.html.twig b/templates/info_providers/from_url/from_url.html.twig index 3370a94c..49d4b116 100644 --- a/templates/info_providers/from_url/from_url.html.twig +++ b/templates/info_providers/from_url/from_url.html.twig @@ -16,6 +16,22 @@ {{ form_start(form) }} {{ form_row(form.url) }} + + {{ form_row(form.method) }} + + + +
    +
    + {{ form_row(form.no_cache) }} + {{ form_row(form.skip_delegation) }} +
    +
    + {{ form_row(form.submit) }} {{ form_end(form) }} {% endblock %} diff --git a/templates/info_providers/search/part_search.html.twig b/templates/info_providers/search/part_search.html.twig index eac3507d..9e1f0834 100644 --- a/templates/info_providers/search/part_search.html.twig +++ b/templates/info_providers/search/part_search.html.twig @@ -33,6 +33,19 @@
  • + + +
    +
    + {{ form_row(form.no_cache_search) }} + {{ form_row(form.no_cache_details) }} +
    +
    + {{ form_row(form.submit) }} {{ form_end(form) }} @@ -116,16 +129,16 @@ {% if update_target %} {# We update an existing part #} {% set href = path('info_providers_update_part', - {'providerKey': dto.provider_key, 'providerId': dto.provider_id, 'id': update_target.iD}) %} + {'providerKey': dto.provider_key, 'providerId': dto.provider_id, 'id': update_target.iD, 'no_cache': no_cache_details ? 1 : null}) %} {% else %} {# Create a fresh part #} {% set href = path('info_providers_create_part', - {'providerKey': dto.provider_key, 'providerId': dto.provider_id}) %} + {'providerKey': dto.provider_key, 'providerId': dto.provider_id, 'no_cache': no_cache_details ? 1 : null}) %} {% endif %} {# If we have no local part, then we can just show the create button #} {% if localPart is null %} + target="_blank" title="{% trans %}part.create.btn{% endtrans %}"> {% else %} {# Otherwise add a button group with all three buttons #} @@ -139,7 +152,7 @@ target="_blank" title="{% trans %}info_providers.search.show_existing_part{% endtrans %}"> - diff --git a/tests/Services/AI/AIPlatformRegistryTest.php b/tests/Services/AI/AIPlatformRegistryTest.php new file mode 100644 index 00000000..1577f9b5 --- /dev/null +++ b/tests/Services/AI/AIPlatformRegistryTest.php @@ -0,0 +1,99 @@ +. + */ + +/** + * Tests for App\Services\AI\AIPlatformRegistry + */ +declare(strict_types=1); + +namespace App\Tests\Services\AI; + +use App\Services\AI\AIPlatformRegistry; +use App\Services\AI\AIPlatforms; +use App\Services\AI\AIPlatformSettingsInterface; +use Jbtronics\SettingsBundle\Manager\SettingsManagerInterface; +use PHPUnit\Framework\TestCase; +use Symfony\AI\Platform\PlatformInterface; + +class AIPlatformRegistryTest extends TestCase +{ + public function testRegistersEnabledPlatformsAndReturnsPlatform(): void + { + // Create a platform mock and expose it under the service tag name (openrouter) + $platformMock = $this->createMock(PlatformInterface::class); + + // Settings for OpenRouter -> enabled + $openRouterSettings = $this->createMock(AIPlatformSettingsInterface::class); + $openRouterSettings->method('isAIPlatformEnabled')->willReturn(true); + + // Settings for LMStudio -> disabled + $lmSettings = $this->createMock(AIPlatformSettingsInterface::class); + $lmSettings->method('isAIPlatformEnabled')->willReturn(false); + + // Settings manager should return the corresponding settings object depending on the requested class name + $settingsManager = $this->createMock(SettingsManagerInterface::class); + $settingsManager->method('get')->willReturnMap([ + [AIPlatforms::OPENROUTER->toSettingsClass(), $openRouterSettings], + [AIPlatforms::LMSTUDIO->toSettingsClass(), $lmSettings], + ]); + + $platforms = new \ArrayIterator([ + AIPlatforms::OPENROUTER->toServiceTagName() => $platformMock, + ]); + + $registry = new AIPlatformRegistry($settingsManager, $platforms); + + // OPENROUTER should be enabled and retrievable + $this->assertTrue($registry->isEnabled(AIPlatforms::OPENROUTER)); + $this->assertSame($platformMock, $registry->getPlatform(AIPlatforms::OPENROUTER)); + + // LMSTUDIO is either not registered or disabled -> should not be enabled + $this->assertFalse($registry->isEnabled(AIPlatforms::LMSTUDIO)); + $this->expectException(\InvalidArgumentException::class); + $registry->getPlatform(AIPlatforms::LMSTUDIO); + } + + public function testGetEnabledPlatformsReturnsIndexedArray(): void + { + $platformMock = $this->createMock(PlatformInterface::class); + + $openRouterSettings = $this->createMock(AIPlatformSettingsInterface::class); + $openRouterSettings->method('isAIPlatformEnabled')->willReturn(true); + + $settingsManager = $this->createMock(SettingsManagerInterface::class); + $settingsManager->method('get')->willReturnMap([ + [AIPlatforms::OPENROUTER->toSettingsClass(), $openRouterSettings], + [AIPlatforms::LMSTUDIO->toSettingsClass(), $this->createMock(AIPlatformSettingsInterface::class)], + ]); + + $platforms = new \ArrayIterator([ + AIPlatforms::OPENROUTER->toServiceTagName() => $platformMock, + // lmstudio not registered + ]); + + $registry = new AIPlatformRegistry($settingsManager, $platforms); + + $enabled = $registry->getEnabledPlatforms(); + + $this->assertArrayHasKey(AIPlatforms::OPENROUTER->value, $enabled); + $this->assertSame($platformMock, $enabled[AIPlatforms::OPENROUTER->value]); + } +} + diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 4da88512..8af54745 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -2780,7 +2780,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Name - + part.table.si_value SI Value @@ -7218,13 +7218,13 @@ Element 1 -> Element 1.2 Subprojects - + project.info.total_build_price Total build price - + project.info.per_unit_price per unit @@ -7254,7 +7254,7 @@ Element 1 -> Element 1.2 Price - + project.bom.ext_price Extended Price @@ -10053,85 +10053,85 @@ Please note, that you can not impersonate a disabled user. If you try you will g When enabled, the datasheet field in KiCad will link to the actual PDF file (if found). When disabled, it will link to the Part-DB page instead. The Part-DB page link is always available as a separate "Part-DB URL" field. - + settings.misc.kicad_eda.editor.title KiCad autocomplete lists - + settings.misc.kicad_eda.editor.link Autocomplete settings - + settings.misc.kicad_eda.editor.description Configure whether KiCad autocomplete uses the autogenerated default lists or your custom override files. The custom files are editable here, while the default files are shown read-only for reference. - + settings.misc.kicad_eda.editor.footprints Footprints list - + settings.misc.kicad_eda.editor.footprints.help One entry per line. Used as autocomplete suggestions for KiCad footprint fields. - + settings.misc.kicad_eda.editor.symbols Symbols list - + settings.misc.kicad_eda.editor.symbols.help One entry per line. Used as autocomplete suggestions for KiCad symbol fields. - + settings.misc.kicad_eda.use_custom_list Use custom autocomplete lists - + settings.misc.kicad_eda.use_custom_list.help When enabled, KiCad autocomplete uses public/kicad/footprints_custom.txt and public/kicad/symbols_custom.txt instead of the autogenerated default files. - + settings.misc.kicad_eda.editor.custom_footprints Custom footprints list - + settings.misc.kicad_eda.editor.custom_symbols Custom symbols list - + settings.misc.kicad_eda.editor.default_footprints Default footprints list - + settings.misc.kicad_eda.editor.default_symbols Default symbols list - + settings.misc.kicad_eda.editor.default_files_help Autogenerated file shown for reference only. Changes must be made in the custom list. @@ -13067,5 +13067,149 @@ Buerklin-API Authentication server: Mapping error: Check if you have selected the right delimiter! + + + settings.ai + AI + + + + + settings.ai.openrouter + OpenRouter + + + + + settings.ai.lmstudio + LMStudio + + + + + settings.ips.ai_extractor.model + AI Model + + + + + settings.ips.ai_extractor.ai_platform + AI Platform + + + + + settings.ips.ai_extractor.model.help + The AI model that should be used for extraction. Must support structured output. + + + + + settings.ips.ai_extractor.max_content_length + Max. Website Content length + + + + + settings.ips.ai_extractor.max_content_length.description + The maximum number of characters of the website that are sent to the AI service. + + + + + settings.ips.ai_extractor.output_language + Output language + + + + + settings.ips.ai_extractor.output_language.description + By default, the providers returns information in the same language as the website. With that option you can ask the AI to translate it for you. Might only work with certain models. + + + + + settings.ips.ai_extractor.additional_instructions + Additional instructions + + + + + settings.ips.ai_extractor.additional_instructions.description + The additional instructions will be appended to the system prompt. + + + + + info_providers.search.advanced_options + Advanced options + + + + + info_providers.no_cache_search + Do not cache search results / Force fresh search + + + + + info_providers.no_cache_details + Do not cache result details / Force fresh part detail retrieval + + + + + info_providers.from_url.method.generic_web + Classic Web Scraper + + + + + info_providers.from_url.method.ai_web + AI Web Scraper + + + + + info_providers.from_url.method + Method + + + + + info_providers.from_url.no_cache + Ignore cache / Force fresh info retrieval + + + + + info_providers.from_url.skip_delegation + Do not delegate to specialized info providers + + + + + settings.ips.ai_extractor + AI Web Extractor + + + + + settings.ips.ai_extractor.description + This info provider uses an large language model (LLM) to extract detailed part information from arbitary shop URLs. + + + + + settings.ai.openrouter.help + Access to many AI models via openrouter.ai + + + + + settings.ai.lmstudio.hosturl + Host URL + +