diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 00f7ec19e..556d88624 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -14,12 +14,13 @@ jobs: strategy: fail-fast: false matrix: - php: [ "8.1", "8.2", "8.3" ] + php: [ "8.2", "8.3" ] postgres: ["14.6"] - symfony: [ "^6.4", "^7.0" ] - exclude: - - php: "8.1" - symfony: "^7.0" + symfony: [ "^6.4", "^7.4" ] + include: + - php: '8.4' + postgres: "14.6" + symfony: "^8.0" name: "PHP ${{ matrix.php }} / Symfony ${{ matrix.symfony }}" env: APP_ENV: test @@ -33,7 +34,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: "${{ matrix.php }}" - tools: symfony + tools: 'symfony, composer:v2, flex' coverage: none - name: Shutdown default MySQL @@ -46,10 +47,7 @@ jobs: postgresql password: "postgres" - name: "Restrict packages' versions" - run: | - composer global config --no-plugins allow-plugins.symfony/flex true - composer global require --no-progress --no-scripts --no-plugins "symfony/flex" - composer config extra.symfony.require "${{ matrix.symfony }}" + run: composer config extra.symfony.require "${{ matrix.symfony }}" - name: Get Composer cache directory id: composer-cache @@ -102,9 +100,7 @@ jobs: - name: "Restrict packages' versions (Admin Ui)" run: | - (cd src/AdminUi/ && composer global config --no-plugins allow-plugins.symfony/flex true) - (cd src/AdminUi/ && composer global config --no-plugins allow-plugins.symfony/runtime true) - (cd src/AdminUi/ && composer global require --no-progress --no-scripts --no-plugins "symfony/flex") + (cd src/AdminUi/ && composer config --no-plugins allow-plugins.symfony/runtime true) (cd src/AdminUi/ && composer config extra.symfony.require "${{ matrix.symfony }}") - name: "Install dependencies (Admin Ui)" @@ -115,9 +111,7 @@ jobs: - name: "Restrict packages' versions (Bootstrap Admin Ui)" run: | - (cd src/BootstrapAdminUi/ && composer global config --no-plugins allow-plugins.symfony/flex true) - (cd src/BootstrapAdminUi/ && composer global config --no-plugins allow-plugins.symfony/runtime true) - (cd src/BootstrapAdminUi/ && composer global require --no-progress --no-scripts --no-plugins "symfony/flex") + (cd src/BootstrapAdminUi/ && composer config --no-plugins allow-plugins.symfony/runtime true) (cd src/BootstrapAdminUi/ && composer config extra.symfony.require "${{ matrix.symfony }}") - name: "Install dependencies (Bootstrap Admin Ui)" @@ -128,9 +122,7 @@ jobs: - name: "Restrict packages' versions (Twig Hooks)" run: | - (cd src/TwigHooks/ && composer global config --no-plugins allow-plugins.symfony/flex true) - (cd src/TwigHooks/ && composer global config --no-plugins allow-plugins.symfony/runtime true) - (cd src/TwigHooks/ && composer global require --no-progress --no-scripts --no-plugins "symfony/flex") + (cd src/TwigHooks/ && composer config --no-plugins allow-plugins.symfony/runtime true) (cd src/TwigHooks/ && composer config extra.symfony.require "${{ matrix.symfony }}") - name: "Install dependencies (Twig Hooks)" @@ -141,9 +133,7 @@ jobs: - name: "Restrict packages' versions (Twig Extra)" run: | - (cd src/TwigExtra/ && composer global config --no-plugins allow-plugins.symfony/flex true) - (cd src/TwigExtra/ && composer global config --no-plugins allow-plugins.symfony/runtime true) - (cd src/TwigExtra/ && composer global require --no-progress --no-scripts --no-plugins "symfony/flex") + (cd src/TwigExtra/ && composer config --no-plugins allow-plugins.symfony/runtime true) (cd src/TwigExtra/ && composer config extra.symfony.require "${{ matrix.symfony }}") - name: "Install dependencies (Twig Extra)" @@ -154,9 +144,7 @@ jobs: - name: "Restrict packages' versions (Ui Translations)" run: | - (cd src/UiTranslations/ && composer global config --no-plugins allow-plugins.symfony/flex true) - (cd src/UiTranslations/ && composer global config --no-plugins allow-plugins.symfony/runtime true) - (cd src/UiTranslations/ && composer global require --no-progress --no-scripts --no-plugins "symfony/flex") + (cd src/UiTranslations/ && composer config --no-plugins allow-plugins.symfony/runtime true) (cd src/UiTranslations/ && composer config extra.symfony.require "${{ matrix.symfony }}") - name: "Install dependencies (Ui Translations)" diff --git a/app/Entity/Book.php b/app/Entity/Book.php index 0c7b2ccf2..324824eda 100644 --- a/app/Entity/Book.php +++ b/app/Entity/Book.php @@ -36,6 +36,10 @@ new Create(), new Update(), new Index(grid: BookGrid::class), + new Index( + template: '@SyliusAdminUi/crud/index.html.twig', + shortName: 'withoutGrid', + ), new Delete(), new BulkDelete(), new Show(), diff --git a/app/Factory/BookFactory.php b/app/Factory/BookFactory.php index 1afe2f9e3..1a7a91f74 100644 --- a/app/Factory/BookFactory.php +++ b/app/Factory/BookFactory.php @@ -14,12 +14,12 @@ namespace App\Factory; use App\Entity\Book; -use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; +use Zenstruck\Foundry\Persistence\PersistentObjectFactory; /** - * @extends PersistentProxyObjectFactory + * @extends PersistentObjectFactory */ -final class BookFactory extends PersistentProxyObjectFactory +final class BookFactory extends PersistentObjectFactory { public static function class(): string { diff --git a/app/Factory/ConferenceFactory.php b/app/Factory/ConferenceFactory.php index c425f6e49..df919a849 100644 --- a/app/Factory/ConferenceFactory.php +++ b/app/Factory/ConferenceFactory.php @@ -14,12 +14,12 @@ namespace App\Factory; use App\Entity\Conference; -use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; +use Zenstruck\Foundry\Persistence\PersistentObjectFactory; /** - * @extends PersistentProxyObjectFactory + * @extends PersistentObjectFactory */ -final class ConferenceFactory extends PersistentProxyObjectFactory +final class ConferenceFactory extends PersistentObjectFactory { public static function class(): string { diff --git a/app/Factory/SpeakerFactory.php b/app/Factory/SpeakerFactory.php index bc82edf47..b1fd53c31 100644 --- a/app/Factory/SpeakerFactory.php +++ b/app/Factory/SpeakerFactory.php @@ -17,12 +17,12 @@ use App\Entity\SpeakerAvatar; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\HttpFoundation\File\UploadedFile; -use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; +use Zenstruck\Foundry\Persistence\PersistentObjectFactory; /** - * @extends PersistentProxyObjectFactory + * @extends PersistentObjectFactory */ -final class SpeakerFactory extends PersistentProxyObjectFactory +final class SpeakerFactory extends PersistentObjectFactory { public static function class(): string { diff --git a/app/Factory/TalkFactory.php b/app/Factory/TalkFactory.php index fe068ee7b..51b849670 100644 --- a/app/Factory/TalkFactory.php +++ b/app/Factory/TalkFactory.php @@ -19,13 +19,13 @@ use App\Enum\Track; use function Zenstruck\Foundry\lazy; use Zenstruck\Foundry\Persistence\Exception\NotEnoughObjects; -use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; +use Zenstruck\Foundry\Persistence\PersistentObjectFactory; use Zenstruck\Foundry\Persistence\Proxy; /** - * @extends PersistentProxyObjectFactory + * @extends PersistentObjectFactory */ -final class TalkFactory extends PersistentProxyObjectFactory +final class TalkFactory extends PersistentObjectFactory { public static function class(): string { diff --git a/app/Factory/UserFactory.php b/app/Factory/UserFactory.php index 7f182fa2d..bc3b093e0 100644 --- a/app/Factory/UserFactory.php +++ b/app/Factory/UserFactory.php @@ -15,12 +15,12 @@ use App\Entity\User; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; -use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; +use Zenstruck\Foundry\Persistence\PersistentObjectFactory; /** - * @extends PersistentProxyObjectFactory + * @extends PersistentObjectFactory */ -final class UserFactory extends PersistentProxyObjectFactory +final class UserFactory extends PersistentObjectFactory { public function __construct( private UserPasswordHasherInterface $userPasswordHasher, diff --git a/composer.json b/composer.json index 075290b27..2d9da6f00 100644 --- a/composer.json +++ b/composer.json @@ -15,26 +15,27 @@ ], "require": { "php": "^8.1", - "doctrine/dbal": "^3", - "doctrine/doctrine-bundle": "^2.12", - "doctrine/orm": "^2.0", + "doctrine/dbal": "^3 || ^4", + "doctrine/doctrine-bundle": "^2.13 || ^3.0 || ^4.0", + "doctrine/orm": "^3.3 || ^4.0", "knplabs/knp-menu-bundle": "^3.0", "laminas/laminas-stdlib": "^3.18", "pagerfanta/doctrine-orm-adapter": "^4.6", "pagerfanta/twig": "^4.6", - "sylius/grid-bundle": "^1.13", - "sylius/resource-bundle": "^1.11", - "symfony/asset": "^6.4 || ^7.0", - "symfony/asset-mapper": "^6.4 || ^7.0", - "symfony/config": "^6.4 || ^7.0", - "symfony/dependency-injection": "^6.4 || ^7.0", - "symfony/expression-language": "^6.4 || ^7.0", - "symfony/http-client": "^6.4 || ^7.0", - "symfony/http-kernel": "^6.4 || ^7.0", - "symfony/security-bundle": "^6.4 || ^7.0", - "symfony/security-http": "^6.4 || ^7.0", - "symfony/stopwatch": "^6.4 || ^7.0", - "symfony/twig-bundle": "^6.4 || ^7.0", + "sylius/grid-bundle": "^1.13 || ^1.15@alpha", + "sylius/resource-bundle": "^1.13 || ^1.14@alpha", + "symfony/asset": "^6.4 || ^7.4 || ^8.0", + "symfony/asset-mapper": "^6.4 || ^7.4 || ^8.0", + "symfony/config": "^6.4 || ^7.4 || ^8.0", + "symfony/dependency-injection": "^6.4 || ^7.4 || ^8.0", + "symfony/expression-language": "^6.4 || ^7.4 || ^8.0", + "symfony/filesystem": "^6.4 || ^7.4 || ^8.0", + "symfony/http-client": "^6.4 || ^7.4 || ^8.0", + "symfony/http-kernel": "^6.4 || ^7.4 || ^8.0", + "symfony/security-bundle": "^6.4 || ^7.4 || ^8.0", + "symfony/security-http": "^6.4 || ^7.4 || ^8.0", + "symfony/stopwatch": "^6.4 || ^7.4 || ^8.0", + "symfony/twig-bundle": "^6.4 || ^7.4 || ^8.0", "symfony/ux-autocomplete": "^2.17", "symfony/ux-icons": "^2.20", "symfony/ux-live-component": "^2.17", @@ -43,26 +44,26 @@ "webmozart/assert": "^1.9" }, "require-dev": { - "doctrine/doctrine-fixtures-bundle": "^3.6", - "matthiasnoback/symfony-config-test": "^5.1", - "matthiasnoback/symfony-dependency-injection-test": "^5.1", + "doctrine/doctrine-fixtures-bundle": "^4.3", + "matthiasnoback/symfony-config-test": "^6.1", + "matthiasnoback/symfony-dependency-injection-test": "^6.1.0", "phpstan/phpstan": "^1.10", "phpstan/phpstan-symfony": "^1.3", - "phpunit/phpunit": "^9.6", + "phpunit/phpunit": "^10.5", "sylius-labs/coding-standard": "^4.0", - "symfony/browser-kit": "^6.4 || ^7.0", - "symfony/console": "^6.4 || ^7.0", - "symfony/css-selector": "^6.4 || ^7.0", - "symfony/debug-bundle": "^6.4 || ^7.0", - "symfony/dom-crawler": "^6.4 || ^7.0", - "symfony/dotenv": "^6.4 || ^7.0", + "symfony/browser-kit": "^6.4 || ^7.4 || ^8.0", + "symfony/console": "^6.4 || ^7.4 || ^8.0", + "symfony/css-selector": "^6.4 || ^7.4 || ^8.0", + "symfony/debug-bundle": "^6.4 || ^7.4 || ^8.0", + "symfony/dom-crawler": "^6.4 || ^7.4 || ^8.0", + "symfony/dotenv": "^6.4 || ^7.4 || ^8.0", "symfony/flex": "^2.4", - "symfony/framework-bundle": "^6.4 || ^7.0", + "symfony/framework-bundle": "^6.4 || ^7.4 || ^8.0", "symfony/maker-bundle": "^1.61", - "symfony/runtime": "^6.4 || ^7.0", - "symfony/translation": "^6.4 || ^7.0", - "symfony/web-profiler-bundle": "^6.4 || ^7.0", - "symfony/yaml": "^6.4 || ^7.0", + "symfony/runtime": "^6.4 || ^7.4 || ^8.0", + "symfony/translation": "^6.4 || ^7.4 || ^8.0", + "symfony/web-profiler-bundle": "^6.4 || ^7.4 || ^8.0", + "symfony/yaml": "^6.4 || ^7.4 || ^8.0", "symplify/monorepo-builder": "11.2.*", "vich/uploader-bundle": "^2.4", "zenstruck/foundry": "^2.0" @@ -108,7 +109,7 @@ }, "extra": { "symfony": { - "require": "7.1.*" + "require": "8.0.*" } }, "scripts": { diff --git a/config/packages/doctrine.yaml b/config/packages/doctrine.yaml index 2942e16f8..cb21798e6 100644 --- a/config/packages/doctrine.yaml +++ b/config/packages/doctrine.yaml @@ -7,11 +7,7 @@ doctrine: #server_version: '16' profiling_collect_backtrace: '%kernel.debug%' - use_savepoints: true orm: - auto_generate_proxy_classes: true - enable_lazy_ghost_objects: true - report_fields_where_declared: true validate_xml_mapping: true naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware auto_mapping: true diff --git a/config/reference.php b/config/reference.php new file mode 100644 index 000000000..0d1559a7a --- /dev/null +++ b/config/reference.php @@ -0,0 +1,1685 @@ + [ + * 'App\\' => [ + * 'resource' => '../src/', + * ], + * ], + * ]); + * ``` + * + * @psalm-type ImportsConfig = list + * @psalm-type ParametersConfig = array|null>|null> + * @psalm-type ArgumentsType = list|array + * @psalm-type CallType = array|array{0:string, 1?:ArgumentsType, 2?:bool}|array{method:string, arguments?:ArgumentsType, returns_clone?:bool} + * @psalm-type TagsType = list>> // arrays inside the list must have only one element, with the tag name as the key + * @psalm-type CallbackType = string|array{0:string|ReferenceConfigurator,1:string}|\Closure|ReferenceConfigurator|ExpressionConfigurator + * @psalm-type DeprecationType = array{package: string, version: string, message?: string} + * @psalm-type DefaultsType = array{ + * public?: bool, + * tags?: TagsType, + * resource_tags?: TagsType, + * autowire?: bool, + * autoconfigure?: bool, + * bind?: array, + * } + * @psalm-type InstanceofType = array{ + * shared?: bool, + * lazy?: bool|string, + * public?: bool, + * properties?: array, + * configurator?: CallbackType, + * calls?: list, + * tags?: TagsType, + * resource_tags?: TagsType, + * autowire?: bool, + * bind?: array, + * constructor?: string, + * } + * @psalm-type DefinitionType = array{ + * class?: string, + * file?: string, + * parent?: string, + * shared?: bool, + * synthetic?: bool, + * lazy?: bool|string, + * public?: bool, + * abstract?: bool, + * deprecated?: DeprecationType, + * factory?: CallbackType, + * configurator?: CallbackType, + * arguments?: ArgumentsType, + * properties?: array, + * calls?: list, + * tags?: TagsType, + * resource_tags?: TagsType, + * decorates?: string, + * decoration_inner_name?: string, + * decoration_priority?: int, + * decoration_on_invalid?: 'exception'|'ignore'|null, + * autowire?: bool, + * autoconfigure?: bool, + * bind?: array, + * constructor?: string, + * from_callable?: CallbackType, + * } + * @psalm-type AliasType = string|array{ + * alias: string, + * public?: bool, + * deprecated?: DeprecationType, + * } + * @psalm-type PrototypeType = array{ + * resource: string, + * namespace?: string, + * exclude?: string|list, + * parent?: string, + * shared?: bool, + * lazy?: bool|string, + * public?: bool, + * abstract?: bool, + * deprecated?: DeprecationType, + * factory?: CallbackType, + * arguments?: ArgumentsType, + * properties?: array, + * configurator?: CallbackType, + * calls?: list, + * tags?: TagsType, + * resource_tags?: TagsType, + * autowire?: bool, + * autoconfigure?: bool, + * bind?: array, + * constructor?: string, + * } + * @psalm-type StackType = array{ + * stack: list>, + * public?: bool, + * deprecated?: DeprecationType, + * } + * @psalm-type ServicesConfig = array{ + * _defaults?: DefaultsType, + * _instanceof?: InstanceofType, + * ... + * } + * @psalm-type ExtensionType = array + * @psalm-type FrameworkConfig = array{ + * secret?: scalar|null|Param, + * 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, + * trust_x_sendfile_type_header?: scalar|null|Param, // Set true to enable support for xsendfile in binary file responses. // Default: "%env(bool:default::SYMFONY_TRUST_X_SENDFILE_TYPE_HEADER)%" + * ide?: scalar|null|Param, // Default: "%env(default::SYMFONY_IDE)%" + * test?: bool|Param, + * default_locale?: scalar|null|Param, // Default: "en" + * 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_proxies?: mixed, // Default: ["%env(default::SYMFONY_TRUSTED_PROXIES)%"] + * trusted_headers?: list, + * error_controller?: scalar|null|Param, // Default: "error_controller" + * handle_all_throwables?: bool|Param, // HttpKernel will handle all kinds of \Throwable. // Default: true + * csrf_protection?: bool|array{ + * enabled?: scalar|null|Param, // Default: null + * stateless_token_ids?: list, + * check_header?: scalar|null|Param, // Whether to check the CSRF token in a header in addition to a cookie when using stateless protection. // Default: false + * cookie_name?: scalar|null|Param, // The name of the cookie to use when using stateless protection. // Default: "csrf-token" + * }, + * form?: bool|array{ // Form configuration + * enabled?: bool|Param, // Default: true + * csrf_protection?: array{ + * enabled?: scalar|null|Param, // Default: null + * token_id?: scalar|null|Param, // Default: null + * field_name?: scalar|null|Param, // Default: "_token" + * field_attr?: array, + * }, + * }, + * http_cache?: bool|array{ // HTTP cache configuration + * enabled?: bool|Param, // Default: false + * debug?: bool|Param, // Default: "%kernel.debug%" + * trace_level?: "none"|"short"|"full"|Param, + * trace_header?: scalar|null|Param, + * default_ttl?: int|Param, + * private_headers?: list, + * skip_response_headers?: list, + * allow_reload?: bool|Param, + * allow_revalidate?: bool|Param, + * stale_while_revalidate?: int|Param, + * stale_if_error?: int|Param, + * terminate_on_cache_hit?: bool|Param, + * }, + * esi?: bool|array{ // ESI configuration + * enabled?: bool|Param, // Default: false + * }, + * ssi?: bool|array{ // SSI configuration + * enabled?: bool|Param, // Default: false + * }, + * fragments?: bool|array{ // Fragments configuration + * enabled?: bool|Param, // Default: false + * hinclude_default_template?: scalar|null|Param, // Default: null + * path?: scalar|null|Param, // Default: "/_fragment" + * }, + * profiler?: bool|array{ // Profiler configuration + * enabled?: bool|Param, // Default: false + * collect?: bool|Param, // Default: true + * collect_parameter?: scalar|null|Param, // The name of the parameter to use to enable or disable collection on a per request basis. // Default: null + * only_exceptions?: bool|Param, // Default: false + * only_main_requests?: bool|Param, // Default: false + * dsn?: scalar|null|Param, // Default: "file:%kernel.cache_dir%/profiler" + * collect_serializer_data?: true|Param, // Default: true + * }, + * workflows?: bool|array{ + * enabled?: bool|Param, // Default: false + * workflows?: array, + * definition_validators?: list, + * support_strategy?: scalar|null|Param, + * initial_marking?: list, + * events_to_dispatch?: list|null, + * places?: list, + * }>, + * transitions: list, + * to?: list, + * weight?: int|Param, // Default: 1 + * metadata?: list, + * }>, + * metadata?: list, + * }>, + * }, + * router?: bool|array{ // Router configuration + * enabled?: bool|Param, // Default: false + * resource: scalar|null|Param, + * type?: scalar|null|Param, + * default_uri?: scalar|null|Param, // The default URI used to generate URLs in a non-HTTP context. // Default: null + * http_port?: scalar|null|Param, // Default: 80 + * https_port?: scalar|null|Param, // Default: 443 + * strict_requirements?: scalar|null|Param, // set to true to throw an exception when a parameter does not match the requirements set to false to disable exceptions when a parameter does not match the requirements (and return null instead) set to null to disable parameter checks against requirements 'true' is the preferred configuration in development mode, while 'false' or 'null' might be preferred in production // Default: true + * utf8?: bool|Param, // Default: true + * }, + * session?: bool|array{ // Session configuration + * enabled?: bool|Param, // Default: false + * storage_factory_id?: scalar|null|Param, // Default: "session.storage.factory.native" + * handler_id?: scalar|null|Param, // Defaults to using the native session handler, or to the native *file* session handler if "save_path" is not null. + * name?: scalar|null|Param, + * cookie_lifetime?: scalar|null|Param, + * cookie_path?: scalar|null|Param, + * cookie_domain?: scalar|null|Param, + * cookie_secure?: true|false|"auto"|Param, // Default: "auto" + * cookie_httponly?: bool|Param, // Default: true + * cookie_samesite?: null|"lax"|"strict"|"none"|Param, // Default: "lax" + * use_cookies?: bool|Param, + * gc_divisor?: scalar|null|Param, + * gc_probability?: scalar|null|Param, + * gc_maxlifetime?: scalar|null|Param, + * save_path?: scalar|null|Param, // Defaults to "%kernel.cache_dir%/sessions" if the "handler_id" option is not null. + * metadata_update_threshold?: int|Param, // Seconds to wait between 2 session metadata updates. // Default: 0 + * }, + * request?: bool|array{ // Request configuration + * enabled?: bool|Param, // Default: false + * formats?: array>, + * }, + * assets?: bool|array{ // Assets configuration + * enabled?: bool|Param, // Default: true + * strict_mode?: bool|Param, // Throw an exception if an entry is missing from the manifest.json. // Default: false + * version_strategy?: scalar|null|Param, // Default: null + * version?: scalar|null|Param, // Default: null + * version_format?: scalar|null|Param, // Default: "%%s?%%s" + * json_manifest_path?: scalar|null|Param, // Default: null + * base_path?: scalar|null|Param, // Default: "" + * base_urls?: list, + * packages?: array, + * }>, + * }, + * asset_mapper?: bool|array{ // Asset Mapper configuration + * enabled?: bool|Param, // Default: true + * paths?: 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 + * public_prefix?: scalar|null|Param, // The public path where the assets will be written to (and served from when "server" is true). // Default: "/assets/" + * missing_import_mode?: "strict"|"warn"|"ignore"|Param, // Behavior if an asset cannot be found when imported from JavaScript or CSS files - e.g. "import './non-existent.js'". "strict" means an exception is thrown, "warn" means a warning is logged, "ignore" means the import is left as-is. // Default: "warn" + * extensions?: array, + * importmap_path?: scalar|null|Param, // The path of the importmap.php file. // Default: "%kernel.project_dir%/importmap.php" + * importmap_polyfill?: scalar|null|Param, // The importmap name that will be used to load the polyfill. Set to false to disable. // Default: "es-module-shims" + * importmap_script_attributes?: array, + * vendor_dir?: scalar|null|Param, // The directory to store JavaScript vendors. // Default: "%kernel.project_dir%/assets/vendor" + * precompress?: bool|array{ // Precompress assets with Brotli, Zstandard and gzip. + * enabled?: bool|Param, // Default: false + * formats?: list, + * extensions?: list, + * }, + * }, + * translator?: bool|array{ // Translator configuration + * enabled?: bool|Param, // Default: true + * fallbacks?: list, + * logging?: bool|Param, // Default: false + * formatter?: scalar|null|Param, // Default: "translator.formatter.default" + * cache_dir?: scalar|null|Param, // Default: "%kernel.cache_dir%/translations" + * default_path?: scalar|null|Param, // The default path used to load translations. // Default: "%kernel.project_dir%/translations" + * paths?: list, + * pseudo_localization?: bool|array{ + * enabled?: bool|Param, // Default: false + * accents?: bool|Param, // Default: true + * expansion_factor?: float|Param, // Default: 1.0 + * brackets?: bool|Param, // Default: true + * parse_html?: bool|Param, // Default: false + * localizable_html_attributes?: list, + * }, + * providers?: array, + * locales?: list, + * }>, + * globals?: array, + * domain?: string|Param, + * }>, + * }, + * validation?: bool|array{ // Validation configuration + * enabled?: bool|Param, // Default: true + * enable_attributes?: bool|Param, // Default: true + * static_method?: list, + * translation_domain?: scalar|null|Param, // Default: "validators" + * email_validation_mode?: "html5"|"html5-allow-no-tld"|"strict"|Param, // Default: "html5" + * mapping?: array{ + * paths?: list, + * }, + * not_compromised_password?: bool|array{ + * enabled?: bool|Param, // When disabled, compromised passwords will be accepted as valid. // Default: true + * endpoint?: scalar|null|Param, // API endpoint for the NotCompromisedPassword Validator. // Default: null + * }, + * disable_translation?: bool|Param, // Default: false + * auto_mapping?: array, + * }>, + * }, + * serializer?: bool|array{ // Serializer configuration + * enabled?: bool|Param, // Default: false + * enable_attributes?: bool|Param, // Default: true + * name_converter?: scalar|null|Param, + * circular_reference_handler?: scalar|null|Param, + * max_depth_handler?: scalar|null|Param, + * mapping?: array{ + * paths?: list, + * }, + * default_context?: list, + * 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 + * }>, + * }, + * property_access?: bool|array{ // Property access configuration + * enabled?: bool|Param, // Default: true + * magic_call?: bool|Param, // Default: false + * magic_get?: bool|Param, // Default: true + * magic_set?: bool|Param, // Default: true + * throw_exception_on_invalid_index?: bool|Param, // Default: false + * throw_exception_on_invalid_property_path?: bool|Param, // Default: true + * }, + * type_info?: bool|array{ // Type info configuration + * enabled?: bool|Param, // Default: true + * aliases?: array, + * }, + * property_info?: bool|array{ // Property info configuration + * enabled?: bool|Param, // Default: true + * with_constructor_extractor?: bool|Param, // Registers the constructor extractor. // Default: true + * }, + * cache?: array{ // Cache configuration + * prefix_seed?: scalar|null|Param, // Used to namespace cache keys when using several apps with the same shared backend. // Default: "_%kernel.project_dir%.%kernel.container_class%" + * app?: scalar|null|Param, // App related cache pools configuration. // Default: "cache.adapter.filesystem" + * system?: scalar|null|Param, // System related cache pools configuration. // Default: "cache.adapter.system" + * directory?: scalar|null|Param, // Default: "%kernel.share_dir%/pools/app" + * default_psr6_provider?: scalar|null|Param, + * default_redis_provider?: scalar|null|Param, // Default: "redis://localhost" + * default_valkey_provider?: scalar|null|Param, // Default: "valkey://localhost" + * default_memcached_provider?: scalar|null|Param, // Default: "memcached://localhost" + * default_doctrine_dbal_provider?: scalar|null|Param, // Default: "database_connection" + * default_pdo_provider?: scalar|null|Param, // Default: null + * pools?: array, + * tags?: scalar|null|Param, // Default: null + * public?: bool|Param, // Default: false + * default_lifetime?: scalar|null|Param, // Default lifetime of the pool. + * provider?: scalar|null|Param, // Overwrite the setting from the default provider for this adapter. + * early_expiration_message_bus?: scalar|null|Param, + * clearer?: scalar|null|Param, + * }>, + * }, + * 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, + * web_link?: bool|array{ // Web links configuration + * enabled?: bool|Param, // Default: false + * }, + * lock?: bool|string|array{ // Lock configuration + * enabled?: bool|Param, // Default: false + * resources?: array>, + * }, + * semaphore?: bool|string|array{ // Semaphore configuration + * enabled?: bool|Param, // Default: false + * resources?: array, + * }, + * messenger?: bool|array{ // Messenger configuration + * enabled?: bool|Param, // Default: false + * routing?: array, + * }>, + * serializer?: array{ + * default_serializer?: scalar|null|Param, // Service id to use as the default serializer for the transports. // Default: "messenger.transport.native_php_serializer" + * symfony_serializer?: array{ + * format?: scalar|null|Param, // Serialization format for the messenger.transport.symfony_serializer service (which is not the serializer used by default). // Default: "json" + * context?: array, + * }, + * }, + * transports?: array, + * failure_transport?: scalar|null|Param, // Transport name to send failed messages to (after all retries have failed). // Default: null + * retry_strategy?: string|array{ + * service?: scalar|null|Param, // 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|null|Param, // Rate limiter name to use when processing messages. // Default: null + * }>, + * failure_transport?: scalar|null|Param, // Transport name to send failed messages to (after all retries have failed). // Default: null + * stop_worker_on_signals?: list, + * default_bus?: scalar|null|Param, // Default: null + * buses?: array, + * }>, + * }>, + * }, + * scheduler?: bool|array{ // Scheduler configuration + * enabled?: bool|Param, // Default: false + * }, + * disallow_search_engine_index?: bool|Param, // Enabled by default when debug is enabled. // Default: true + * http_client?: bool|array{ // HTTP Client configuration + * enabled?: bool|Param, // Default: true + * max_host_connections?: int|Param, // The maximum number of connections to a single host. + * default_options?: array{ + * headers?: array, + * vars?: array, + * max_redirects?: int|Param, // The maximum number of redirects to follow. + * http_version?: scalar|null|Param, // The default HTTP version, typically 1.1 or 2.0, leave to null for the best version. + * resolve?: array, + * proxy?: scalar|null|Param, // The URL of the proxy to pass requests through or null for automatic detection. + * no_proxy?: scalar|null|Param, // 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|null|Param, // 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|null|Param, // A certificate authority file. + * capath?: scalar|null|Param, // A directory that contains multiple certificate authority files. + * local_cert?: scalar|null|Param, // A PEM formatted certificate file. + * local_pk?: scalar|null|Param, // A private key file. + * passphrase?: scalar|null|Param, // The passphrase used to encrypt the "local_pk" file. + * ciphers?: scalar|null|Param, // 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|null|Param, // The minimum version of TLS to accept; must be one of STREAM_CRYPTO_METHOD_TLSv*_CLIENT constants. + * extra?: array, + * rate_limiter?: scalar|null|Param, // 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|null|Param, // 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 + * }, + * }, + * mock_response_factory?: scalar|null|Param, // 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|null|Param, // The default HTTP version, typically 1.1 or 2.0, leave to null for the best version. + * resolve?: array, + * proxy?: scalar|null|Param, // The URL of the proxy to pass requests through or null for automatic detection. + * no_proxy?: scalar|null|Param, // 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|null|Param, // 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|null|Param, // A certificate authority file. + * capath?: scalar|null|Param, // A directory that contains multiple certificate authority files. + * local_cert?: scalar|null|Param, // A PEM formatted certificate file. + * local_pk?: scalar|null|Param, // A private key file. + * passphrase?: scalar|null|Param, // The passphrase used to encrypt the "local_pk" file. + * ciphers?: scalar|null|Param, // 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|null|Param, // The minimum version of TLS to accept; must be one of STREAM_CRYPTO_METHOD_TLSv*_CLIENT constants. + * extra?: array, + * rate_limiter?: scalar|null|Param, // 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|null|Param, // 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 + * }, + * }>, + * }, + * mailer?: bool|array{ // Mailer configuration + * enabled?: bool|Param, // Default: false + * message_bus?: scalar|null|Param, // The message bus to use. Defaults to the default bus if the Messenger component is installed. // Default: null + * dsn?: scalar|null|Param, // Default: null + * transports?: array, + * envelope?: array{ // Mailer Envelope configuration + * sender?: scalar|null|Param, + * recipients?: list, + * allowed_recipients?: list, + * }, + * headers?: array, + * dkim_signer?: bool|array{ // DKIM signer configuration + * enabled?: bool|Param, // Default: false + * key?: scalar|null|Param, // Key content, or path to key (in PEM format with the `file://` prefix) // Default: "" + * domain?: scalar|null|Param, // Default: "" + * select?: scalar|null|Param, // Default: "" + * passphrase?: scalar|null|Param, // The private key passphrase // Default: "" + * options?: array, + * }, + * smime_signer?: bool|array{ // S/MIME signer configuration + * enabled?: bool|Param, // Default: false + * key?: scalar|null|Param, // Path to key (in PEM format) // Default: "" + * certificate?: scalar|null|Param, // Path to certificate (in PEM format without the `file://` prefix) // Default: "" + * passphrase?: scalar|null|Param, // The private key passphrase // Default: null + * extra_certificates?: scalar|null|Param, // Default: null + * sign_options?: int|Param, // Default: null + * }, + * smime_encrypter?: bool|array{ // S/MIME encrypter configuration + * enabled?: bool|Param, // Default: false + * repository?: scalar|null|Param, // S/MIME certificate repository service. This service shall implement the `Symfony\Component\Mailer\EventListener\SmimeCertificateRepositoryInterface`. // Default: "" + * cipher?: int|Param, // A set of algorithms used to encrypt the message // Default: null + * }, + * }, + * secrets?: bool|array{ + * enabled?: bool|Param, // Default: true + * vault_directory?: scalar|null|Param, // Default: "%kernel.project_dir%/config/secrets/%kernel.runtime_environment%" + * local_dotenv_file?: scalar|null|Param, // Default: "%kernel.project_dir%/.env.%kernel.runtime_environment%.local" + * decryption_env_var?: scalar|null|Param, // Default: "base64:default::SYMFONY_DECRYPTION_SECRET" + * }, + * notifier?: bool|array{ // Notifier configuration + * enabled?: bool|Param, // Default: false + * message_bus?: scalar|null|Param, // The message bus to use. Defaults to the default bus if the Messenger component is installed. // Default: null + * chatter_transports?: array, + * texter_transports?: array, + * notification_on_failed_messages?: bool|Param, // Default: false + * channel_policy?: array>, + * admin_recipients?: list, + * }, + * rate_limiter?: bool|array{ // Rate limiter configuration + * enabled?: bool|Param, // Default: false + * limiters?: array, + * limit?: int|Param, // The maximum allowed hits in a fixed interval or burst. + * interval?: scalar|null|Param, // 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|null|Param, // 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: false + * default_uuid_version?: 7|6|4|1|Param, // Default: 7 + * name_based_uuid_version?: 5|3|Param, // Default: 5 + * name_based_uuid_namespace?: scalar|null|Param, + * time_based_uuid_version?: 7|6|1|Param, // Default: 7 + * time_based_uuid_node?: scalar|null|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 + * }>, + * }, + * webhook?: bool|array{ // Webhook configuration + * enabled?: bool|Param, // Default: false + * message_bus?: scalar|null|Param, // The message bus to use. // Default: "messenger.default_bus" + * routing?: array, + * }, + * remote-event?: bool|array{ // RemoteEvent configuration + * enabled?: bool|Param, // Default: false + * }, + * json_streamer?: bool|array{ // JSON streamer configuration + * enabled?: bool|Param, // Default: false + * }, + * } + * @psalm-type TwigConfig = array{ + * form_themes?: list, + * globals?: array, + * autoescape_service?: scalar|null|Param, // Default: null + * autoescape_service_method?: scalar|null|Param, // Default: null + * cache?: scalar|null|Param, // Default: true + * charset?: scalar|null|Param, // Default: "%kernel.charset%" + * debug?: bool|Param, // Default: "%kernel.debug%" + * strict_variables?: bool|Param, // Default: "%kernel.debug%" + * auto_reload?: scalar|null|Param, + * optimizations?: int|Param, + * default_path?: scalar|null|Param, // The default path used to load templates. // Default: "%kernel.project_dir%/templates" + * file_name_pattern?: list, + * paths?: array, + * date?: array{ // The default format options used by the date filter. + * format?: scalar|null|Param, // Default: "F j, Y H:i" + * interval_format?: scalar|null|Param, // Default: "%d days" + * timezone?: scalar|null|Param, // The timezone used when formatting dates, when set to null, the timezone returned by date_default_timezone_get() is used. // Default: null + * }, + * number_format?: array{ // The default format options for the number_format filter. + * decimals?: int|Param, // Default: 0 + * decimal_point?: scalar|null|Param, // Default: "." + * thousands_separator?: scalar|null|Param, // Default: "," + * }, + * mailer?: array{ + * html_to_text_converter?: scalar|null|Param, // A service implementing the "Symfony\Component\Mime\HtmlToTextConverter\HtmlToTextConverterInterface". // Default: null + * }, + * } + * @psalm-type TwigComponentConfig = array{ + * defaults?: array, + * anonymous_template_directory?: scalar|null|Param, // Defaults to `components` + * profiler?: bool|Param, // Enables the profiler for Twig Component (in debug mode) // Default: "%kernel.debug%" + * controllers_json?: scalar|null|Param, // Deprecated: The "twig_component.controllers_json" config option is deprecated, and will be removed in 3.0. // Default: null + * } + * @psalm-type SyliusTwigHooksConfig = array{ + * enable_autoprefixing?: bool|Param, // Default: false + * hook_name_section_separator?: scalar|null|Param, // Default: false + * supported_hookable_types?: array, + * hooks?: array, + * props?: array, + * configuration?: array, + * priority?: int|Param, // Default: null + * }>>, + * } + * @psalm-type SyliusTwigExtraConfig = array{ + * twig_ux?: array{ + * anonymous_component_template_prefixes?: array, + * }, + * } + * @psalm-type SyliusAdminUiConfig = array{ + * routing?: array, + * } + * @psalm-type SyliusUiTranslationsConfig = array + * @psalm-type SyliusBootstrapAdminUiConfig = array + * @psalm-type WebProfilerConfig = array{ + * toolbar?: bool|array{ // Profiler toolbar configuration + * enabled?: bool|Param, // Default: false + * ajax_replace?: bool|Param, // Replace toolbar on AJAX requests // Default: false + * }, + * intercept_redirects?: bool|Param, // Default: false + * excluded_ajax_paths?: scalar|null|Param, // Default: "^/((index|app(_[\\w]+)?)\\.php/)?_wdt" + * } + * @psalm-type DebugConfig = array{ + * max_items?: int|Param, // Max number of displayed items past the first level, -1 means no limit. // Default: 2500 + * min_depth?: int|Param, // Minimum tree depth to clone all the items, 1 is default. // Default: 1 + * max_string_length?: int|Param, // Max length of displayed strings, -1 means no limit. // Default: -1 + * dump_destination?: scalar|null|Param, // A stream URL where dumps should be written to. // Default: null + * theme?: "dark"|"light"|Param, // Changes the color of the dump() output when rendered directly on the templating. "dark" (default) or "light". // Default: "dark" + * } + * @psalm-type LiveComponentConfig = array{ + * secret?: scalar|null|Param, // The secret used to compute fingerprints and checksums // Default: "%kernel.secret%" + * } + * @psalm-type StimulusConfig = array{ + * controller_paths?: list, + * controllers_json?: scalar|null|Param, // Default: "%kernel.project_dir%/assets/controllers.json" + * } + * @psalm-type DoctrineConfig = array{ + * dbal?: array{ + * default_connection?: scalar|null|Param, + * types?: array, + * driver_schemes?: array, + * connections?: array, + * mapping_types?: array, + * default_table_options?: array, + * schema_manager_factory?: scalar|null|Param, // Default: "doctrine.dbal.default_schema_manager_factory" + * result_cache?: scalar|null|Param, + * replicas?: array, + * }>, + * }, + * orm?: array{ + * default_entity_manager?: scalar|null|Param, + * enable_native_lazy_objects?: bool|Param, // Deprecated: The "enable_native_lazy_objects" option is deprecated and will be removed in DoctrineBundle 4.0, as native lazy objects are now always enabled. // Default: true + * controller_resolver?: bool|array{ + * enabled?: bool|Param, // Default: true + * auto_mapping?: bool|Param, // Deprecated: The "doctrine.orm.controller_resolver.auto_mapping.auto_mapping" option is deprecated and will be removed in DoctrineBundle 4.0, as it only accepts `false` since 3.0. // Set to true to enable using route placeholders as lookup criteria when the primary key doesn't match the argument name // Default: false + * evict_cache?: bool|Param, // Set to true to fetch the entity from the database instead of using the cache, if any // Default: false + * }, + * entity_managers?: array, + * }>, + * }>, + * }, + * connection?: scalar|null|Param, + * class_metadata_factory_name?: scalar|null|Param, // Default: "Doctrine\\ORM\\Mapping\\ClassMetadataFactory" + * default_repository_class?: scalar|null|Param, // Default: "Doctrine\\ORM\\EntityRepository" + * auto_mapping?: scalar|null|Param, // Default: false + * naming_strategy?: scalar|null|Param, // Default: "doctrine.orm.naming_strategy.default" + * quote_strategy?: scalar|null|Param, // Default: "doctrine.orm.quote_strategy.default" + * typed_field_mapper?: scalar|null|Param, // Default: "doctrine.orm.typed_field_mapper.default" + * entity_listener_resolver?: scalar|null|Param, // Default: null + * fetch_mode_subselect_batch_size?: scalar|null|Param, + * repository_factory?: scalar|null|Param, // Default: "doctrine.orm.container_repository_factory" + * schema_ignore_classes?: list, + * 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 and will be mandatory in ORM 3.0. See https://github.com/doctrine/orm/pull/6728. // Default: false + * second_level_cache?: array{ + * region_cache_driver?: string|array{ + * type?: scalar|null|Param, // Default: null + * id?: scalar|null|Param, + * pool?: scalar|null|Param, + * }, + * region_lock_lifetime?: scalar|null|Param, // Default: 60 + * log_enabled?: bool|Param, // Default: true + * region_lifetime?: scalar|null|Param, // Default: 3600 + * enabled?: bool|Param, // Default: true + * factory?: scalar|null|Param, + * regions?: array, + * loggers?: array, + * }, + * hydrators?: array, + * mappings?: array, + * dql?: array{ + * string_functions?: array, + * numeric_functions?: array, + * datetime_functions?: array, + * }, + * filters?: array, + * }>, + * identity_generation_preferences?: array, + * }>, + * resolve_target_entities?: array, + * }, + * } + * @psalm-type BabdevPagerfantaConfig = array{ + * default_view?: scalar|null|Param, // Default: "default" + * default_twig_template?: scalar|null|Param, // Default: "@BabDevPagerfanta/default.html.twig" + * exceptions_strategy?: array{ + * out_of_range_page?: "to_http_not_found"|"custom"|Param, // Default: "to_http_not_found" + * not_valid_current_page?: "to_http_not_found"|"custom"|Param, // Default: "to_http_not_found" + * }, + * } + * @psalm-type SyliusResourceConfig = array{ + * resources?: array, + * settings?: array{ + * paginate?: mixed, // Default: null + * limit?: mixed, // Default: null + * allowed_paginate?: list, + * default_page_size?: int|Param, // Default: 10 + * default_templates_dir?: scalar|null|Param, // Default: null + * sortable?: bool|Param, // Default: false + * sorting?: mixed, // Default: null + * filterable?: bool|Param, // Default: false + * criteria?: mixed, // Default: null + * state_machine_component?: scalar|null|Param, // Default: null + * }, + * translation?: bool|array{ + * enabled?: bool|Param, // Default: true + * locale_provider?: scalar|null|Param, // Default: "sylius.translation_locale_provider.immutable" + * }, + * drivers?: list<"doctrine/orm"|"doctrine/mongodb-odm"|"doctrine/phpcr-odm"|Param>, + * mapping?: array{ + * imports?: list, + * paths?: list, + * }, + * authorization_checker?: scalar|null|Param, // Default: "sylius.resource_controller.authorization_checker.disabled" + * routing_path_bc_layer?: bool|Param, + * path_segment_name_generator?: scalar|null|Param, // Specify a path name generator to use. // Default: "sylius.metadata.path_segment_name_generator.dash" + * } + * @psalm-type ZenstruckFoundryConfig = array{ + * auto_refresh_proxies?: bool|null|Param, // Deprecated: Since 2.0 auto_refresh_proxies defaults to true and this configuration has no effect. // Whether to auto-refresh proxies by default (https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#auto-refresh) // Default: null + * enable_auto_refresh_with_lazy_objects?: bool|null|Param, // Enable auto-refresh using PHP 8.4 lazy objects (cannot be enabled if PHP < 8.4). // Default: null + * faker?: array{ // Configure the faker used by your factories. + * locale?: scalar|null|Param, // The default locale to use for faker. // Default: null + * seed?: scalar|null|Param, // Deprecated: The "faker.seed" configuration is deprecated and will be removed in 3.0. Use environment variable "FOUNDRY_FAKER_SEED" instead. // Random number generator seed to produce the same fake values every run. // Default: null + * service?: scalar|null|Param, // Service id for custom faker instance. // Default: null + * }, + * instantiator?: array{ // Configure the default instantiator used by your object factories. + * use_constructor?: bool|Param, // Use the constructor to instantiate objects. // Default: true + * allow_extra_attributes?: bool|Param, // Whether or not to skip attributes that do not correspond to properties. // Default: false + * always_force_properties?: bool|Param, // Whether or not to skip setters and force set object properties (public/private/protected) directly. // Default: false + * service?: scalar|null|Param, // Service id of your custom instantiator. // Default: null + * }, + * global_state?: list, + * persistence?: array{ + * flush_once?: bool|Param, // Flush only once per call of `PersistentObjectFactory::create()` in userland. // Default: false + * }, + * orm?: array{ + * auto_persist?: bool|Param, // Deprecated: Since 2.4 auto_persist defaults to true and this configuration has no effect. // Automatically persist entities when created. // Default: true + * reset?: array{ + * connections?: list, + * entity_managers?: list, + * mode?: \Zenstruck\Foundry\ORM\ResetDatabase\ResetDatabaseMode::SCHEMA|\Zenstruck\Foundry\ORM\ResetDatabase\ResetDatabaseMode::MIGRATE|Param, // Reset mode to use with ResetDatabase trait // Default: "schema" + * migrations?: array{ + * configurations?: list, + * }, + * }, + * }, + * mongo?: array{ + * auto_persist?: bool|Param, // Deprecated: Since 2.4 auto_persist defaults to true and this configuration has no effect. // Automatically persist documents when created. // Default: true + * reset?: array{ + * document_managers?: list, + * }, + * }, + * make_factory?: array{ + * default_namespace?: scalar|null|Param, // Default namespace where factories will be created by maker. // Default: "Factory" + * add_hints?: bool|Param, // Add "beginner" hints in the created factory. // Default: true + * }, + * make_story?: array{ + * default_namespace?: scalar|null|Param, // Default namespace where stories will be created by maker. // Default: "Story" + * }, + * } + * @psalm-type KnpMenuConfig = array{ + * providers?: array{ + * builder_alias?: bool|Param, // Default: true + * }, + * twig?: array{ + * template?: scalar|null|Param, // Default: "@KnpMenu/menu.html.twig" + * }, + * templating?: bool|Param, // Default: false + * default_renderer?: scalar|null|Param, // Default: "twig" + * } + * @psalm-type SyliusGridConfig = array{ + * drivers?: list<"doctrine/orm"|"doctrine/phpcr-odm"|Param>, + * templates?: array{ + * filter?: array, + * action?: array, + * bulk_action?: array, + * }, + * grids?: array, + * }, + * sorting?: array, + * limits?: list, + * fields?: array, + * }>, + * filters?: array, + * form_options?: list, + * default_value?: mixed, + * }>, + * actions?: array, + * }>>, + * removals?: array, + * }>, + * } + * @psalm-type VichUploaderConfig = array{ + * default_filename_attribute_suffix?: scalar|null|Param, // Default: "_name" + * db_driver: scalar|null|Param, + * storage?: scalar|null|Param, // Default: "file_system" + * use_flysystem_to_resolve_uri?: bool|Param, // Default: false + * twig?: scalar|null|Param, // twig requires templating // Default: true + * form?: scalar|null|Param, // Default: true + * metadata?: array{ + * cache?: scalar|null|Param, // Default: "file" + * type?: scalar|null|Param, // Default: "attribute" + * file_cache?: array{ + * dir?: scalar|null|Param, // Default: "%kernel.cache_dir%/vich_uploader" + * }, + * auto_detection?: bool|Param, // Default: true + * directories?: list, + * }, + * mappings?: array, + * } + * @psalm-type MakerConfig = array{ + * root_namespace?: scalar|null|Param, // Default: "App" + * generate_final_classes?: bool|Param, // Default: true + * generate_final_entities?: bool|Param, // Default: false + * } + * @psalm-type SecurityConfig = array{ + * access_denied_url?: scalar|null|Param, // Default: null + * session_fixation_strategy?: "none"|"migrate"|"invalidate"|Param, // Default: "migrate" + * expose_security_errors?: \Symfony\Component\Security\Http\Authentication\ExposeSecurityLevel::None|\Symfony\Component\Security\Http\Authentication\ExposeSecurityLevel::AccountStatus|\Symfony\Component\Security\Http\Authentication\ExposeSecurityLevel::All|Param, // Default: "none" + * erase_credentials?: bool|Param, // Default: true + * access_decision_manager?: array{ + * strategy?: "affirmative"|"consensus"|"unanimous"|"priority"|Param, + * service?: scalar|null|Param, + * strategy_service?: scalar|null|Param, + * allow_if_all_abstain?: bool|Param, // Default: false + * allow_if_equal_granted_denied?: bool|Param, // Default: true + * }, + * password_hashers?: array, + * hash_algorithm?: scalar|null|Param, // Name of hashing algorithm for PBKDF2 (i.e. sha256, sha512, etc..) See hash_algos() for a list of supported algorithms. // Default: "sha512" + * key_length?: scalar|null|Param, // Default: 40 + * ignore_case?: bool|Param, // Default: false + * encode_as_base64?: bool|Param, // Default: true + * iterations?: scalar|null|Param, // Default: 5000 + * cost?: int|Param, // Default: null + * memory_cost?: scalar|null|Param, // Default: null + * time_cost?: scalar|null|Param, // Default: null + * id?: scalar|null|Param, + * }>, + * providers?: array, + * }, + * entity?: array{ + * class: scalar|null|Param, // The full entity class name of your user class. + * property?: scalar|null|Param, // Default: null + * manager_name?: scalar|null|Param, // Default: null + * }, + * memory?: array{ + * users?: array, + * }>, + * }, + * ldap?: array{ + * service: scalar|null|Param, + * base_dn: scalar|null|Param, + * search_dn?: scalar|null|Param, // Default: null + * search_password?: scalar|null|Param, // Default: null + * extra_fields?: list, + * default_roles?: list, + * role_fetcher?: scalar|null|Param, // Default: null + * uid_key?: scalar|null|Param, // Default: "sAMAccountName" + * filter?: scalar|null|Param, // Default: "({uid_key}={user_identifier})" + * password_attribute?: scalar|null|Param, // Default: null + * }, + * }>, + * firewalls: array, + * security?: bool|Param, // Default: true + * user_checker?: scalar|null|Param, // The UserChecker to use when authenticating users in this firewall. // Default: "security.user_checker" + * request_matcher?: scalar|null|Param, + * access_denied_url?: scalar|null|Param, + * access_denied_handler?: scalar|null|Param, + * entry_point?: scalar|null|Param, // An enabled authenticator name or a service id that implements "Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface". + * provider?: scalar|null|Param, + * stateless?: bool|Param, // Default: false + * lazy?: bool|Param, // Default: false + * context?: scalar|null|Param, + * logout?: array{ + * enable_csrf?: bool|null|Param, // Default: null + * csrf_token_id?: scalar|null|Param, // Default: "logout" + * csrf_parameter?: scalar|null|Param, // Default: "_csrf_token" + * csrf_token_manager?: scalar|null|Param, + * path?: scalar|null|Param, // Default: "/logout" + * target?: scalar|null|Param, // Default: "/" + * invalidate_session?: bool|Param, // Default: true + * clear_site_data?: list<"*"|"cache"|"cookies"|"storage"|"executionContexts"|Param>, + * delete_cookies?: array, + * }, + * switch_user?: array{ + * provider?: scalar|null|Param, + * parameter?: scalar|null|Param, // Default: "_switch_user" + * role?: scalar|null|Param, // Default: "ROLE_ALLOWED_TO_SWITCH" + * target_route?: scalar|null|Param, // Default: null + * }, + * required_badges?: list, + * custom_authenticators?: list, + * login_throttling?: array{ + * limiter?: scalar|null|Param, // A service id implementing "Symfony\Component\HttpFoundation\RateLimiter\RequestRateLimiterInterface". + * max_attempts?: int|Param, // Default: 5 + * interval?: scalar|null|Param, // Default: "1 minute" + * lock_factory?: scalar|null|Param, // The service ID of the lock factory used by the login rate limiter (or null to disable locking). // Default: null + * cache_pool?: string|Param, // The cache pool to use for storing the limiter state // Default: "cache.rate_limiter" + * storage_service?: string|Param, // The service ID of a custom storage implementation, this precedes any configured "cache_pool" // Default: null + * }, + * x509?: array{ + * provider?: scalar|null|Param, + * user?: scalar|null|Param, // Default: "SSL_CLIENT_S_DN_Email" + * credentials?: scalar|null|Param, // Default: "SSL_CLIENT_S_DN" + * user_identifier?: scalar|null|Param, // Default: "emailAddress" + * }, + * remote_user?: array{ + * provider?: scalar|null|Param, + * user?: scalar|null|Param, // Default: "REMOTE_USER" + * }, + * login_link?: array{ + * check_route: scalar|null|Param, // Route that will validate the login link - e.g. "app_login_link_verify". + * check_post_only?: scalar|null|Param, // If true, only HTTP POST requests to "check_route" will be handled by the authenticator. // Default: false + * signature_properties: list, + * lifetime?: int|Param, // The lifetime of the login link in seconds. // Default: 600 + * max_uses?: int|Param, // Max number of times a login link can be used - null means unlimited within lifetime. // Default: null + * used_link_cache?: scalar|null|Param, // Cache service id used to expired links of max_uses is set. + * success_handler?: scalar|null|Param, // A service id that implements Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface. + * failure_handler?: scalar|null|Param, // A service id that implements Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface. + * provider?: scalar|null|Param, // The user provider to load users from. + * secret?: scalar|null|Param, // Default: "%kernel.secret%" + * always_use_default_target_path?: bool|Param, // Default: false + * default_target_path?: scalar|null|Param, // Default: "/" + * login_path?: scalar|null|Param, // Default: "/login" + * target_path_parameter?: scalar|null|Param, // Default: "_target_path" + * use_referer?: bool|Param, // Default: false + * failure_path?: scalar|null|Param, // Default: null + * failure_forward?: bool|Param, // Default: false + * failure_path_parameter?: scalar|null|Param, // Default: "_failure_path" + * }, + * form_login?: array{ + * provider?: scalar|null|Param, + * remember_me?: bool|Param, // Default: true + * success_handler?: scalar|null|Param, + * failure_handler?: scalar|null|Param, + * check_path?: scalar|null|Param, // Default: "/login_check" + * use_forward?: bool|Param, // Default: false + * login_path?: scalar|null|Param, // Default: "/login" + * username_parameter?: scalar|null|Param, // Default: "_username" + * password_parameter?: scalar|null|Param, // Default: "_password" + * csrf_parameter?: scalar|null|Param, // Default: "_csrf_token" + * csrf_token_id?: scalar|null|Param, // Default: "authenticate" + * enable_csrf?: bool|Param, // Default: false + * post_only?: bool|Param, // Default: true + * form_only?: bool|Param, // Default: false + * always_use_default_target_path?: bool|Param, // Default: false + * default_target_path?: scalar|null|Param, // Default: "/" + * target_path_parameter?: scalar|null|Param, // Default: "_target_path" + * use_referer?: bool|Param, // Default: false + * failure_path?: scalar|null|Param, // Default: null + * failure_forward?: bool|Param, // Default: false + * failure_path_parameter?: scalar|null|Param, // Default: "_failure_path" + * }, + * form_login_ldap?: array{ + * provider?: scalar|null|Param, + * remember_me?: bool|Param, // Default: true + * success_handler?: scalar|null|Param, + * failure_handler?: scalar|null|Param, + * check_path?: scalar|null|Param, // Default: "/login_check" + * use_forward?: bool|Param, // Default: false + * login_path?: scalar|null|Param, // Default: "/login" + * username_parameter?: scalar|null|Param, // Default: "_username" + * password_parameter?: scalar|null|Param, // Default: "_password" + * csrf_parameter?: scalar|null|Param, // Default: "_csrf_token" + * csrf_token_id?: scalar|null|Param, // Default: "authenticate" + * enable_csrf?: bool|Param, // Default: false + * post_only?: bool|Param, // Default: true + * form_only?: bool|Param, // Default: false + * always_use_default_target_path?: bool|Param, // Default: false + * default_target_path?: scalar|null|Param, // Default: "/" + * target_path_parameter?: scalar|null|Param, // Default: "_target_path" + * use_referer?: bool|Param, // Default: false + * failure_path?: scalar|null|Param, // Default: null + * failure_forward?: bool|Param, // Default: false + * failure_path_parameter?: scalar|null|Param, // Default: "_failure_path" + * service?: scalar|null|Param, // Default: "ldap" + * dn_string?: scalar|null|Param, // Default: "{user_identifier}" + * query_string?: scalar|null|Param, + * search_dn?: scalar|null|Param, // Default: "" + * search_password?: scalar|null|Param, // Default: "" + * }, + * json_login?: array{ + * provider?: scalar|null|Param, + * remember_me?: bool|Param, // Default: true + * success_handler?: scalar|null|Param, + * failure_handler?: scalar|null|Param, + * check_path?: scalar|null|Param, // Default: "/login_check" + * use_forward?: bool|Param, // Default: false + * login_path?: scalar|null|Param, // Default: "/login" + * username_path?: scalar|null|Param, // Default: "username" + * password_path?: scalar|null|Param, // Default: "password" + * }, + * json_login_ldap?: array{ + * provider?: scalar|null|Param, + * remember_me?: bool|Param, // Default: true + * success_handler?: scalar|null|Param, + * failure_handler?: scalar|null|Param, + * check_path?: scalar|null|Param, // Default: "/login_check" + * use_forward?: bool|Param, // Default: false + * login_path?: scalar|null|Param, // Default: "/login" + * username_path?: scalar|null|Param, // Default: "username" + * password_path?: scalar|null|Param, // Default: "password" + * service?: scalar|null|Param, // Default: "ldap" + * dn_string?: scalar|null|Param, // Default: "{user_identifier}" + * query_string?: scalar|null|Param, + * search_dn?: scalar|null|Param, // Default: "" + * search_password?: scalar|null|Param, // Default: "" + * }, + * access_token?: array{ + * provider?: scalar|null|Param, + * remember_me?: bool|Param, // Default: true + * success_handler?: scalar|null|Param, + * failure_handler?: scalar|null|Param, + * realm?: scalar|null|Param, // Default: null + * token_extractors?: list, + * token_handler: string|array{ + * id?: scalar|null|Param, + * oidc_user_info?: string|array{ + * base_uri: scalar|null|Param, // Base URI of the userinfo endpoint on the OIDC server, or the OIDC server URI to use the discovery (require "discovery" to be configured). + * discovery?: array{ // Enable the OIDC discovery. + * cache?: array{ + * id: scalar|null|Param, // Cache service id to use to cache the OIDC discovery configuration. + * }, + * }, + * claim?: scalar|null|Param, // Claim which contains the user identifier (e.g. sub, email, etc.). // Default: "sub" + * client?: scalar|null|Param, // HttpClient service id to use to call the OIDC server. + * }, + * oidc?: array{ + * discovery?: array{ // Enable the OIDC discovery. + * base_uri: list, + * cache?: array{ + * id: scalar|null|Param, // Cache service id to use to cache the OIDC discovery configuration. + * }, + * }, + * claim?: scalar|null|Param, // Claim which contains the user identifier (e.g.: sub, email..). // Default: "sub" + * audience: scalar|null|Param, // Audience set in the token, for validation purpose. + * issuers: list, + * algorithms: list, + * keyset?: scalar|null|Param, // JSON-encoded JWKSet used to sign the token (must contain a list of valid public keys). + * encryption?: bool|array{ + * enabled?: bool|Param, // Default: false + * enforce?: bool|Param, // When enabled, the token shall be encrypted. // Default: false + * algorithms: list, + * keyset: scalar|null|Param, // JSON-encoded JWKSet used to decrypt the token (must contain a list of valid private keys). + * }, + * }, + * cas?: array{ + * validation_url: scalar|null|Param, // CAS server validation URL + * prefix?: scalar|null|Param, // CAS prefix // Default: "cas" + * http_client?: scalar|null|Param, // HTTP Client service // Default: null + * }, + * oauth2?: scalar|null|Param, + * }, + * }, + * http_basic?: array{ + * provider?: scalar|null|Param, + * realm?: scalar|null|Param, // Default: "Secured Area" + * }, + * http_basic_ldap?: array{ + * provider?: scalar|null|Param, + * realm?: scalar|null|Param, // Default: "Secured Area" + * service?: scalar|null|Param, // Default: "ldap" + * dn_string?: scalar|null|Param, // Default: "{user_identifier}" + * query_string?: scalar|null|Param, + * search_dn?: scalar|null|Param, // Default: "" + * search_password?: scalar|null|Param, // Default: "" + * }, + * remember_me?: array{ + * secret?: scalar|null|Param, // Default: "%kernel.secret%" + * service?: scalar|null|Param, + * user_providers?: list, + * catch_exceptions?: bool|Param, // Default: true + * signature_properties?: list, + * token_provider?: string|array{ + * service?: scalar|null|Param, // The service ID of a custom remember-me token provider. + * doctrine?: bool|array{ + * enabled?: bool|Param, // Default: false + * connection?: scalar|null|Param, // Default: null + * }, + * }, + * token_verifier?: scalar|null|Param, // The service ID of a custom rememberme token verifier. + * name?: scalar|null|Param, // Default: "REMEMBERME" + * lifetime?: int|Param, // Default: 31536000 + * path?: scalar|null|Param, // Default: "/" + * domain?: scalar|null|Param, // Default: null + * secure?: true|false|"auto"|Param, // Default: null + * httponly?: bool|Param, // Default: true + * samesite?: null|"lax"|"strict"|"none"|Param, // Default: "lax" + * always_remember_me?: bool|Param, // Default: false + * remember_me_parameter?: scalar|null|Param, // Default: "_remember_me" + * }, + * }>, + * access_control?: list, + * attributes?: array, + * route?: scalar|null|Param, // Default: null + * methods?: list, + * allow_if?: scalar|null|Param, // Default: null + * roles?: list, + * }>, + * role_hierarchy?: array>, + * } + * @psalm-type UxIconsConfig = array{ + * icon_dir?: scalar|null|Param, // The local directory where icons are stored. // Default: "%kernel.project_dir%/assets/icons" + * default_icon_attributes?: mixed, // Default attributes to add to all icons. // Default: {"fill":"currentColor"} + * icon_sets?: array, + * }>, + * aliases?: list, + * iconify?: bool|array{ // Configuration for the remote icon service. + * enabled?: bool|Param, // Default: true + * on_demand?: bool|Param, // Whether to download icons "on demand". // Default: true + * endpoint?: scalar|null|Param, // The endpoint for the Iconify icons API. // Default: "https://api.iconify.design" + * }, + * ignore_not_found?: bool|Param, // Ignore error when an icon is not found. Set to 'true' to fail silently. // Default: false + * } + * @psalm-type WhiteOctoberPagerfantaConfig = array{ // Deprecated: The "white_october_pagerfanta" configuration node is deprecated, migrate your configuration to the "babdev_pagerfanta" configuration node. + * exceptions_strategy?: array{ + * out_of_range_page?: scalar|null|Param, // Default: "to_http_not_found" + * not_valid_current_page?: scalar|null|Param, // Default: "to_http_not_found" + * }, + * default_view?: scalar|null|Param, // Default: "default" + * } + * @psalm-type ConfigType = array{ + * imports?: ImportsConfig, + * parameters?: ParametersConfig, + * services?: ServicesConfig, + * framework?: FrameworkConfig, + * twig?: TwigConfig, + * twig_component?: TwigComponentConfig, + * sylius_twig_hooks?: SyliusTwigHooksConfig, + * sylius_twig_extra?: SyliusTwigExtraConfig, + * sylius_admin_ui?: SyliusAdminUiConfig, + * sylius_ui_translations?: SyliusUiTranslationsConfig, + * sylius_bootstrap_admin_ui?: SyliusBootstrapAdminUiConfig, + * live_component?: LiveComponentConfig, + * stimulus?: StimulusConfig, + * doctrine?: DoctrineConfig, + * babdev_pagerfanta?: BabdevPagerfantaConfig, + * sylius_resource?: SyliusResourceConfig, + * knp_menu?: KnpMenuConfig, + * sylius_grid?: SyliusGridConfig, + * vich_uploader?: VichUploaderConfig, + * security?: SecurityConfig, + * ux_icons?: UxIconsConfig, + * white_october_pagerfanta?: WhiteOctoberPagerfantaConfig, + * "when@dev"?: array{ + * imports?: ImportsConfig, + * parameters?: ParametersConfig, + * services?: ServicesConfig, + * framework?: FrameworkConfig, + * twig?: TwigConfig, + * twig_component?: TwigComponentConfig, + * sylius_twig_hooks?: SyliusTwigHooksConfig, + * sylius_twig_extra?: SyliusTwigExtraConfig, + * sylius_admin_ui?: SyliusAdminUiConfig, + * sylius_ui_translations?: SyliusUiTranslationsConfig, + * sylius_bootstrap_admin_ui?: SyliusBootstrapAdminUiConfig, + * web_profiler?: WebProfilerConfig, + * debug?: DebugConfig, + * live_component?: LiveComponentConfig, + * stimulus?: StimulusConfig, + * doctrine?: DoctrineConfig, + * babdev_pagerfanta?: BabdevPagerfantaConfig, + * sylius_resource?: SyliusResourceConfig, + * zenstruck_foundry?: ZenstruckFoundryConfig, + * knp_menu?: KnpMenuConfig, + * sylius_grid?: SyliusGridConfig, + * vich_uploader?: VichUploaderConfig, + * maker?: MakerConfig, + * security?: SecurityConfig, + * ux_icons?: UxIconsConfig, + * }, + * "when@prod"?: array{ + * imports?: ImportsConfig, + * parameters?: ParametersConfig, + * services?: ServicesConfig, + * framework?: FrameworkConfig, + * twig?: TwigConfig, + * twig_component?: TwigComponentConfig, + * sylius_twig_hooks?: SyliusTwigHooksConfig, + * sylius_twig_extra?: SyliusTwigExtraConfig, + * sylius_admin_ui?: SyliusAdminUiConfig, + * sylius_ui_translations?: SyliusUiTranslationsConfig, + * sylius_bootstrap_admin_ui?: SyliusBootstrapAdminUiConfig, + * live_component?: LiveComponentConfig, + * stimulus?: StimulusConfig, + * doctrine?: DoctrineConfig, + * babdev_pagerfanta?: BabdevPagerfantaConfig, + * sylius_resource?: SyliusResourceConfig, + * knp_menu?: KnpMenuConfig, + * sylius_grid?: SyliusGridConfig, + * vich_uploader?: VichUploaderConfig, + * security?: SecurityConfig, + * ux_icons?: UxIconsConfig, + * }, + * "when@test"?: array{ + * imports?: ImportsConfig, + * parameters?: ParametersConfig, + * services?: ServicesConfig, + * framework?: FrameworkConfig, + * twig?: TwigConfig, + * twig_component?: TwigComponentConfig, + * sylius_twig_hooks?: SyliusTwigHooksConfig, + * sylius_twig_extra?: SyliusTwigExtraConfig, + * sylius_admin_ui?: SyliusAdminUiConfig, + * sylius_ui_translations?: SyliusUiTranslationsConfig, + * sylius_bootstrap_admin_ui?: SyliusBootstrapAdminUiConfig, + * web_profiler?: WebProfilerConfig, + * live_component?: LiveComponentConfig, + * stimulus?: StimulusConfig, + * doctrine?: DoctrineConfig, + * babdev_pagerfanta?: BabdevPagerfantaConfig, + * sylius_resource?: SyliusResourceConfig, + * zenstruck_foundry?: ZenstruckFoundryConfig, + * knp_menu?: KnpMenuConfig, + * sylius_grid?: SyliusGridConfig, + * vich_uploader?: VichUploaderConfig, + * security?: SecurityConfig, + * ux_icons?: UxIconsConfig, + * }, + * ..., + * }> + * } + */ +final class App +{ + /** + * @param ConfigType $config + * + * @psalm-return ConfigType + */ + public static function config(array $config): array + { + return AppReference::config($config); + } +} + +namespace Symfony\Component\Routing\Loader\Configurator; + +/** + * This class provides array-shapes for configuring the routes of an application. + * + * Example: + * + * ```php + * // config/routes.php + * namespace Symfony\Component\Routing\Loader\Configurator; + * + * return Routes::config([ + * 'controllers' => [ + * 'resource' => 'routing.controllers', + * ], + * ]); + * ``` + * + * @psalm-type RouteConfig = array{ + * path: string|array, + * controller?: string, + * methods?: string|list, + * requirements?: array, + * defaults?: array, + * options?: array, + * host?: string|array, + * schemes?: string|list, + * condition?: string, + * locale?: string, + * format?: string, + * utf8?: bool, + * stateless?: bool, + * } + * @psalm-type ImportConfig = array{ + * resource: string, + * type?: string, + * exclude?: string|list, + * prefix?: string|array, + * name_prefix?: string, + * trailing_slash_on_root?: bool, + * controller?: string, + * methods?: string|list, + * requirements?: array, + * defaults?: array, + * options?: array, + * host?: string|array, + * schemes?: string|list, + * condition?: string, + * locale?: string, + * format?: string, + * utf8?: bool, + * stateless?: bool, + * } + * @psalm-type AliasConfig = array{ + * alias: string, + * deprecated?: array{package:string, version:string, message?:string}, + * } + * @psalm-type RoutesConfig = array{ + * "when@dev"?: array, + * "when@prod"?: array, + * "when@test"?: array, + * ... + * } + */ +final class Routes +{ + /** + * @param RoutesConfig $config + * + * @psalm-return RoutesConfig + */ + public static function config(array $config): array + { + return $config; + } +} diff --git a/config/routes/framework.yaml b/config/routes/framework.yaml index 0fc74bbac..bc1feace1 100644 --- a/config/routes/framework.yaml +++ b/config/routes/framework.yaml @@ -1,4 +1,4 @@ when@dev: _errors: - resource: '@FrameworkBundle/Resources/config/routing/errors.xml' + resource: '@FrameworkBundle/Resources/config/routing/errors.php' prefix: /_error diff --git a/config/routes/sylius_resource.yaml b/config/routes/sylius_resource.yaml index 0c8ade2c2..e4c326435 100644 --- a/config/routes/sylius_resource.yaml +++ b/config/routes/sylius_resource.yaml @@ -2,6 +2,6 @@ sylius_crud_routes: resource: 'sylius.routing.loader.crud_routes_attributes' type: service -sylius_routes: - resource: 'sylius.routing.loader.routes_attributes' +sylius_resource_routes: + resource: 'sylius.symfony.routing.loader.resource' type: service diff --git a/config/routes/web_profiler.yaml b/config/routes/web_profiler.yaml index 8d85319fd..b3b7b4b0e 100644 --- a/config/routes/web_profiler.yaml +++ b/config/routes/web_profiler.yaml @@ -1,8 +1,8 @@ when@dev: web_profiler_wdt: - resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml' + resource: '@WebProfilerBundle/Resources/config/routing/wdt.php' prefix: /_wdt web_profiler_profiler: - resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml' + resource: '@WebProfilerBundle/Resources/config/routing/profiler.php' prefix: /_profiler diff --git a/docs/.gitbook/assets/img_1.png b/docs/.gitbook/assets/img_1.png deleted file mode 100644 index 0069f40bf..000000000 Binary files a/docs/.gitbook/assets/img_1.png and /dev/null differ diff --git a/docs/.gitbook/assets/managing_speakers_subheader.png b/docs/.gitbook/assets/managing_speakers_subheader.png deleted file mode 100644 index 5eac4e94c..000000000 Binary files a/docs/.gitbook/assets/managing_speakers_subheader.png and /dev/null differ diff --git a/docs/.gitbook/assets/speakers_icon.png b/docs/.gitbook/assets/speakers_icon.png deleted file mode 100644 index 99c8deeb5..000000000 Binary files a/docs/.gitbook/assets/speakers_icon.png and /dev/null differ diff --git a/docs/.gitbook/includes/less-than-div-class-field-greater-than-for....md b/docs/.gitbook/includes/less-than-div-class-field-greater-than-for....md new file mode 100644 index 000000000..b4571ff18 --- /dev/null +++ b/docs/.gitbook/includes/less-than-div-class-field-greater-than-for....md @@ -0,0 +1,28 @@ +--- +title:
{{ for... +--- + +{% tabs %} +{% tab title="property access via hookable_metadata" %} +
<div class="field">
+  {{ form_row(hookable_metadata.context.form.some_field) }}
+ </div>
+
+{% endtab %} + +{% tab title="variable binding" %} +
<div class="field">
+  {% set context = hookable_metadata.context %}
+  {{ form_row(context.form.some_field) }}
+ </div>
+
+{% endtab %} + +{% tab title="utility function" %} +
<div class="field">
+  {% set context = get_hookable_context() %}
+  {{ form_row(context.form.some_field) }}
+ </div>
+
+{% endtab %} +{% endtabs %} diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 77bf032e7..90cf34719 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -12,6 +12,8 @@ * [Customizing the menu](cookbook/admin_panel/menu.md) * [Configuring the security access](cookbook/admin_panel/security.md) * [Customizing the page titles](cookbook/admin_panel/page_titles.md) + * [Customizing the metatags](cookbook/admin_panel/metatags.md) + * [Using autocompletes](cookbook/admin_panel/using-autocompletes.md) * [How to use in a DDD architecture](cookbook/ddd_architecture.md) * [Architecture overview](cookbook/ddd_architecture/overview.md) * [Resource configuration](cookbook/ddd_architecture/resource_configuration.md) @@ -30,6 +32,7 @@ * [Resource Bundle documentation](resource/index.md) * [Installation](resource/installation.md) + * [Resource Lifecycle](resource/lifecycle.md) * [Create new resource](resource/create_new_resource.md) * [Configure your resource](resource/configure_your_resource.md) * [Configure your operations](resource/configure_your_operations.md) @@ -39,7 +42,7 @@ * [Providers](resource/providers.md) * [Processors](resource/processors.md) * [Responders](resource/responders.md) - * [Legacy Resource Documentation](resource/legacy/index.md) + * [Legacy Resource Documentation](resource/legacy/index.md) * [Configuration](resource/legacy/configuration.md) * [Services](resource/legacy/services.md) * [Routing](resource/legacy/routing.md) diff --git a/docs/bootstrap-admin-ui/getting-started.md b/docs/bootstrap-admin-ui/getting-started.md index 1cc1d1df6..0e8925f47 100644 --- a/docs/bootstrap-admin-ui/getting-started.md +++ b/docs/bootstrap-admin-ui/getting-started.md @@ -21,6 +21,27 @@ CRUD templates are split into configurable blocks. You can add new blocks, disable existing ones, or reorder them using the [TwigHooks package](../twig-hooks/getting-started.md). +### Usage with the Sylius Resource package + +{% code title="src/Entity/Speaker.php" lineNumbers="true" %} +```php +namespace App\Entity; + +use Sylius\Resource\Metadata\AsResource; +use Sylius\Resource\Model\ResourceInterface; + +#[AsResource( + // We still use the Sylius admin ui templates dir. + templatesDir: '@SyliusAdminUi/crud', +)] +class Speaker implements ResourceInterface +{ + // ... +} + +``` +{% endcode %} + ### Create This package sets up the template content needed to create a new resource. diff --git a/docs/cookbook/admin_panel.md b/docs/cookbook/admin_panel.md index e84be7da6..4f69ab2c1 100644 --- a/docs/cookbook/admin_panel.md +++ b/docs/cookbook/admin_panel.md @@ -6,3 +6,6 @@ * [Customizing the menu](admin_panel/menu.md) * [Configuring the security access](admin_panel/security.md) * [Customizing the page titles](admin_panel/page_titles.md) +* [Customizing the metatags](admin_panel/metatags.md) +* [Using autocompletes](admin_panel/using-autocompletes.md) + diff --git a/docs/cookbook/admin_panel/basic_operations.md b/docs/cookbook/admin_panel/basic_operations.md index 43461499b..3f8b14564 100644 --- a/docs/cookbook/admin_panel/basic_operations.md +++ b/docs/cookbook/admin_panel/basic_operations.md @@ -1,12 +1,14 @@ # Basic operations -## List of resources +In this cookbook, we assume that you have already created a `Book` resource. -
+{% hint style="info" %} +Learn more on how to [create a Sylius resource](../../resource/create_new_resource.md). +{% endhint %} -
List of books
+## List of resources -
+
List of books
Create a grid for your resource using Symfony's Maker Bundle. @@ -19,7 +21,7 @@ bin/console cache:clear # To refresh grid's cache Magic! Here is the generated grid. -````php +```php final class BookGrid extends AbstractGrid implements ResourceAwareGridInterface { public function __construct() @@ -70,7 +72,7 @@ final class BookGrid extends AbstractGrid implements ResourceAwareGridInterface { return Book::class; } -```` +``` Configure the `index` operation in your resource. @@ -98,6 +100,10 @@ class Book implements ResourceInterface } ``` +{% hint style="info" %} +Note: When you are in a Sylius project, the `templatesDir` path is: `@SyliusAdmin/shared/crud` +{% endhint %} + Use the Symfony `debug:router` command to check the results. ```shell @@ -115,11 +121,7 @@ Your route should look like this: ## Resource creation page -
- -
Book creation page
- -
+
Book creation page
Create a form type for your resource. @@ -153,6 +155,10 @@ class Book implements ResourceInterface } ``` +{% hint style="info" %} +Note: When you are in a Sylius project, the `templatesDir` path is: `@SyliusAdmin/shared/crud` +{% endhint %} + Use the Symfony `debug:router` command to check the results. ```shell @@ -170,13 +176,9 @@ Your route should look like this: ## Resource edition page -
- -
Book edition page
- -
+
Book edition page
-Ensure you already created the Symfony form type in the [previous section](#resource-creation-page). +Ensure you already created the Symfony form type in the [previous section](basic_operations.md#resource-creation-page). Configure the `update` operation in your resource. @@ -204,6 +206,10 @@ class Book implements ResourceInterface } ``` +{% hint style="info" %} +Note: When you are in a Sylius project, the `templatesDir` path is: `@SyliusAdmin/shared/crud` +{% endhint %} + Use the Symfony `debug:router` command to check the results. ```shell @@ -221,11 +227,7 @@ Your route should look like this: ## Resource details page -
- -
Book details page
- -
+
Book details page
Configure the `show` operation in your resource. @@ -252,6 +254,10 @@ class Book implements ResourceInterface } ``` +{% hint style="info" %} +Note: When you are in a Sylius project, the `templatesDir` path is: `@SyliusAdmin/shared/crud` +{% endhint %} + Use the Symfony `debug:router` command to check the results. ```shell @@ -313,10 +319,8 @@ return static function (ContainerConfigurator $container): void { {% endtab %} {% endtabs %} +{% code title="templates/book/show/content/body.html.twig" overflow="wrap" lineNumbers="true" %} ```twig -{% raw %} -{# templates/book/show/content/body.html.twig #} - {% set book = hookable_metadata.context.book %}
@@ -330,8 +334,8 @@ return static function (ContainerConfigurator $container): void {
-{% endraw %} ``` +{% endcode %} {% hint style="info" %} Note that you can also [replace the default title](page_titles.md). diff --git a/docs/cookbook/admin_panel/grids.md b/docs/cookbook/admin_panel/grids.md index f69ce94ff..bf8e10819 100644 --- a/docs/cookbook/admin_panel/grids.md +++ b/docs/cookbook/admin_panel/grids.md @@ -1,18 +1,10 @@ # Customizing your grids -
- -
Overview of an admin dashboard
- -
- -
+
Overview of an admin dashboard
Based on the grid generated by default, our goal here is to obtain a nicely customized grid with autocomplete filters and more! -
Overview of an admin dashboard
- -
+
Overview of an admin dashboard
Let's imagine we have the following grid. @@ -34,21 +26,20 @@ use Sylius\Bundle\GridBundle\Builder\Field\DateTimeField; use Sylius\Bundle\GridBundle\Builder\Field\StringField; use Sylius\Bundle\GridBundle\Builder\GridBuilderInterface; use Sylius\Bundle\GridBundle\Grid\AbstractGrid; -use Sylius\Bundle\GridBundle\Grid\ResourceAwareGridInterface; +use Sylius\Component\Grid\Attribute\AsGrid; -final class TalkGrid extends AbstractGrid implements ResourceAwareGridInterface +#[AsGrid( + name: 'app_talk', + resourceClass: Talk::class, +)] +final class TalkGrid extends AbstractGrid { public function __construct() { // TODO inject services if required } - public static function getName(): string - { - return 'app_talk'; - } - - public function buildGrid(GridBuilderInterface $gridBuilder): void + public function __invoke(GridBuilderInterface $gridBuilder): void { $gridBuilder // see https://github.com/Sylius/SyliusGridBundle/blob/master/docs/field_types.md @@ -95,22 +86,13 @@ final class TalkGrid extends AbstractGrid implements ResourceAwareGridInterface ) ; } - - public function getResourceClass(): string - { - return Talk::class; - } } ``` -{% endcode %} +{% endcode %} ## Fields -
- -
Overview of an admin dashboard
- -
+
Overview of an admin dashboard
Let's clean up our grid and remove unnecessary fields. @@ -132,16 +114,15 @@ use Sylius\Bundle\GridBundle\Builder\Field\DateTimeField; use Sylius\Bundle\GridBundle\Builder\Field\StringField; use Sylius\Bundle\GridBundle\Builder\GridBuilderInterface; use Sylius\Bundle\GridBundle\Grid\AbstractGrid; -use Sylius\Bundle\GridBundle\Grid\ResourceAwareGridInterface; +use Sylius\Component\Grid\Attribute\AsGrid; -final class TalkGrid extends AbstractGrid implements ResourceAwareGridInterface +#[AsGrid( + name: 'app_talk', + resourceClass: Talk::class, +)] +final class TalkGrid extends AbstractGrid { - public static function getName(): string - { - return 'app_talk'; - } - - public function buildGrid(GridBuilderInterface $gridBuilder): void + public function __invoke(GridBuilderInterface $gridBuilder): void { $gridBuilder ->addField( @@ -172,30 +153,17 @@ final class TalkGrid extends AbstractGrid implements ResourceAwareGridInterface ) ; } - - public function getResourceClass(): string - { - return Talk::class; - } } ``` {% endcode %} -We have removed the "description", "endsAt" and "track" grid fields. +We have removed the `description`, `endsAt` and `track` grid fields. -
- -
Overview of an admin dashboard
- -
+
Overview of an admin dashboard
## Adding the speaker avatar using Twig field -
- -
Overview of an admin dashboard
- -
+
Overview of an admin dashboard
Now, let's add the speaker avatar into our talk grid. @@ -207,14 +175,18 @@ namespace App\Grid; use Sylius\Bundle\GridBundle\Builder\Field\TwigField; use Sylius\Bundle\GridBundle\Builder\GridBuilderInterface; use Sylius\Bundle\GridBundle\Grid\AbstractGrid; -use Sylius\Bundle\GridBundle\Grid\ResourceAwareGridInterface; +use Sylius\Component\Grid\Attribute\AsGrid; // ... -final class TalkGrid extends AbstractGrid implements ResourceAwareGridInterface +#[AsGrid( + name: 'app_talk', + resourceClass: Talk::class, +)] +final class TalkGrid extends AbstractGrid { // ... - public function buildGrid(GridBuilderInterface $gridBuilder): void + public function __invoke(GridBuilderInterface $gridBuilder): void { $gridBuilder ->addField( @@ -230,22 +202,17 @@ final class TalkGrid extends AbstractGrid implements ResourceAwareGridInterface } ``` -templates/talk/grid/speaker_avatar.html.twig -{% import '@SyliusBootstrapAdminUi/shared/helper/avatar.html.twig' as avatar %} - -{% set avatar_path = data.avatar.path is defined ? vich_uploader_asset(data.avatar) : null %} - +{% code title="templates/talk/grid/speaker_avatar.html.twig" lineNumbers="true" %} +```php {{ avatar.default(avatar_path, 'img-thumbnail') }} +``` +{% endcode %} ## Filters ### Adding an autocomplete filter -
- -
Overview of an admin dashboard
- -
+
Overview of an admin dashboard
We'd like to filter our talks by a specific speaker. @@ -296,11 +263,16 @@ declare(strict_types=1); namespace App\Grid\Filter; use App\Form\SpeakerAutocompleteType; +use Sylius\Component\Grid\Attribute\AsFilter; use Sylius\Component\Grid\Data\DataSourceInterface; use Sylius\Component\Grid\Filter\EntityFilter; -use Sylius\Component\Grid\Filtering\ConfigurableFilterInterface; +use Sylius\Component\Grid\Filtering\FilterInterface; -final class SpeakerFilter implements ConfigurableFilterInterface +#[AsFilter( + formType: SpeakerAutocompleteType::class, + template: '@SyliusBootstrapAdminUi/shared/grid/filter/select.html.twig', +)] +final class SpeakerFilter implements FilterInterface { public function __construct( private readonly EntityFilter $entityFilter, @@ -312,21 +284,11 @@ final class SpeakerFilter implements ConfigurableFilterInterface // We simply reuse the logic of the built-in EntityFilter provided by the Sylius Grid package. $this->entityFilter->apply($dataSource, $name, $data, $options); } - - public static function getFormType(): string - { - return SpeakerAutocompleteType::class; - } - - public static function getType(): string - { - return self::class; - } } ``` {% endcode %} -Then, we add our SpeakerFilter to our grid. +Then, we add our `SpeakerFilter` to our grid. {% code title="src/Grid/TalkGrid.php" lineNumbers="true" %} ```php @@ -341,22 +303,27 @@ use Sylius\Bundle\GridBundle\Builder\Filter\Filter; use Sylius\Bundle\GridBundle\Builder\GridBuilderInterface; use Sylius\Bundle\GridBundle\Grid\AbstractGrid; use Sylius\Bundle\GridBundle\Grid\ResourceAwareGridInterface; +use Sylius\Component\Grid\Attribute\AsGrid; // ... -final class TalkGrid extends AbstractGrid implements ResourceAwareGridInterface +#[AsGrid( + name: 'app_talk', + resourceClass: Talk::class, +)] +final class TalkGrid extends AbstractGrid { // ... - public function buildGrid(GridBuilderInterface $gridBuilder): void + public function __invoke(GridBuilderInterface $gridBuilder): void { $gridBuilder ->addFilter( Filter::create(name: 'speaker', type: SpeakerFilter::class) ->setLabel('app.ui.speaker') - ->setOptions(['fields' => ['speaker.id']]), // TODO, we need to check if this is necessary - ) + ->setOptions(['fields' => ['speaker.id']]) + ); + // ... - ; } // ... diff --git a/docs/cookbook/admin_panel/menu.md b/docs/cookbook/admin_panel/menu.md index 12c32d210..65b0aed62 100644 --- a/docs/cookbook/admin_panel/menu.md +++ b/docs/cookbook/admin_panel/menu.md @@ -88,3 +88,19 @@ final readonly class MenuBuilder implements MenuBuilderInterface } } ``` + +{% hint style="success" %} +**🧠 Collapse your custom menu by default** + +It's possible to expand your parent menu category on page load by default. For that, you have to set the `setExtra` attribute like this: + +```php +$library = $menu + ->addChild('library') + ->setLabel('app.ui.library') + ->setLabelAttribute('icon', 'tabler:books') + ->setExtra('always_open', true); +``` + +However, ensure that you set the attribute in the parent menu, not in one of the child menu items. +{% endhint %} diff --git a/docs/cookbook/admin_panel/metatags.md b/docs/cookbook/admin_panel/metatags.md new file mode 100644 index 000000000..c5daba1ca --- /dev/null +++ b/docs/cookbook/admin_panel/metatags.md @@ -0,0 +1,51 @@ +# Customizing the metatags + +## Adding metatags + +To add new `` meta tags, you can use the `sylius_admin#metatags` hook. This is useful for adding a favicon or SEO meta tags, for example. +You can register your own Twig template for meta tags via YAML or PHP. + +{% tabs %} +{% tab title="YAML" %} +{% code title="config/packages/sylius_bootstrap_admin_ui.yaml" lineNumbers="true" %} +```yaml +# ... +sylius_twig_hooks: + hooks: + # ... + 'sylius_admin.base#metatags': + favicon: + template: 'favicon.html.twig' + seo_metatags: + template: 'seo_metatags.html.twig' +``` +{% endcode %} +{% endtab %} + +{% tab title="PHP" %} +{% code title="config/packages/sylius_bootstrap_admin_ui.php" lineNumbers="true" %} +```php +use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + +return static function (ContainerConfigurator $containerConfigurator): void { + // ... + + // Define your own Twig template for the favicon. + $containerConfigurator->extension('sylius_twig_hooks', [ + 'hooks' => [ + 'sylius_admin.base#metatags' => [ + 'favicon' => [ + 'template' => 'favicon.html.twig', + ], + 'seo_metatags' => [ + 'template' => 'seo_metatags.html.twig', + ], + ], + ], + + ]); +}; +``` +{% endcode %} +{% endtab %} +{% endtabs %} diff --git a/docs/cookbook/admin_panel/security.md b/docs/cookbook/admin_panel/security.md index 0697d5aca..d813bf8d3 100644 --- a/docs/cookbook/admin_panel/security.md +++ b/docs/cookbook/admin_panel/security.md @@ -71,9 +71,15 @@ security: # These routes are provided by Sylius Admin Ui package path: sylius_admin_ui_logout target: sylius_admin_ui_login + main: + lazy: true ``` {% endcode %} +{% hint style="warning" %} +It's important to move the main block under the admin configuration. Otherwise the admin login functionality won't work properly. +{% endhint %} + {% hint style="info" %} Learn more on how to [configure the firewall on the Symfony documentation](https://symfony.com/doc/current/security.html#the-firewall) {% endhint %} diff --git a/docs/cookbook/admin_panel/using-autocompletes.md b/docs/cookbook/admin_panel/using-autocompletes.md new file mode 100644 index 000000000..798853dac --- /dev/null +++ b/docs/cookbook/admin_panel/using-autocompletes.md @@ -0,0 +1,263 @@ +# Using autocompletes + +The [SyliusBootstrapAdminUi](../../bootstrap-admin-ui/getting-started.md) package uses[ Symfony UX ](https://ux.symfony.com/)under the hood. Thus, [UX autocomplete](https://symfony.com/bundles/ux-autocomplete/current/index.html) is already setup and configured in your admin panel. This means that any simple `ChoiceType` filter form can be turned into an autocomplete simply by setting `autocomplete` to `true` in form options. + +```php +public function configureOptions(OptionsResolver $resolver): void +{ + $resolver->setDefaults([ + 'choices' => $this->getChoices(), + 'placeholder' => 'sylius.ui.all', + 'autocomplete' => true, + ]); +} + +public function getParent(): string +{ + return ChoiceType::class; +} + +``` + + However, if your autocomplete filter requires fetching data from another entity, you will need to use a `BaseEntityAutocompleteType` in order to fetch your options via AJAX. + +All you need to start leveraging this functionality is a bit of routing config. + +### Configure the entity autocomplete route + +{% tabs %} +{% tab title="PHP" %} +{% code title="config/routes/ux_autocomplete.php" %} +```php +add('ux_entity_autocomplete_admin', '/admin/autocomplete/{alias}') + ->controller('ux.autocomplete.entity_autocomplete_controller') + ; +}; + +``` +{% endcode %} +{% endtab %} + +{% tab title="YAML" %} +{% code title="config/routes/ux_autocomplete.yaml" %} +```yaml +# ... +ux_entity_autocomplete_admin: + path: '/admin/autocomplete/{alias}' + controller: 'ux.autocomplete.entity_autocomplete_controller' +``` +{% endcode %} +{% endtab %} +{% endtabs %} + +This adds a new `ux_entity_autocomplete_admin` AJAX route dedicated to your autocompletes. + +### Add a grid filter with entity autocomplete + +First, you need to create an [entity autocomplete field ](https://symfony.com/bundles/ux-autocomplete/current/index.html#usage-in-a-form-with-ajax). + +{% code title="src/Form/SpeakerAutocompleteType.php" lineNumbers="true" %} +```php +setDefaults([ + 'class' => Speaker::class, + 'choice_label' => 'fullName', + ]); + } + + public function getParent(): string + { + return BaseEntityAutocompleteType::class; + } +} +``` +{% endcode %} + +Then, you need to create your grid filter. + +{% tabs %} +{% tab title="SyliusGridBundle v1.14" %} +{% code title="src/Grid/Filter/SpeakerFilter.php" lineNumbers="true" %} +```php +entityFilter->apply($dataSource, $name, $data, $options); + } + } +``` +{% endcode %} +{% endtab %} + +{% tab title="SyliusGridBundle v1.13 " %} +1\) Create your filter class. + +{% code title="src/Grid/Filter/SpeakerFilter.php" lineNumbers="true" %} +```php +entityFilter->apply($dataSource, $name, $data, $options); + } + + public static function getFormType(): string + { + return SpeakerAutocompleteType::class; + } + + public static function getType(): string + { + return self::class; // this will allow us to use FQCN instead of a string key. + } + + } +``` +{% endcode %} + +2\) then configure the Twig template this filter will use. + +PHP config : + +{% code title="config/packages/sylius_grid.php +" %} +```php +extension('sylius_grid', [ + 'templates' => [ + 'filter' => [ + SpeakerFilter::class => '@SyliusBootstrapAdminUi/shared/grid/filter/select.html.twig', + ], + ], + ]); +}; +``` +{% endcode %} + +YAML config: + +{% code title="config/packages/sylius_grid.yaml" %} +```yaml +sylius_grid: + # ... + templates: + filter: + 'App\Grid\Filter\SpeakerFilter': '@SyliusBootstrapAdminUi/shared/grid/filter/entity.html.twig' +``` +{% endcode %} +{% endtab %} +{% endtabs %} + +Now that the filter is configured, you can use it inside any grid. + +{% code title="src/Grid/TalkGrid.php" lineNumbers="true" %} +```php +addFilter( + Filter::create(name: 'speaker', type: SpeakerFilter::class) + ->setLabel('app.ui.speaker') + ->setOptions(['fields' => ['speakers.id']]), + ) + // ... + ; + } + + // ... +} +``` +{% endcode %} diff --git a/docs/getting-started.md b/docs/getting-started.md index 4309562d4..fe1a7f3fc 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -14,7 +14,7 @@ You can set up the Sylius Stack on existing Symfony projects, but in the case yo composer create-project symfony/skeleton my_project # Or with Symfony CLI: -symfony new --docker --php 8.4 my_project +symfony new --docker --php 8.5 my_project ```` ### Install the package using Composer and Symfony Flex @@ -23,7 +23,7 @@ Go to your project directory and run the following command: ```bash composer require -W \ - doctrine/orm "^2.16" \ + doctrine/orm \ doctrine/doctrine-bundle \ pagerfanta/doctrine-orm-adapter \ symfony/asset-mapper \ @@ -31,6 +31,21 @@ composer require -W \ sylius/ui-translations ``` +> The Sylius Stack now supports Symfony 8 ! As some packages remain in alpha, if you are starting a Symfony 8 project, +> run the following command instead to ensure all dependencies are resolved correctly: +> +> ```bash +> composer require -W \ +> doctrine/orm \ +> doctrine/doctrine-bundle \ +> pagerfanta/doctrine-orm-adapter \ +> symfony/asset-mapper \ +> sylius/bootstrap-admin-ui \ +> sylius/ui-translations \ +> sylius/resource-bundle "^1.14@alpha" \ +> sylius/grid-bundle "^1.15@alpha" +> ``` +
Flex recipes
@@ -39,12 +54,6 @@ composer require -W \ Type "a" or "p" to configure the packages via Symfony Flex. -### Install missing tom-select assets - -```bash -symfony console importmap:require tom-select/dist/css/tom-select.default.css -``` - ### Run your web server ```bash @@ -67,7 +76,7 @@ To prevent duplicate Ajax calls, disable the auto-initialized Stimulus app and S #### Disabling Stimulus app & Symfony UX stylesheets from third party package -First, you need to disable the Stimulus App started by the `sylius/bootstrap-admin-ui` package. +First, you need to disable the Stimulus App started by the `sylius/bootstrap-admin-ui` package and add a custom javascript app hook for the asset mapper. {% tabs %} {% tab title="YAML" %} @@ -83,8 +92,11 @@ sylius_twig_hooks: symfony_ux: enabled: false - # Disabling Stimulus App 'sylius_admin.base#javascripts': + app: + priority: 200 + template: 'base/javascripts/app.html.twig' + # Disabling Stimulus App symfony_ux: enabled: false ``` @@ -109,6 +121,11 @@ return static function (ContainerConfigurator $containerConfigurator): void { ], 'sylius_admin.base#javascripts' => [ + // New hook + 'app' => [ + 'priority' => 200, + 'template' => 'base/javascripts/app.html.twig', + ], // Disabling Stimulus App 'symfony_ux' => [ 'enabled' => false, @@ -122,6 +139,12 @@ return static function (ContainerConfigurator $containerConfigurator): void { {% endtab %} {% endtabs %} +{% code title="base/javascripts/app.html.twig" lineNumbers="true" %} +```twig +{{ importmap('app') }} +``` +{% endcode %} + #### Starting Stimulus App ```js diff --git a/docs/grid/custom_filter.md b/docs/grid/custom_filter.md index 56d0e23b0..affe21213 100644 --- a/docs/grid/custom_filter.md +++ b/docs/grid/custom_filter.md @@ -4,6 +4,85 @@ Sylius Grids come with built-in filters, but there are use-cases where you need To add a new filter, we need to create an appropriate class and form type. +{% tabs %} +{% tab title="SyliusGridBundle v1.14 - using attributes" %} +{% code title="src/Grid/Filter/SuppliersStatisticsFilter.php" lineNumbers="true" %} +```php +getQueryBuilder(); + $queryBuilder + ->andWhere('stats = :stats') + ->setParameter(':stats', $data['stats']) + ; + + // You can leverage the ExpressionBuilder to apply driver-agnostic filters to the data source. + // Combined with restrict(), it provides query builder–style functionalities for grid filters. + $dataSource->restrict($dataSource->getExpressionBuilder()->equals('stats', $data['stats'])); + } +} +``` +{% endcode %} + +And the form type: + +{% code title="src/Form/Type/Filter/SuppliersStatisticsFilterType.php" lineNumbers="true" %} +```php +add( + 'stats', + ChoiceType::class, + ['choices' => range($options['range'][0], $options['range'][1])] + ); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver + ->setDefaults([ + 'range' => [0, 10], + ]) + ->setAllowedTypes('range', ['array']) + ; + } +} +``` +{% endcode %} +{% endtab %} + +{% tab title="SyliusGridBundle v1.13" %} {% code title="src/Grid/Filter/SuppliersStatisticsFilter.php" lineNumbers="true" %} ```php getQueryBuilder(); $queryBuilder ->andWhere('stats = :stats') @@ -28,8 +106,6 @@ class SuppliersStatisticsFilter implements ConfigurableFilterInterface ; // For driver abstraction you can use the expression builder. ExpressionBuilder is a kind of query builder. - // $data['stats'] contains the submitted value! - // here is an example $dataSource->restrict($dataSource->getExpressionBuilder()->equals('stats', $data['stats'])); } @@ -85,15 +161,15 @@ class SuppliersStatisticsFilterType extends AbstractType Create a template for the filter, similar to the existing ones: -{% code title="templates/Grid/Filter/suppliers_statistics.html.twig" lineNumbers="true" %} +{% code title="templates/grid/filter/suppliers_statistics.html.twig" lineNumbers="true" %} ```twig -{% form_theme form '@SyliusUi/Form/theme.html.twig' %} + +
{{ form_row(form) }} ``` {% endcode %} - If you use autoconfiguration, the filter is automatically registered as a grid filter. But if you don't use autoconfiguration, let's register your new filter type as a service. @@ -105,30 +181,12 @@ services: tags: ['sylius.grid_filter'] ``` {% endcode %} +{% endtab %} +{% endtabs %} -Now you can use your new filter type in the grid configuration! +Now you can use your new filter type in any grid configuration! {% tabs %} -{% tab title="YAML" %} -{% code title="config/packages/sylius_grid.yaml" lineNumbers="true" %} -```yaml -sylius_grid: - grids: - app_tournament: - driver: doctrine/orm - resource: app.tournament - filters: - stats: - type: suppliers_statistics - form_options: - range: [0, 100] - templates: - filter: - suppliers_statistics: '@App/Grid/Filter/suppliers_statistics.html.twig' -``` -{% endcode %} -{% endtab %} - {% tab title="PHP" %} {% code title="config/packages/sylius_grid.php" lineNumbers="true" %} ```php @@ -165,16 +223,14 @@ namespace App\Grid; use App\Entity\Tournament; use Sylius\Bundle\GridBundle\Builder\GridBuilderInterface; use Sylius\Bundle\GridBundle\Grid\AbstractGrid; -use Sylius\Bundle\GridBundle\Grid\ResourceAwareGridInterface; -final class TournamentGrid extends AbstractGrid implements ResourceAwareGridInterface +#[AsGrid( + name: 'app_tournament', + resourceClass: Tournament::class, +)] +final class TournamentGrid extends AbstractGrid { - public static function getName(): string - { - return 'app_tournament'; - } - - public function buildGrid(GridBuilderInterface $gridBuilder): void + public function __invoke(GridBuilderInterface $gridBuilder): void { $gridBuilder ->addFilter( @@ -183,13 +239,29 @@ final class TournamentGrid extends AbstractGrid implements ResourceAwareGridInte ) ; } - - public function getResourceClass(): string - { - return Tournament::class; - } } ``` {% endcode %} {% endtab %} + +{% tab title="YAML" %} +{% code title="config/packages/sylius_grid.yaml" lineNumbers="true" %} +```yaml +sylius_grid: + grids: + app_tournament: + driver: doctrine/orm + resource: app.tournament + filters: + stats: + type: suppliers_statistics + form_options: + range: [0, 100] + + templates: # only needed if you didn't use AsFilter attribute + filter: + suppliers_statistics: '@App/Grid/Filter/suppliers_statistics.html.twig' +``` +{% endcode %} +{% endtab %} {% endtabs %} diff --git a/docs/grid/field_types.md b/docs/grid/field_types.md index 9e9b1622e..2e7f0ba34 100644 --- a/docs/grid/field_types.md +++ b/docs/grid/field_types.md @@ -1,4 +1,4 @@ -# Field Types +# Field types This is the list of built-in field types. @@ -9,6 +9,42 @@ The simplest column type, which displays the value at the specified path as plai By default, it uses the name of the field, but you can specify a different path if needed. For example: {% tabs %} +{% tab title="PHP (recommended)" %} +{% code title="src/Grid/UserGrid.php" lineNumbers="true" %} +```php +addField( + StringField::create('email') + ->setLabel('app.ui.email') // # each field type can have a label, we suggest using translation keys instead of messages + ->setPath('contactDetails.email') + ) + ; + } +} +``` +{% endcode %} +{% endtab %} + {% tab title="YAML" %} {% code title="config/packages/sylius_grid.yaml" lineNumbers="true" %} ```yaml @@ -23,7 +59,8 @@ sylius_grid: ``` {% endcode %} {% endtab %} -{% tab title="PHP" %} + +{% tab title="PHP config file" %} {% code title="config/packages/sylius_grid.php" lineNumbers="true" %} ```php getContactDetails()->getEmail()`. +## DateTime + +This column type works exactly the same way as _StringField_, but expects a _DateTime_ instance and outputs a formatted date and time string. + +Available options: + +* `format` - defaults to `Y:m:d H:i:s`, you can set it to any supported format (see [https://www.php.net/manual/en/datetime.format.php](https://www.php.net/manual/en/datetime.format.php)) +* `timezone` - defaults to `%sylius_grid.timezone%` parameter, null if such a parameter does not exist, you can set it to any supported timezone (see [https://www.php.net/manual/en/timezones.php](https://www.php.net/manual/en/timezones.php)) + +{% tabs %} +{% tab title="PHP (recommended)" %} {% code title="src/Grid/UserGrid.php" lineNumbers="true" %} ```php addField( - StringField::create('email') - ->setLabel('app.ui.email') // # each field type can have a label, we suggest using translation keys instead of messages - ->setPath('contactDetails.email') + DateTimeField::create('birthday', 'Y:m:d H:i:s', null) // this format and timezone are the default value, but you can modify them + ->setLabel('app.ui.birthday') ) ; } - - public function getResourceClass(): string - { - return User::class; - } } ``` {% endcode %} {% endtab %} -{% endtabs %} -This configuration will display the value of `$user->getContactDetails()->getEmail()`. - -## DateTime - -This column type works exactly the same way as *StringField*, but expects a *DateTime* instance and outputs a formatted date and time string. - -Available options: -* `format` - defaults to `Y:m:d H:i:s`, you can set it to any supported format (see https://www.php.net/manual/en/datetime.format.php) -* `timezone` - defaults to `%sylius_grid.timezone%` parameter, null if such a parameter does not exist, you can set it to any supported timezone (see https://www.php.net/manual/en/timezones.php) - -{% tabs %} {% tab title="YAML" %} {% code title="config/packages/sylius_grid.yaml" lineNumbers="true" %} ```yaml @@ -116,7 +147,7 @@ sylius_grid: {% endcode %} {% endtab %} -{% tab title="PHP" %} +{% tab title="PHP config file" %} {% code title="config/packages/sylius_grid.php" lineNumbers="true" %} ```php setOptions([ + 'format' => 'Y-m-d H:i:s', + 'timezone' => 'null' + + // Your options here +]); +``` +{% endhint %} + +## Twig + +The Twig column type is the most flexible one, because it delegates the logic of rendering the value to the Twig templating engine. First, you must specify the template you want to render. +{% tabs %} +{% tab title="PHP (recommended)" %} {% code title="src/Grid/UserGrid.php" lineNumbers="true" %} ```php addField( - DateTimeField::create('birthday', 'Y:m:d H:i:s', null) // this format and timezone are the default value, but you can modify them - ->setLabel('app.ui.birthday') + TwigField::create('name', ':Grid/Column:_prettyName.html.twig') + ->setLabel('app.ui.name') ) ; } - - public function getResourceClass(): string - { - return User::class; - } } ``` {% endcode %} {% endtab %} -{% endtabs %} -{% hint style="warning" %} -If you want to call the `setOptions` function, you must pass both `'format'` and `'timezone'` as arguments again. Otherwise, they will be unset. - -{% code %} -```php -$field->setOptions([ - 'format' => 'Y-m-d H:i:s', - 'timezone' => 'null' - - // Your options here -]); -``` -{% endcode %} -{% endhint %} - -## Twig - -The Twig column type is the most flexible one, because it delegates the logic of rendering the value to the Twig templating engine. -First, you must specify the template you want to render. - -{% tabs %} {% tab title="YAML" %} {% code title="config/packages/sylius_grid.yaml" lineNumbers="true" %} ```yaml @@ -216,7 +238,7 @@ sylius_grid: {% endcode %} {% endtab %} -{% tab title="PHP" %} +{% tab title="PHP config file" %} {% code title="config/packages/sylius_grid.php" lineNumbers="true" %} ```php {{ data }} +``` +{% endcode %} + +#### Binding a Field to the Full Object Instance + +To render more complex data in a grid field, you can bind the field to the root object by redefining the field path. This gives you access to all attributes of the underlying object when rendering the field. + +{% tabs %} +{% tab title="PHP (recommended)" %} +{% code title="src/Grid/UserGrid.php" %} ```php addField( TwigField::create('name', ':Grid/Column:_prettyName.html.twig') ->setLabel('app.ui.name') + ->setPath('.') // sets the field path to the root object ) ; } - - public function getResourceClass(): string - { - return User::class; - } } ``` {% endcode %} {% endtab %} -{% endtabs %} -Then, within the template, you can render the field's value via the `data` variable. +{% tab title="YAML" %} +{% code title="config/packages/sylius_grid.yaml" %} +```yaml +sylius_grid: + grids: + app_user: + fields: + name: + type: twig + label: app.ui.name + path: . # sets the field path to the root object +``` +{% endcode %} +{% endtab %} -{% code title="@Grid/Column/_prettyName.html.twig" %} -```twig -{{ data }} +{% tab title="PHP config file" %} +{% code title="config/packages/sylius_grid.php" %} +```php +addGrid(GridBuilder::create('app_user', '%app.model.user.class%') + ->addField( + TwigField::create('name', '@Grid/Column/_prettyName.html.twig') + ->setLabel('app.ui.name') + ->setPath('.') // sets the field path to the root object + ) + ) +}; ``` {% endcode %} +{% endtab %} +{% endtabs %} -If you wish to render more complex grid fields, just redefine the path of -the field to root in your grid – `path: .` and then you can access all -attributes of the object instance: +This allows you to render multiple properties inside the same field Twig template. -{% code %} ```twig {{ data.name }}

{{ data.description|markdown }}

``` -{% endcode %} {% hint style="warning" %} If you want to call the `setOptions` function, you must pass `'template'` as an argument again. Otherwise, it will be unset. -{% code %} ```php $field->setOptions([ 'template' => ':Grid/Column:_prettyName.html.twig', @@ -309,5 +364,4 @@ $field->setOptions([ // Your options here ]); ``` -{% endcode %} {% endhint %} diff --git a/docs/grid/your_first_grid.md b/docs/grid/your_first_grid.md index 801d8180d..8d91a7306 100644 --- a/docs/grid/your_first_grid.md +++ b/docs/grid/your_first_grid.md @@ -1,10 +1,6 @@ -Creating Your First Grid -======================== +# Creating your first grid -In order to use grids, you need to register your entity as a Sylius -resource. Let us assume you have a Supplier model in your application, -which represents a supplier of goods in your shop and has several -fields, including _name_, _description_ and _enabled_. +In order to use grids, you need to register your entity as a Sylius resource. Let us assume you have a Supplier model in your application, which represents a supplier of goods in your shop and has several fields, including _name_, _description_ and _enabled_. In order to make it a Sylius resource, you need to add the `AsResource` attribute and implement `ResourceInterface`. @@ -23,51 +19,74 @@ class Supplier implements ResourceInterface ``` {% endcode %} -That's it! Your class is now a resource. In order to learn what it -means, please refer to the -[SyliusResourceBundle](../resource/index.md) -documentation. +That's it! Your class is now a resource. In order to learn what it means, please refer to the [SyliusResourceBundle](../resource/index.md) documentation. ## Grid Maker You can create your grid using the [Symfony Maker bundle](https://symfony.com/bundles/SymfonyMakerBundle/current/index.html). +{% hint style="info" %} +Since SyliusGridBundle 1.14, this command supports any PHP class, including Sylius Resources too ! +{% endhint %} + ```shell $ bin/console make:grid ``` +{% hint style="info" %} +This command will generate a grid with a field entry for each of your PHP class properties except `id`. +{% endhint %} + ## Grid Definition Now we can configure our first grid: {% tabs %} -{% tab title="YAML" %} -{% code title="config/packages/sylius_grid.yaml" lineNumbers="true" %} -```yaml -sylius_grid: - grids: - app_admin_supplier: - driver: - name: doctrine/orm - options: - class: App\Entity\Supplier - fields: - name: - type: string - label: app.ui.name - enabled: - type: twig - label: app.ui.enabled - options: - template: '@SyliusBootstrapAdminUi/shared/grid/field/boolean.html.twig' # This will be a checkbox field +{% tab title="PHP (recommended)" %} +{% code title="/src/Grid/AdminSupplierGrid.php" lineNumbers="true" %} +```php +addField( + StringField::create('name') + ->setLabel('app.ui.name') + ) + ->addField( + TwigField::create('enabled', '@SyliusBootstrapAdminUi/shared/grid/field/boolean.html.twig') + ->setLabel('app.ui.enabled') + ) + ; + } +} ``` {% endcode %} - {% endtab %} -{% tab title="PHP" %} +{% tab title="PHP config file" %} {% code lineNumbers="true" %} ```php +addField( - StringField::create('name') - ->setLabel('app.ui.name') - ) - ->addField( - TwigField::create('enabled', '@SyliusBootstrapAdminUi/shared/grid/field/boolean.html.twig') - ->setLabel('app.ui.enabled') - ) - ; - } - - public function getResourceClass(): string - { - return Supplier::class; - } -} +{% tab title="YAML" %} +{% code title="config/packages/sylius_grid.yaml" lineNumbers="true" %} +```yaml +sylius_grid: + grids: + app_admin_supplier: + driver: + name: doctrine/orm + options: + class: App\Entity\Supplier + fields: + name: + type: string + label: app.ui.name + enabled: + type: twig + label: app.ui.enabled + options: + template: '@SyliusBootstrapAdminUi/shared/grid/field/boolean.html.twig' # This will be a checkbox field ``` {% endcode %} {% endtab %} {% endtabs %} {% hint style="info" %} -Remember that a grid is *the way objects of a desired entity are -displayed on its index view*. Therefore, only fields that are useful -for identification of objects are available - only `string` and `twig` -types. Although a Supplier also has a _description_ field, it is not needed on the index and won't be displayed here. +Available field types are `string`, `datetime`, `callable` and `twig`. The Twig field type is particularly powerful and can be leveraged to render more complex fields (booleans, combinations of resource properties etc) and gives you full flexibility of styling. {% endhint %} ## Using your grid on an index operation @@ -175,6 +168,10 @@ class Supplier implements ResourceInterface ``` {% endcode %} +{% hint style="info" %} +Note: When you are in a Sylius project, the `templatesDir` path is: `@SyliusAdmin/shared/crud` +{% endhint %} + This will generate the following path: ```shell @@ -183,13 +180,12 @@ This will generate the following path: ------------------------------ --------------------------- app_admin_supplier_index /admin/suppliers ``` -{% endcode %} {% hint style="info" %} See how to add this new page into your [administration menu](../cookbook/admin_panel/menu.md). {% endhint %} -Now, your new grid should look like this when accessing the index on */admin/suppliers/*: +Now, your new grid should look like this when accessing the index on _/admin/suppliers/_: ![image](../.gitbook/assets/suppliers_grid.png) @@ -198,26 +194,46 @@ Now, your new grid should look like this when accessing the index on */admin/sup To allow users to search for specific items in the grid, you can use filters. {% tabs %} -{% tab title="YAML" %} -{% code title="config/packages/sylius_grid.yaml" lineNumbers="true" %} -```yaml -sylius_grid: - grids: - app_admin_supplier: - # ... - filters: - name: - type: string - label: Name - enabled: - type: boolean - label: Enabled +{% tab title="PHP (recommended)" %} +{% code title="src/Grid/AdminSupplierGrid.php" lineNumbers="true" %} +```php +addFilter( + StringFilter::create('name') + ->setLabel('Name') + ) + ->addFilter( + BooleanFilter::create('enabled') + ->setLabel('Enabled') + ) + ; + } +} ``` {% endcode %} - {% endtab %} -{% tab title="PHP" %} +{% tab title="PHP config file" %} {% code lineNumbers="true" %} ```php addFilter( - StringFilter::create('name') - ->setLabel('Name') - ) - ->addFilter( - BooleanFilter::create('enabled') - ->setLabel('Enabled') - ) - ; - } - - public function getResourceClass(): string - { - return Supplier::class; + $gridBuilder->setRepositoryMethod('mySupplierGridQuery'); } } ``` {% endcode %} - {% endtab %} -{% endtabs %} - -How will it look like in the admin panel? -![image](../.gitbook/assets/grid_filters.png) +{% tab title="PHP config file" %} +{% code title="config/packages/sylius_grid.php" lineNumbers="true" %} +```php +addGrid(GridBuilder::create('app_admin_supplier', Supplier::class) + ->setRepositoryMethod('mySupplierGridQuery') + ) +}; +``` +{% endcode %} +{% endtab %} -{% tabs %} {% tab title="YAML" %} {% code title="config/packages/sylius_grid.yaml" lineNumbers="true" %} ```yaml @@ -319,30 +359,126 @@ sylius_grid: method: mySupplierGridQuery ``` {% endcode %} - {% endtab %} +{% endtabs %} -{% tab title="PHP" %} -{% code title="config/packages/sylius_grid.php" lineNumbers="true" %} +{% hint style="info" %} +The repository method must return a `QueryBuilder` object, as the query needs to adjust based on the filters and sorting the user will apply later. Furthermore, all sub-entities you wish to use later on for filtering must be joined explicitly in the query. +{% endhint %} + +#### Custom data provider + +Here is a simple example of a custom data provider. You're obviously free to actually fetch your data in whatever way suits your need, by calling a repository of any kind (Doctrine, API, in-memory,..) directly or call a query bus for instance. + +{% code title="src/Grid/Provider/SupplierGridProvider.php" lineNumbers="true" %} ```php addGrid(GridBuilder::create('app_admin_supplier', Supplier::class) - ->setRepositoryMethod('mySupplierGridQuery') - ) -}; +use App\Repository\SupplierRepository; +use App\Resource\SupplierResource; +use Pagerfanta\Adapter\FixedAdapter; +use Pagerfanta\Pagerfanta; +use Pagerfanta\PagerfantaInterface; +use Sylius\Component\Grid\Data\DataProviderInterface; +use Sylius\Component\Grid\Definition\Grid; +use Sylius\Component\Grid\Parameters; + +final readonly class SupplierGridProvider implements DataProviderInterface +{ + public function __construct( + private SupplierRepository $supplierRepository, + ) { + } + + public function getData(Grid $grid, Parameters $parameters): PagerFantaInterface + { + $page = (int) $parameters->get('page', 1); + $itemsPerPage = (int) $parameters->get('limit', 10); + $criteria = $parameters->get('criteria'); + + $paginator = $this->getSuppliersPaginator($page, $itemsPerPage, $criteria); + + $data = []; + + foreach ($paginator as $row) { + $data[] = new SupplierResource( + enabled: $row['enabled'], + name: $row['name'], + // ... + ); + } + + return new Pagerfanta(new FixedAdapter($paginator->count(), $data)); + } + + public function getSuppliersPaginator(int $page, int $itemsPerPage, ?array $criteria): PagerfantaInterface + { + $supplierRepository = $this->supplierRepository; + + if (!empty($criteria['country'] ?? null)) { + $supplierRepository = $supplierRepository->withCountryCode($criteria['country']); + } + + return $supplierRepository->withPagination($page, $itemsPerPage)->paginator(); + } +} +``` +{% endcode %} + +Then, this example Doctrine repository uses a JOIN statement on our related Address Entity. + +{% code title="src/Repository/SupplierRepository.php" lineNumbers="true" %} +```php +queryBuilder + ->innerJoin(sprintf('%s.address', self::ALIAS), self::ADDRESS_ALIAS); + } + + public function withCountryCode(string $countryCode): static + { + return $this->filter(static function (QueryBuilder $queryBuilder) use ($countryCode): void { + $queryBuilder + ->andWhere($queryBuilder->expr()->eq(sprintf('%s.countryCode', self::ADDRESS_ALIAS), ':countryCode')) + ->setParameter('countryCode', $countryCode) + ; + }); + } + + // .... + +} ``` {% endcode %} -OR +#### Adding your entity filter to your grid +Then you can simply insert your filter inside the grid. + +{% tabs %} +{% tab title="PHP (recommended)" %} {% code title="src/Grid/AdminSupplierGrid.php" lineNumbers="true" %} ```php setRepositoryMethod('mySupplierGridQuery') + ->addFilter( + StringFilter::create('country', ['address.country'], 'contains') + ->setLabel('origin') + ) ; } - - public function getResourceClass(): string - { - return Supplier::class; - } + + // ... } ``` {% endcode %} - {% endtab %} -{% endtabs %} -### *Note* - -The repository method must return a queryBuilder object, -as the query needs to adjust based on the filters and sorting the user will apply later. +{% tab title="PHP config file" %} +{% code title="config/packages/sylius_grid.php" lineNumbers="true" %} +```php +addGrid(GridBuilder::create('app_admin_supplier', Supplier::class) + ->addFilter( + StringFilter::create('country', ['address.country'], 'contains') + ->setLabel('origin') + ) + ) +}; +``` +{% endcode %} +{% endtab %} -{% tabs %} {% tab title="YAML" %} {% code title="config/packages/sylius_grid.php" lineNumbers="true" %} ```yaml @@ -412,32 +557,15 @@ sylius_grid: type: contains ``` {% endcode %} - {% endtab %} +{% endtabs %} -{% tab title="PHP" %} -{% code title="config/packages/sylius_grid.php" lineNumbers="true" %} -```php -addGrid(GridBuilder::create('app_admin_supplier', Supplier::class) - ->addFilter( - StringFilter::create('country', ['address.country'], 'contains') - ->setLabel('origin') - ) - ) -}; -``` -{% endcode %} +## Default Sorting -OR +You can define by which field you want the grid to be sorted and how using `orderBy()` . +{% tabs %} +{% tab title="PHP (recommended)" %} {% code title="src/Grid/AdminSupplierGrid.php" lineNumbers="true" %} ```php addFilter( - StringFilter::create('country', ['address.country'], 'contains') - ->setLabel('origin') - ) - ; - } - - public function getResourceClass(): string + public function __invoke(GridBuilderInterface $gridBuilder): void { - return Supplier::class; + $gridBuilder->orderBy(name: 'name', direction: 'asc'); } } ``` {% endcode %} -{% endtab %} -{% endtabs %} - -Default Sorting ---------------- - -You can define by which field you want the grid to be sorted and how. - -{% tabs %} -{% tab title="YAML" %} -{% code title="config/packages/sylius_grid.yaml" lineNumbers="true" %} -```yaml -sylius_grid: - grids: - app_admin_supplier: - # ... - sorting: - name: asc - # ... -``` -{% endcode %} {% endtab %} -{% tab title="PHP" %} +{% tab title="PHP config file" %} {% code title="config/packages/sylius_grid.php" lineNumbers="true" %} ```php orderBy('name', 'asc') + ->addField( + StringField::create('name') + ->setLabel('sylius.ui.name') + ->setSortable(true) + ) ; } - public function getResourceClass(): string - { - return Supplier::class; - } } ``` {% endcode %} - -{% endtab %} -{% endtabs %} - -Then in the fields section, indicate that the field can be used for sorting: - -{% tabs %} -{% tab title="YAML" %} -{% code title="config/packages/sylius_grid.php" lineNumbers="true" %} -```yaml -sylius_grid: - grids: - app_admin_supplier: - # ... - fields: - name: - type: string - label: sylius.ui.name - sortable: ~ - # ... -``` -{% endcode %} - {% endtab %} -{% tab title="PHP" %} +{% tab title="PHP config file" %} {% code title="config/packages/sylius_grid.php" lineNumbers="true" %} ```php addField( - StringField::create('name') - ->setLabel('sylius.ui.name') - ->setSortable(true) + TwigField::create('name', '@App/Grid/Fields/my_country_flags.html.twig') + ->setPath('address.country') + ->setLabel('app.ui.country') + ->setSortable(true, 'address.country') ) ; - } - - public function getResourceClass(): string - { - return Supplier::class; - } + } } ``` {% endcode %} - {% endtab %} -{% endtabs %} -If your field is not of a "simple" type, e.g. a twig template with a -specific path, you can enable sorting with the following definition: +{% tab title="PHP config file" %} +{% code title="config/packages/sylius_grid.php" lineNumbers="true" %} +```php +addGrid(GridBuilder::create('app_admin_supplier', Supplier::class) + ->addField( + TwigField::create('name', '@App/Grid/Fields/myCountryFlags.html.twig') + ->setPath('address.country') + ->setLabel('app.ui.country') + ->setSortable(true, 'address.country') + ) + ) +}; +``` +{% endcode %} +{% endtab %} -{% tabs %} {% tab title="YAML" %} -{% code title="config/packages/sylius_grid.php" lineNumbers="true" %} +{% code title="config/packages/sylius_grid.yaml" lineNumbers="true" %} ```yaml sylius_grid: grids: @@ -669,34 +794,19 @@ sylius_grid: # ... ``` {% endcode %} - {% endtab %} +{% endtabs %} -{% tab title="PHP" %} -{% code title="config/packages/sylius_grid.php" lineNumbers="true" %} -```php -addGrid(GridBuilder::create('app_admin_supplier', Supplier::class) - ->addField( - TwigField::create('name', '@App/Grid/Fields/myCountryFlags.html.twig') - ->setPath('address.country') - ->setLabel('app.ui.country') - ->setSortable(true, 'address.country') - ) - ) -}; -``` -{% endcode %} +You can limit how many items are visible on each page by providing an array of integers into the `limits` parameter. The first element of the array will be treated as the default. -OR +{% hint style="info" %} +Pagination limits are set by default to 10, 25 and 50 items per page. In order to turn it off, configure `limits: ~` . +{% endhint %} +{% tabs %} +{% tab title="PHP (recommended)" %} {% code title="src/Grid/AdminSupplierGrid.php" lineNumbers="true" %} ```php addField( - TwigField::create('name', '@App/Grid/Fields/myCountryFlags.html.twig') - ->setPath('address.country') - ->setLabel('app.ui.country') - ->setSortable(true, 'address.country') - ) - ; - } - - public function getResourceClass(): string + public function __invoke(GridBuilderInterface $gridBuilder): void { - return Supplier::class; + $gridBuilder->setLimits([12, 24, 48]); } } ``` {% endcode %} - {% endtab %} -{% endtabs %} - -Pagination ----------- - -You can limit how many items are visible on each page by providing an -array of integers into the `limits` parameter. The first element of the -array will be treated as the default, so by configuring: -{% tabs %} {% tab title="YAML" %} -{% code title="config/packages/sylius_grid.php" lineNumbers="true" %} +{% code title="config/packages/sylius_grid.yaml" %} ```yaml sylius_grid: grids: app_admin_supplier: - # ... - limits: [30, 12, 48] - # ... + limits: + - 12 + - 24 + - 48 ``` {% endcode %} - {% endtab %} -{% tab title="PHP" %} -{% code title="config/packages/sylius_grid.php" lineNumbers="true" %} +{% tab title="PHP config file" %} +{% code title="config/packages/sylius_grid.php" %} ```php addGrid(GridBuilder::create('app_admin_supplier', Supplier::class) - ->setLimits([30, 12, 48]) - ) + $grid->addGrid( + GridBuilder::create('app_admin_supplier', Supplier::class) + ->setLimits([12, 24, 48]) + ); }; ``` {% endcode %} - -OR - -{% code title="src/Grid/AdminSupplierGrid.php" lineNumbers="true" %} -```php -setLimits([30, 12, 48]) - ; - } - - public function getResourceClass(): string - { - return Supplier::class; - } -} -``` -{% endcode %} - {% endtab %} {% endtabs %} -you will see thirty suppliers per page, also you will have the -possibility to change the number of elements to either 12 or 48. - -### *Note* - -Pagination limits are set by default to 10, 25 and 50 items per page. -In order to turn it off, configure limits: \~. - -Actions Configuration ---------------------- +## Actions Configuration Next step is adding some actions to the grid: create, update and delete. @@ -878,69 +917,15 @@ These new operations are now available: Then we need to add these operations into our Grid using Actions. -### *Note* +{% hint style="info" %} +There are two types of actions that can be added to a grid: -There are two types of actions that can be added to a grid: `main` -which "influence" the whole grid (like adding new objects) and `item` -which influence one row of the grid (one object) like editing or -deleting. +* `main` actions affect the entire grid, such as adding new items or deleting items in bulk +* and `item` actions which apply to a single row of the grid (one object), such as editing or deleting. +{% endhint %} {% tabs %} -{% tab title="YAML" %} -{% code title="config/packages/sylius_grid.php" lineNumbers="true" %} -```yaml -sylius_grid: - grids: - app_admin_supplier: - # ... - actions: - main: - create: - type: create - item: - update: - type: update - delete: - type: delete -``` -{% endcode %} - -{% endtab %} - -{% tab title="PHP" %} -{% code title="config/packages/sylius_grid.php" lineNumbers="true" %} -```php -addGrid(GridBuilder::create('app_admin_supplier', Supplier::class) - ->addActionGroup( - MainActionGroup::create( - CreateAction::create() - ) - ) - ->addActionGroup( - ItemActionGroup::create( - UpdateAction::create(), - DeleteAction::create() - ) - ) - ) -}; -``` -{% endcode %} - -OR - +{% tab title="PHP (recommended)" %} {% code title="src/Grid/AdminSupplierGrid.php" lineNumbers="true" %} ```php addActionGroup( @@ -982,15 +966,62 @@ final class AdminSupplierGrid extends AbstractGrid implements ResourceAwareGridI ) ; } - - public function getResourceClass(): string - { - return Supplier::class; - } } ``` {% endcode %} +{% endtab %} + +{% tab title="PHP config file" %} +{% code title="config/packages/sylius_grid.php" lineNumbers="true" %} +```php +addGrid(GridBuilder::create('app_admin_supplier', Supplier::class) + ->addActionGroup( + MainActionGroup::create( + CreateAction::create() + ) + ) + ->addActionGroup( + ItemActionGroup::create( + UpdateAction::create(), + DeleteAction::create() + ) + ) + ) +}; +``` +{% endcode %} +{% endtab %} +{% tab title="YAML" %} +{% code title="config/packages/sylius_grid.php" lineNumbers="true" %} +```yaml +sylius_grid: + grids: + app_admin_supplier: + # ... + actions: + main: + create: + type: create + item: + update: + type: update + delete: + type: delete +``` +{% endcode %} {% endtab %} {% endtabs %} diff --git a/docs/resource/configure_your_operations.md b/docs/resource/configure_your_operations.md index 0ba40cc3f..24ece1b0c 100644 --- a/docs/resource/configure_your_operations.md +++ b/docs/resource/configure_your_operations.md @@ -447,7 +447,7 @@ use Sylius\Resource\Metadata\Update; #[AsResource( routePrefix: 'admin', operations: [ - new Index(), + new Index(routePrefix: ''), // you can also customize the route prefix at the operation level too for extra flexibility new Create(), new Update(), new Delete(), @@ -463,13 +463,55 @@ class Book implements ResourceInterface | Name | Method | Path | |----------------------|-----------------|--------------------------| -| app_book_index | GET | /admin/books/ | +| app_book_index | GET | /books/ | | app_book_create | GET, POST | /admin/books/new | | app_book_update | GET, PUT, PATCH | /admin/books/{id}/edit | | app_book_delete | DELETE | /admin/books/{id} | | app_book_bulk_delete | DELETE | /admin/books/bulk_delete | | app_book_show | GET | /admin/books/{id} | +### Configure the routes' name + +It customizes the route name for individual operations. + +{% code title="src/Entity/Book.php" lineNumbers="true" %} +```php +namespace App\Entity; + +use Sylius\Resource\Model\ResourceInterface; +use Sylius\Resource\Metadata\AsResource; +use Sylius\Resource\Metadata\BulkDelete; +use Sylius\Resource\Metadata\Create; +use Sylius\Resource\Metadata\Delete; +use Sylius\Resource\Metadata\Index; +use Sylius\Resource\Metadata\Show; +use Sylius\Resource\Metadata\Update; + +#[AsResource( + operations: [ + new Index(routeName: 'library_book_list'), + new Create(routeName: 'library_book_add'), + new Update(), + new Delete(), + new BulkDelete(), + new Show(), + ], +) +class Book implements ResourceInterface +{ +} +``` +{% endcode %} + +| Name | Method | Path | +|----------------------|-----------------|--------------------------| +| library_book_list | GET | /books/ | +| library_book_add | GET, POST | /books/new | +| app_book_update | GET, PUT, PATCH | /books/{id}/edit | +| app_book_delete | DELETE | /books/{id} | +| app_book_bulk_delete | DELETE | /books/bulk_delete | +| app_book_show | GET | /books/{id} | + ### Configure the section It changes the route name for each operation. diff --git a/docs/resource/configure_your_resource.md b/docs/resource/configure_your_resource.md index 7c63e8ce3..c9a2c10df 100644 --- a/docs/resource/configure_your_resource.md +++ b/docs/resource/configure_your_resource.md @@ -59,15 +59,30 @@ $ bin/console sylius:debug:resource 'App\Entity\Book' ``` ``` -+--------------------+------------------------------------------------------------+ -| name | book | -| application | app | -| driver | doctrine/orm | -| classes.model | App\Entity\Book | -| classes.controller | Sylius\Bundle\ResourceBundle\Controller\ResourceController | -| classes.factory | Sylius\Component\Resource\Factory\Factory | -| classes.form | Sylius\Bundle\ResourceBundle\Form\Type\DefaultResourceType | -+--------------------+------------------------------------------------------------+ +Resource Metadata +----------------- + + ------------------------ ------------------- + Option Value + ------------------------ ------------------- + alias "app.book" + section null + formType null + templatesDir null + routePrefix null + name "book" + pluralName null + applicationName "app" + identifier null + normalizationContext null + denormalizationContext null + validationContext null + class "App\Entity\Book" + driver null + vars null + ------------------------ ------------------- + + [...] ``` By default, the alias for your Sylius resource will be `app.book`, which combines the application name and the resource name diff --git a/docs/resource/create_new_resource.md b/docs/resource/create_new_resource.md index 795d2631c..a8561b3d7 100644 --- a/docs/resource/create_new_resource.md +++ b/docs/resource/create_new_resource.md @@ -126,10 +126,8 @@ class BookRepository extends ServiceEntityRepository ``` {% endcode %} -The generated code is not compatible with Sylius Resource yet, so we need to make a few changes. - -* First, your repository should implement the `Sylius\Component\Resource\Repository\RepositoryInterface` interface -* Then, add the `Sylius\Bundle\ResourceBundle\Doctrine\ORM\ResourceRepositoryTrait` trait +The generated code is not fully compatible with Sylius Resource yet, so we need to make a few changes. +Please add the `createPaginator` method using the `Sylius\Bundle\ResourceBundle\Doctrine\ORM\CreatePaginatorTrait` trait Your repository should look like this: @@ -142,20 +140,14 @@ namespace App\Repository; use App\Entity\Book; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Persistence\ManagerRegistry; -use Sylius\Bundle\ResourceBundle\Doctrine\ORM\ResourceRepositoryTrait; -use Sylius\Resource\Doctrine\Persistence\RepositoryInterface; +use Sylius\Bundle\ResourceBundle\Doctrine\ORM\CreatePaginatorTrait; /** - * @extends ServiceEntityRepository - * - * @method Book|null find($id, $lockMode = null, $lockVersion = null) - * @method Book|null findOneBy(array $criteria, array $orderBy = null) - * @method Book[] findAll() - * @method Book[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + * [...] */ -class BookRepository extends ServiceEntityRepository implements RepositoryInterface +class BookRepository extends ServiceEntityRepository { - use ResourceRepositoryTrait; + use CreatePaginatorTrait; public function __construct(ManagerRegistry $registry) { diff --git a/docs/resource/index.md b/docs/resource/index.md index d0accdee1..a262b62c9 100644 --- a/docs/resource/index.md +++ b/docs/resource/index.md @@ -1,24 +1,32 @@ # SyliusResourceBundle -There are plenty of things you need to handle for every single Resource in your web application. +The **Sylius Resource Bundle** provides a powerful and extensible foundation for exposing your **business resources** (entities, aggregates, etc.) in a declarative way. +Rather than generating controllers or relying on rigid admin generators, it offers a flexible architecture that lets you focus on your domain model while the bundle handles the boilerplate. -Several "Admin Generators" are available for Symfony, but we needed something really simple, that will allow us to have reusable controllers -but preserve the performance and standard Symfony workflow. We did not want to generate any code or write "Admin" class definitions in PHP. -The big goal was to have exactly the same workflow as with writing controllers manually but without actually creating them! +A *Resource* is any business object you want to expose — for example, a `Product`, `Order`, or `UserProfile`. -Another idea was not to limit ourselves to a single persistence backend. -``Resource`` component provides us with generic purpose persistence services and you can use this bundle with multiple persistence backends. -So far we support: +Each resource can define a set of **operations** — actions that can be performed on it. +Typical operations include `index`, `show`, `create`, `update`, and `delete`, but you can also define custom, domain-specific operations too. +The bundle orchestrates each operation through a well-defined lifecycle involving **providers**, **processors**, and **responders**: -* Doctrine ORM -* Doctrine MongoDB ODM -* Doctrine PHPCR ODM +- **Providers** are responsible for loading or creating the resource object and **validating it** (ensuring the object is consistent before any business logic is applied). + - Example: load from Doctrine, create a new instance, hydrate from request data, validate the object, or fetch from an external API. +- **Processors** handle the business logic or persistence layer (e.g. saving, executing domain services, dispatching events). +- **Responders** produce the final response (e.g. rendering a template). + +This architecture allows you to use the bundle in two main ways: + +- **Rapid Application Mode (RAD)** – perfect for quick CRUD setup with Doctrine ORM. You define your entity, mark it as a resource, and everything just works. +- **Domain-Driven Design / Advanced Mode** – where you control how data is provided and processed by writing your own providers and processors. + +In short, the Sylius Resource Bundle is both **declarative** and **extensible** — define your resources and their operations, and let the framework handle the rest, while still giving you full control over the domain logic when you need it. ## Resource system for Symfony applications. * [Installation](installation.md) # New documentation +* [Resource lifecycle](lifecycle.md) * [Create a new resource](create_new_resource.md) * [Configure your resource](configure_your_resource.md) * [Configure your operations](configure_your_operations.md) diff --git a/docs/resource/lifecycle.md b/docs/resource/lifecycle.md new file mode 100644 index 000000000..9ac98e242 --- /dev/null +++ b/docs/resource/lifecycle.md @@ -0,0 +1,63 @@ +# Resource Lifecycle + +Each operation on a resource follows a well-defined lifecycle. +This flow ensures clear separation of concerns between reading data, applying business logic, and producing the final response. + +```mermaid +flowchart TD + A[Request] --> B[Routing & Metadata] + B --> C[Provider] + C --> D[Processor] + D --> E[Responder] + E --> F[Response] + + subgraph ProviderPhase [Provider Phase] + direction LR + C1[Load existing resource] + C2[Create new instance] + C3[Hydrate resource from request] + C4[Validate resource] + end + + subgraph ProcessorPhase [Processor Phase] + direction LR + D1[Apply business logic] + D2[Persist resource] + D3[Dispatch domain events] + end + + subgraph ResponderPhase [Responder Phase] + direction LR + E1[Render template or redirect] + E2[Return HTTP response] + end + + C --> ProviderPhase + D --> ProcessorPhase + E --> ResponderPhase + + style ProviderPhase fill:#f4f8ff,stroke:#93c5fd,stroke-width:1px + style ProcessorPhase fill:#fef9c3,stroke:#facc15,stroke-width:1px + style ResponderPhase fill:#fef2f2,stroke:#f87171,stroke-width:1px +``` + +## Step-by-step explanation + +### Routing & Metadata +The request is matched to a `Resource` and an `Operation` using metadata collected from attributes (eg `#[AsResource]`). +This step defines which provider, processor, and responder should handle the request. + +### Provider Phase +The provider is responsible for loading or creating the resource object, hydrating it from the request, and validating it before any business logic is applied. + +**Example:** load from Doctrine, create a new instance, populate from request data, validate, or fetch from an external API. + +### Processor Phase +The processor applies the business logic of the operation. + +**Example:** persist the entity, execute domain services, or dispatch events. + +You can use the default Doctrine processor, or implement your own for DDD use cases. + +### Responder Phase +The responder produces the final output — it can render a Twig template or redirect to another route. diff --git a/docs/resource/processors.md b/docs/resource/processors.md index 0c84a82d4..7b77997cb 100644 --- a/docs/resource/processors.md +++ b/docs/resource/processors.md @@ -5,8 +5,8 @@ Processors process data: send an email, persist to storage, add to queue etc. * [Default processors](#default-processors) * [Custom processors](#custom-processors) - * [Example #1: Sending an email after persisting data](#example-1-sending-an-email-after-persisting-data) - * [Example #2: Use a custom delete processor](#example-2-use-a-custom-delete-processor) + * [Example #1: Sending an email after persisting data](#example-1-sending-an-email-after-persisting-data) + * [Example #2: Use a custom delete processor](#example-2-use-a-custom-delete-processor) * [Disable processing data](#disable-processing-data) @@ -33,10 +33,17 @@ As an example, send an email after customer registration {% code title="src/Sylius/State/Processor/CreateCustomerProcessor.php" lineNumbers="true" %} ```php + -{% for book in books|sort_by('name') %} +{% for book in books|sylius_sort_by('name') %}
  • {{ book.name }}
  • {% endif %} @@ -71,7 +71,7 @@ You just need to encapsulate the key with `[]`. ```twig {% raw %}
      -{% for book in books|sort_by('[name]') %} +{% for book in books|sylius_sort_by('[name]') %}
    • {{ book.name }}
    • {% endif %}
    diff --git a/docs/twig-hooks/passing-data-to-your-hookables.md b/docs/twig-hooks/passing-data-to-your-hookables.md index 72f967f3f..89479ca4d 100644 --- a/docs/twig-hooks/passing-data-to-your-hookables.md +++ b/docs/twig-hooks/passing-data-to-your-hookables.md @@ -1,69 +1,42 @@ # Passing data to your hookables -One of the most powerful aspects of hooks & hookables is an ability to pass their data down to the children. We can have two sources of the context data: +One of the most powerful aspects of hooks & hookables is the ability to pass data down to children elements. We can have two sources of context data: * Hook-level defined data * Hookable-level defined data -The context data from these two sources are merged and passed with the metadata to the hookable template or component, so we can use them. +Context data from these two sources is merged and passed to the **hookable** template or component together with the metadata , so we can access them. -
    +
    -
    +### Example -
    +|

    Let's assume we want to render a form in our index.html.twig template via a form variable containing a FormView instance.

    Here, we define an index.form hook, and we can pass it the form's context data thanks to the with keyword.

    | +| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -### Example +This means that we can technically pass down multiple pieces of data to hookables that will hook into `index.form`. {% code title="index.html.twig" lineNumbers="true" fullWidth="false" %} ```twig -{# - # we assume there is a `form` variable holding a `FormView` instance passed - # from the controller - #} -
    {{ form_start(form) }} - {{ form_errors(form) }} - {{ form_widget(form._token) }} + {{ form_errors(form }} + {{ form_widget(form._token) }} + + {% hook 'index.form' with { form } %} - {% raw %} -{% hook 'index.form' with { form } %} -{% endraw %} - {{ form_end(form, {render_rest: false}) }} + {{ form_end(form, {render_rest: false} }}
    ``` {% endcode %} -So, as we see at `line 8` we define the `index.form` hook. But also, we pass the `form` with using the `with` keyword. Thanks to it, we are able to pass multiple data to hookables that will hook into the `index.form` hook. - {% hint style="info" %} `with { form }` is a short-hand for `with { form: form }`, so the key for our `FormView` in the context data bag will be `form.` {% endhint %} -Now let's create a Twig template rendering some field from our form, and let's make it a hookable. +Now let's create a Twig template that renders a field from our form and let's make it a hookable. We have 3 possible options to do this : -{% code title="index/some_field.html.twig" lineNumbers="true" %} -```twig -
    - {{ form_row(hookable_metadata.context.form.some_field) }} - - {# we can also write it like #} - - {% raw %} -{% set context = hookable_metadata.context %} - - {{ form_row(context.form.some_field) }} - - {# or #} - - {% set context = get_hookable_context() %} -{% endraw %} - - {{ form_row(context.form.some_field) }} -
    -``` -{% endcode %} +{% include "../.gitbook/includes/less-than-div-class-field-greater-than-for....md" %} {% hint style="info" %} You can access the context data in multiple ways, so you can pick the one you like the most. Available options are: @@ -72,6 +45,10 @@ You can access the context data in multiple ways, so you can pick the one you li * getting the context data bag via the Twig function like `get_hookable_context().` {% endhint %} +### Override behavior + +When the same context data key is defined at both the **hook** and **hookable** levels, the **hookable-level** value takes precedence. + {% hint style="info" %} -Sometimes you might want to override some data that are defined at the hook-level. It is possible by defining the same context data key on the hookable level. If the same context data key is defined at both hook-level and hookable-level the hookable-level one is used. +You can use this to override hook-level data by redefining the key at the hookable level. {% endhint %} diff --git a/ecs.php b/ecs.php index f05b3cb3d..e95ce5c4a 100644 --- a/ecs.php +++ b/ecs.php @@ -11,6 +11,12 @@ __DIR__ . '/src', ]); + $ecsConfig->skip([ + __DIR__ . '/config/reference.php', + __DIR__ . '/src/AdminUi/tests/Functional/.application/config/reference.php', + __DIR__ . '/src/BootstrapAdminUi/tests/Functional/.application/config/reference.php', + ]); + $ecsConfig->import('vendor/sylius-labs/coding-standard/ecs.php'); $ecsConfig->ruleWithConfiguration(HeaderCommentFixer::class, [ diff --git a/importmap.php b/importmap.php index 3b49af095..ea1cce89e 100644 --- a/importmap.php +++ b/importmap.php @@ -45,4 +45,12 @@ '@symfony/ux-live-component' => [ 'path' => './vendor/symfony/ux-live-component/assets/dist/live_controller.js', ], + 'tom-select/dist/css/tom-select.bootstrap4.css' => [ + 'version' => '2.4.3', + 'type' => 'css', + ], + 'tom-select/dist/css/tom-select.bootstrap5.css' => [ + 'version' => '2.4.3', + 'type' => 'css', + ], ]; diff --git a/link b/link new file mode 100755 index 000000000..b2181a231 --- /dev/null +++ b/link @@ -0,0 +1,97 @@ +#!/usr/bin/env php + + * See https://github.com/symfony/symfony/blob/7.3/link + * + * (c) Fabien Potencier + */ + +$copy = false !== $k = array_search('--copy', $argv, true); +$copy && array_splice($argv, $k, 1); +$rollback = false !== $k = array_search('--rollback', $argv, true); +$rollback && array_splice($argv, $k, 1); +$pathToProject = $argv[1] ?? getcwd(); + +if (!is_dir("$pathToProject/vendor/sylius")) { + echo 'Links dependencies of a project to a local clone of the main Sylius/Stack GitHub repository.'.PHP_EOL.PHP_EOL; + echo "Usage: $argv[0] /path/to/the/project".PHP_EOL; + echo ' Use `--copy` to copy dependencies instead of symlink'.PHP_EOL.PHP_EOL; + echo ' Use `--rollback` to rollback'.PHP_EOL.PHP_EOL; + echo "The directory \"$pathToProject\" does not exist or the dependencies are not installed, did you forget to run \"composer install\" in your project?".PHP_EOL; + exit(1); +} + +$stackPackages = []; + +$filesystem = new Filesystem(); +$directories = glob(__DIR__.'/src/*', GLOB_ONLYDIR | GLOB_NOSORT); + +foreach ($directories as $dir) { + if ($filesystem->exists($composer = "$dir/composer.json")) { + $stackPackages[json_decode($filesystem->readFile($composer), flags: JSON_THROW_ON_ERROR)->name] = $dir; + } +} + +foreach (glob("$pathToProject/vendor/sylius/*", GLOB_ONLYDIR | GLOB_NOSORT) as $dir) { + $package = 'sylius/'.basename($dir); + + if (!isset($stackPackages[$package])) { + continue; + } + + if ($rollback) { + $filesystem->remove($dir); + echo "\"$package\" has been rollback from \"$stackPackages[$package]\".".PHP_EOL; + continue; + } + + if (!$copy && is_link($dir)) { + echo "\"$package\" is already a symlink, skipping.".PHP_EOL; + continue; + } + + $sfDir = ('\\' === DIRECTORY_SEPARATOR || $copy) ? $stackPackages[$package] : $filesystem->makePathRelative($stackPackages[$package], dirname(realpath($dir))); + + $filesystem->remove($dir); + + if ($copy) { + $filesystem->mirror($sfDir, $dir); + echo "\"$package\" has been copied from \"$stackPackages[$package]\".".PHP_EOL; + } else { + $filesystem->symlink($sfDir, $dir); + echo "\"$package\" has been linked to \"$stackPackages[$package]\".".PHP_EOL; + } +} + +foreach (glob("$pathToProject/var/cache/*", GLOB_NOSORT) as $cacheDir) { + $filesystem->remove($cacheDir); +} + +if ($rollback) { + echo PHP_EOL."Rollback done, do not forget to run \"composer install\" in your project \"$pathToProject\".".PHP_EOL; +} diff --git a/monorepo-builder.php b/monorepo-builder.php index 98505c2cd..3298891c6 100644 --- a/monorepo-builder.php +++ b/monorepo-builder.php @@ -22,7 +22,7 @@ ComposerJsonSection::REQUIRE_DEV => [ 'phpstan/phpstan' => '^1.10', 'phpstan/phpstan-symfony' => '^1.3', - 'symfony/debug-bundle' => '^6.4 || ^7.0', + 'symfony/debug-bundle' => '^6.4 || ^7.4 || ^8.0', 'symfony/flex' => '^2.4', 'symplify/monorepo-builder' => '11.2.*', ], diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 33f9b132a..8b23813b6 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,6 +1,16 @@ parameters: ignoreErrors: - - message: "#^Unsafe usage of new static\\(\\)\\.$#" + message: "#^Call to method arrayNode\\(\\) on an unknown class Symfony\\\\Component\\\\Config\\\\Definition\\\\Builder\\\\NodeBuilder\\\\.$#" count: 1 - path: src/TwigHooks/src/Hookable/AbstractHookable.php + path: src/TwigExtra/src/Symfony/DependencyInjection/Configuration.php + + - + message: "#^Call to method arrayNode\\(\\) on an unknown class Symfony\\\\Component\\\\Config\\\\Definition\\\\Builder\\\\NodeBuilder\\\\.$#" + count: 2 + path: src/TwigHooks/src/DependencyInjection/Configuration.php + + - + message: "#^Call to method booleanNode\\(\\) on an unknown class Symfony\\\\Component\\\\Config\\\\Definition\\\\Builder\\\\NodeBuilder\\\\.$#" + count: 1 + path: src/TwigHooks/src/DependencyInjection/Configuration.php diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 57dd47cab..03391d70c 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -6,7 +6,6 @@ backupGlobals="false" colors="true" bootstrap="tests/bootstrap.php" - convertDeprecationsToExceptions="false" > diff --git a/src/AdminUi/composer.json b/src/AdminUi/composer.json index 09daa0710..4a555b3f6 100644 --- a/src/AdminUi/composer.json +++ b/src/AdminUi/composer.json @@ -5,28 +5,28 @@ "require": { "php": "^8.1", "knplabs/knp-menu-bundle": "^3.0", - "sylius/twig-hooks": "^0.8", - "symfony/http-kernel": "^6.4 || ^7.0", - "symfony/security-bundle": "^6.4 || ^7.0", - "symfony/security-http": "^6.4 || ^7.0", - "symfony/twig-bundle": "^6.4 || ^7.0", - "sylius/resource-bundle": "^1.11", + "sylius/twig-hooks": "^0.10", + "symfony/http-kernel": "^6.4 || ^7.4 || ^8.0", + "symfony/security-bundle": "^6.4 || ^7.4 || ^8.0", + "symfony/security-http": "^6.4 || ^7.4 || ^8.0", + "symfony/twig-bundle": "^6.4 || ^7.4 || ^8.0", + "sylius/resource-bundle": "^1.13 || ^1.14@alpha", "twig/twig": "^2.15 || ^3.0" }, "require-dev": { - "doctrine/doctrine-bundle": "^2.12", - "doctrine/orm": "^2.0", - "matthiasnoback/symfony-dependency-injection-test": "^5.1", - "phpunit/phpunit": "^9.6", - "sylius/grid-bundle": "^1.13", - "symfony/browser-kit": "^6.4 || ^7.0", - "symfony/config": "^6.4 || ^7.0", - "symfony/console": "^6.4 || ^7.0", - "symfony/css-selector": "^6.4 || ^7.0", - "symfony/dotenv": "^6.4 || ^7.0", - "symfony/framework-bundle": "^6.4 || ^7.0", - "symfony/runtime": "^6.4 || ^7.0", - "symfony/yaml": "^6.4 || ^7.0" + "doctrine/doctrine-bundle": "^2.13 || ^3.0 || ^4.0", + "doctrine/orm": "^3.3 || ^4.0", + "matthiasnoback/symfony-dependency-injection-test": "^6.1.0", + "phpunit/phpunit": "^10.5", + "sylius/grid-bundle": "^1.13 || ^1.15@alpha", + "symfony/browser-kit": "^6.4 || ^7.4 || ^8.0", + "symfony/config": "^6.4 || ^7.4 || ^8.0", + "symfony/console": "^6.4 || ^7.4 || ^8.0", + "symfony/css-selector": "^6.4 || ^7.4 || ^8.0", + "symfony/dotenv": "^6.4 || ^7.4 || ^8.0", + "symfony/framework-bundle": "^6.4 || ^7.4 || ^8.0", + "symfony/runtime": "^6.4 || ^7.4 || ^8.0", + "symfony/yaml": "^6.4 || ^7.4 || ^8.0" }, "suggest": { "sylius/bootstrap-admin-ui": "^0.5" @@ -54,7 +54,7 @@ "require": "7.1.*" }, "branch-alias": { - "dev-main": "0.8-dev" + "dev-main": "0.10-dev" } }, "config": { diff --git a/src/AdminUi/phpunit.xml.dist b/src/AdminUi/phpunit.xml.dist index 324062d0e..b82c87ad6 100644 --- a/src/AdminUi/phpunit.xml.dist +++ b/src/AdminUi/phpunit.xml.dist @@ -5,7 +5,6 @@ xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.5/phpunit.xsd" colors="true" bootstrap="tests/Functional/.application/config/bootstrap.php" - convertDeprecationsToExceptions="true" > diff --git a/src/AdminUi/src/Symfony/DependencyInjection/Configuration.php b/src/AdminUi/src/Symfony/DependencyInjection/Configuration.php index 64fcf157e..72e081c6b 100644 --- a/src/AdminUi/src/Symfony/DependencyInjection/Configuration.php +++ b/src/AdminUi/src/Symfony/DependencyInjection/Configuration.php @@ -35,6 +35,7 @@ private function addRoutingConfiguration(ArrayNodeDefinition $rootNode): void $rootNode ->children() ->arrayNode('routing') + ->setDeprecated('sylius/admin-ui', '0.9', 'This configuration is not used anymore, use the Admin Ui routes instead.') ->useAttributeAsKey('name') ->validate() ->always(static function ($values): array { diff --git a/src/AdminUi/templates/base.html.twig b/src/AdminUi/templates/base.html.twig index 33093e9b3..eca6001d3 100644 --- a/src/AdminUi/templates/base.html.twig +++ b/src/AdminUi/templates/base.html.twig @@ -9,7 +9,10 @@ {% block title %}{% apply striptags %}{% hook generic_hook ~ '#base_title' %}{% endapply %}{% endblock %} - {% block metatags %}{% endblock %} + {% block metatags %} + {% hook generic_hook ~ '#metatags' %} + {% endblock %} + {% block stylesheets %} {% hook generic_hook ~ '#stylesheets' %} {% endblock %} diff --git a/src/AdminUi/templates/crud/create.html.twig b/src/AdminUi/templates/crud/create.html.twig index 3522e2c47..39fc882af 100644 --- a/src/AdminUi/templates/crud/create.html.twig +++ b/src/AdminUi/templates/crud/create.html.twig @@ -1,9 +1,10 @@ {% extends '@SyliusAdminUi/base.html.twig' %} -{% set prefixes = [ +{% set prefixes = configuration.vars.hook_prefix is defined ? [configuration.vars.hook_prefix] %} +{% set prefixes = prefixes|default({})|merge([ 'sylius_admin.%resource_name%'|replace({'%resource_name%': resource_name|default(metadata.name)}), 'sylius_admin.common' -] %} +]) %} {% block title %} {% apply striptags %} diff --git a/src/AdminUi/templates/crud/index.html.twig b/src/AdminUi/templates/crud/index.html.twig index e0413ec3c..f58afa798 100644 --- a/src/AdminUi/templates/crud/index.html.twig +++ b/src/AdminUi/templates/crud/index.html.twig @@ -1,9 +1,10 @@ {% extends '@SyliusAdminUi/base.html.twig' %} -{% set prefixes = [ +{% set prefixes = configuration.vars.hook_prefix is defined ? [configuration.vars.hook_prefix] %} +{% set prefixes = prefixes|default({})|merge([ 'sylius_admin.%resource_name%'|replace({'%resource_name%': resource_name|default(metadata.name)}), 'sylius_admin.common' -] %} +]) %} {% block title %} {% apply striptags %} diff --git a/src/AdminUi/templates/crud/show.html.twig b/src/AdminUi/templates/crud/show.html.twig index dbf495749..f05bfd86c 100644 --- a/src/AdminUi/templates/crud/show.html.twig +++ b/src/AdminUi/templates/crud/show.html.twig @@ -1,9 +1,10 @@ {% extends '@SyliusAdminUi/base.html.twig' %} -{% set prefixes = [ +{% set prefixes = configuration.vars.hook_prefix is defined ? [configuration.vars.hook_prefix] %} +{% set prefixes = prefixes|default({})|merge([ 'sylius_admin.%resource_name%'|replace({'%resource_name%': resource_name|default(metadata.name)}), 'sylius_admin.common' -] %} +]) %} {% block title %} {% apply striptags %} diff --git a/src/AdminUi/templates/crud/update.html.twig b/src/AdminUi/templates/crud/update.html.twig index ed825c7c4..3db8e694e 100644 --- a/src/AdminUi/templates/crud/update.html.twig +++ b/src/AdminUi/templates/crud/update.html.twig @@ -1,9 +1,10 @@ {% extends '@SyliusAdminUi/base.html.twig' %} -{% set prefixes = [ +{% set prefixes = configuration.vars.hook_prefix is defined ? [configuration.vars.hook_prefix] %} +{% set prefixes = prefixes|default({})|merge([ 'sylius_admin.%resource_name%'|replace({'%resource_name%': resource_name|default(metadata.name)}), 'sylius_admin.common' -] %} +]) %} {% block title %} {% apply striptags %} diff --git a/src/AdminUi/tests/Functional/.application/config/packages/doctrine.yaml b/src/AdminUi/tests/Functional/.application/config/packages/doctrine.yaml index d42c52d6d..171cd6588 100644 --- a/src/AdminUi/tests/Functional/.application/config/packages/doctrine.yaml +++ b/src/AdminUi/tests/Functional/.application/config/packages/doctrine.yaml @@ -7,11 +7,7 @@ doctrine: #server_version: '16' profiling_collect_backtrace: '%kernel.debug%' - use_savepoints: true orm: - auto_generate_proxy_classes: true - enable_lazy_ghost_objects: true - report_fields_where_declared: true validate_xml_mapping: true naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware auto_mapping: true diff --git a/src/AdminUi/tests/Functional/.application/config/reference.php b/src/AdminUi/tests/Functional/.application/config/reference.php new file mode 100644 index 000000000..3e0f5f1b7 --- /dev/null +++ b/src/AdminUi/tests/Functional/.application/config/reference.php @@ -0,0 +1,1492 @@ + [ + * 'App\\' => [ + * 'resource' => '../src/', + * ], + * ], + * ]); + * ``` + * + * @psalm-type ImportsConfig = list + * @psalm-type ParametersConfig = array|null>|null> + * @psalm-type ArgumentsType = list|array + * @psalm-type CallType = array|array{0:string, 1?:ArgumentsType, 2?:bool}|array{method:string, arguments?:ArgumentsType, returns_clone?:bool} + * @psalm-type TagsType = list>> // arrays inside the list must have only one element, with the tag name as the key + * @psalm-type CallbackType = string|array{0:string|ReferenceConfigurator,1:string}|\Closure|ReferenceConfigurator|ExpressionConfigurator + * @psalm-type DeprecationType = array{package: string, version: string, message?: string} + * @psalm-type DefaultsType = array{ + * public?: bool, + * tags?: TagsType, + * resource_tags?: TagsType, + * autowire?: bool, + * autoconfigure?: bool, + * bind?: array, + * } + * @psalm-type InstanceofType = array{ + * shared?: bool, + * lazy?: bool|string, + * public?: bool, + * properties?: array, + * configurator?: CallbackType, + * calls?: list, + * tags?: TagsType, + * resource_tags?: TagsType, + * autowire?: bool, + * bind?: array, + * constructor?: string, + * } + * @psalm-type DefinitionType = array{ + * class?: string, + * file?: string, + * parent?: string, + * shared?: bool, + * synthetic?: bool, + * lazy?: bool|string, + * public?: bool, + * abstract?: bool, + * deprecated?: DeprecationType, + * factory?: CallbackType, + * configurator?: CallbackType, + * arguments?: ArgumentsType, + * properties?: array, + * calls?: list, + * tags?: TagsType, + * resource_tags?: TagsType, + * decorates?: string, + * decoration_inner_name?: string, + * decoration_priority?: int, + * decoration_on_invalid?: 'exception'|'ignore'|null, + * autowire?: bool, + * autoconfigure?: bool, + * bind?: array, + * constructor?: string, + * from_callable?: CallbackType, + * } + * @psalm-type AliasType = string|array{ + * alias: string, + * public?: bool, + * deprecated?: DeprecationType, + * } + * @psalm-type PrototypeType = array{ + * resource: string, + * namespace?: string, + * exclude?: string|list, + * parent?: string, + * shared?: bool, + * lazy?: bool|string, + * public?: bool, + * abstract?: bool, + * deprecated?: DeprecationType, + * factory?: CallbackType, + * arguments?: ArgumentsType, + * properties?: array, + * configurator?: CallbackType, + * calls?: list, + * tags?: TagsType, + * resource_tags?: TagsType, + * autowire?: bool, + * autoconfigure?: bool, + * bind?: array, + * constructor?: string, + * } + * @psalm-type StackType = array{ + * stack: list>, + * public?: bool, + * deprecated?: DeprecationType, + * } + * @psalm-type ServicesConfig = array{ + * _defaults?: DefaultsType, + * _instanceof?: InstanceofType, + * ... + * } + * @psalm-type ExtensionType = array + * @psalm-type FrameworkConfig = array{ + * secret?: scalar|null, + * http_method_override?: bool, // 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, + * trust_x_sendfile_type_header?: scalar|null, // Set true to enable support for xsendfile in binary file responses. // Default: "%env(bool:default::SYMFONY_TRUST_X_SENDFILE_TYPE_HEADER)%" + * ide?: scalar|null, // Default: "%env(default::SYMFONY_IDE)%" + * test?: bool, + * default_locale?: scalar|null, // Default: "en" + * set_locale_from_accept_language?: bool, // 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, // Whether to set the Content-Language HTTP header on the Response using the Request locale. // Default: false + * enabled_locales?: list, + * trusted_hosts?: list, + * trusted_proxies?: mixed, // Default: ["%env(default::SYMFONY_TRUSTED_PROXIES)%"] + * trusted_headers?: list, + * error_controller?: scalar|null, // Default: "error_controller" + * handle_all_throwables?: bool, // HttpKernel will handle all kinds of \Throwable. // Default: true + * csrf_protection?: bool|array{ + * enabled?: scalar|null, // Default: null + * stateless_token_ids?: list, + * check_header?: scalar|null, // Whether to check the CSRF token in a header in addition to a cookie when using stateless protection. // Default: false + * cookie_name?: scalar|null, // The name of the cookie to use when using stateless protection. // Default: "csrf-token" + * }, + * form?: bool|array{ // Form configuration + * enabled?: bool, // Default: true + * csrf_protection?: array{ + * enabled?: scalar|null, // Default: null + * token_id?: scalar|null, // Default: null + * field_name?: scalar|null, // Default: "_token" + * field_attr?: array, + * }, + * }, + * http_cache?: bool|array{ // HTTP cache configuration + * enabled?: bool, // Default: false + * debug?: bool, // Default: "%kernel.debug%" + * trace_level?: "none"|"short"|"full", + * trace_header?: scalar|null, + * default_ttl?: int, + * private_headers?: list, + * skip_response_headers?: list, + * allow_reload?: bool, + * allow_revalidate?: bool, + * stale_while_revalidate?: int, + * stale_if_error?: int, + * terminate_on_cache_hit?: bool, + * }, + * esi?: bool|array{ // ESI configuration + * enabled?: bool, // Default: false + * }, + * ssi?: bool|array{ // SSI configuration + * enabled?: bool, // Default: false + * }, + * fragments?: bool|array{ // Fragments configuration + * enabled?: bool, // Default: false + * hinclude_default_template?: scalar|null, // Default: null + * path?: scalar|null, // Default: "/_fragment" + * }, + * profiler?: bool|array{ // Profiler configuration + * enabled?: bool, // Default: false + * collect?: bool, // Default: true + * collect_parameter?: scalar|null, // The name of the parameter to use to enable or disable collection on a per request basis. // Default: null + * only_exceptions?: bool, // Default: false + * only_main_requests?: bool, // Default: false + * dsn?: scalar|null, // Default: "file:%kernel.cache_dir%/profiler" + * collect_serializer_data?: true, // Default: true + * }, + * workflows?: bool|array{ + * enabled?: bool, // Default: false + * workflows?: array, + * definition_validators?: list, + * support_strategy?: scalar|null, + * initial_marking?: list, + * events_to_dispatch?: list|null, + * places?: list, + * }>, + * transitions: list, + * to?: list, + * weight?: int, // Default: 1 + * metadata?: list, + * }>, + * metadata?: list, + * }>, + * }, + * router?: bool|array{ // Router configuration + * enabled?: bool, // Default: false + * resource: scalar|null, + * type?: scalar|null, + * default_uri?: scalar|null, // The default URI used to generate URLs in a non-HTTP context. // Default: null + * http_port?: scalar|null, // Default: 80 + * https_port?: scalar|null, // Default: 443 + * strict_requirements?: scalar|null, // set to true to throw an exception when a parameter does not match the requirements set to false to disable exceptions when a parameter does not match the requirements (and return null instead) set to null to disable parameter checks against requirements 'true' is the preferred configuration in development mode, while 'false' or 'null' might be preferred in production // Default: true + * utf8?: bool, // Default: true + * }, + * session?: bool|array{ // Session configuration + * enabled?: bool, // Default: false + * storage_factory_id?: scalar|null, // Default: "session.storage.factory.native" + * handler_id?: scalar|null, // Defaults to using the native session handler, or to the native *file* session handler if "save_path" is not null. + * name?: scalar|null, + * cookie_lifetime?: scalar|null, + * cookie_path?: scalar|null, + * cookie_domain?: scalar|null, + * cookie_secure?: true|false|"auto", // Default: "auto" + * cookie_httponly?: bool, // Default: true + * cookie_samesite?: null|"lax"|"strict"|"none", // Default: "lax" + * use_cookies?: bool, + * gc_divisor?: scalar|null, + * gc_probability?: scalar|null, + * gc_maxlifetime?: scalar|null, + * save_path?: scalar|null, // Defaults to "%kernel.cache_dir%/sessions" if the "handler_id" option is not null. + * metadata_update_threshold?: int, // Seconds to wait between 2 session metadata updates. // Default: 0 + * }, + * request?: bool|array{ // Request configuration + * enabled?: bool, // Default: false + * formats?: array>, + * }, + * assets?: bool|array{ // Assets configuration + * enabled?: bool, // Default: true + * strict_mode?: bool, // Throw an exception if an entry is missing from the manifest.json. // Default: false + * version_strategy?: scalar|null, // Default: null + * version?: scalar|null, // Default: null + * version_format?: scalar|null, // Default: "%%s?%%s" + * json_manifest_path?: scalar|null, // Default: null + * base_path?: scalar|null, // Default: "" + * base_urls?: list, + * packages?: array, + * }>, + * }, + * asset_mapper?: bool|array{ // Asset Mapper configuration + * enabled?: bool, // Default: true + * paths?: array, + * excluded_patterns?: list, + * exclude_dotfiles?: bool, // If true, any files starting with "." will be excluded from the asset mapper. // Default: true + * server?: bool, // If true, a "dev server" will return the assets from the public directory (true in "debug" mode only by default). // Default: true + * public_prefix?: scalar|null, // The public path where the assets will be written to (and served from when "server" is true). // Default: "/assets/" + * missing_import_mode?: "strict"|"warn"|"ignore", // Behavior if an asset cannot be found when imported from JavaScript or CSS files - e.g. "import './non-existent.js'". "strict" means an exception is thrown, "warn" means a warning is logged, "ignore" means the import is left as-is. // Default: "warn" + * extensions?: array, + * importmap_path?: scalar|null, // The path of the importmap.php file. // Default: "%kernel.project_dir%/importmap.php" + * importmap_polyfill?: scalar|null, // The importmap name that will be used to load the polyfill. Set to false to disable. // Default: "es-module-shims" + * importmap_script_attributes?: array, + * vendor_dir?: scalar|null, // The directory to store JavaScript vendors. // Default: "%kernel.project_dir%/assets/vendor" + * precompress?: bool|array{ // Precompress assets with Brotli, Zstandard and gzip. + * enabled?: bool, // Default: false + * formats?: list, + * extensions?: list, + * }, + * }, + * translator?: bool|array{ // Translator configuration + * enabled?: bool, // Default: true + * fallbacks?: list, + * logging?: bool, // Default: false + * formatter?: scalar|null, // Default: "translator.formatter.default" + * cache_dir?: scalar|null, // Default: "%kernel.cache_dir%/translations" + * default_path?: scalar|null, // The default path used to load translations. // Default: "%kernel.project_dir%/translations" + * paths?: list, + * pseudo_localization?: bool|array{ + * enabled?: bool, // Default: false + * accents?: bool, // Default: true + * expansion_factor?: float, // Default: 1.0 + * brackets?: bool, // Default: true + * parse_html?: bool, // Default: false + * localizable_html_attributes?: list, + * }, + * providers?: array, + * locales?: list, + * }>, + * globals?: array, + * domain?: string, + * }>, + * }, + * validation?: bool|array{ // Validation configuration + * enabled?: bool, // Default: true + * enable_attributes?: bool, // Default: true + * static_method?: list, + * translation_domain?: scalar|null, // Default: "validators" + * email_validation_mode?: "html5"|"html5-allow-no-tld"|"strict", // Default: "html5" + * mapping?: array{ + * paths?: list, + * }, + * not_compromised_password?: bool|array{ + * enabled?: bool, // When disabled, compromised passwords will be accepted as valid. // Default: true + * endpoint?: scalar|null, // API endpoint for the NotCompromisedPassword Validator. // Default: null + * }, + * disable_translation?: bool, // Default: false + * auto_mapping?: array, + * }>, + * }, + * serializer?: bool|array{ // Serializer configuration + * enabled?: bool, // Default: false + * enable_attributes?: bool, // Default: true + * name_converter?: scalar|null, + * circular_reference_handler?: scalar|null, + * max_depth_handler?: scalar|null, + * mapping?: array{ + * paths?: list, + * }, + * default_context?: list, + * named_serializers?: array, + * include_built_in_normalizers?: bool, // Whether to include the built-in normalizers // Default: true + * include_built_in_encoders?: bool, // Whether to include the built-in encoders // Default: true + * }>, + * }, + * property_access?: bool|array{ // Property access configuration + * enabled?: bool, // Default: true + * magic_call?: bool, // Default: false + * magic_get?: bool, // Default: true + * magic_set?: bool, // Default: true + * throw_exception_on_invalid_index?: bool, // Default: false + * throw_exception_on_invalid_property_path?: bool, // Default: true + * }, + * type_info?: bool|array{ // Type info configuration + * enabled?: bool, // Default: true + * aliases?: array, + * }, + * property_info?: bool|array{ // Property info configuration + * enabled?: bool, // Default: true + * with_constructor_extractor?: bool, // Registers the constructor extractor. // Default: true + * }, + * cache?: array{ // Cache configuration + * prefix_seed?: scalar|null, // Used to namespace cache keys when using several apps with the same shared backend. // Default: "_%kernel.project_dir%.%kernel.container_class%" + * app?: scalar|null, // App related cache pools configuration. // Default: "cache.adapter.filesystem" + * system?: scalar|null, // System related cache pools configuration. // Default: "cache.adapter.system" + * directory?: scalar|null, // Default: "%kernel.share_dir%/pools/app" + * default_psr6_provider?: scalar|null, + * default_redis_provider?: scalar|null, // Default: "redis://localhost" + * default_valkey_provider?: scalar|null, // Default: "valkey://localhost" + * default_memcached_provider?: scalar|null, // Default: "memcached://localhost" + * default_doctrine_dbal_provider?: scalar|null, // Default: "database_connection" + * default_pdo_provider?: scalar|null, // Default: null + * pools?: array, + * tags?: scalar|null, // Default: null + * public?: bool, // Default: false + * default_lifetime?: scalar|null, // Default lifetime of the pool. + * provider?: scalar|null, // Overwrite the setting from the default provider for this adapter. + * early_expiration_message_bus?: scalar|null, + * clearer?: scalar|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, // Throw PHP errors as \ErrorException instances. // Default: true + * }, + * exceptions?: array, + * web_link?: bool|array{ // Web links configuration + * enabled?: bool, // Default: false + * }, + * lock?: bool|string|array{ // Lock configuration + * enabled?: bool, // Default: false + * resources?: array>, + * }, + * semaphore?: bool|string|array{ // Semaphore configuration + * enabled?: bool, // Default: false + * resources?: array, + * }, + * messenger?: bool|array{ // Messenger configuration + * enabled?: bool, // Default: false + * routing?: array, + * }>, + * serializer?: array{ + * default_serializer?: scalar|null, // Service id to use as the default serializer for the transports. // Default: "messenger.transport.native_php_serializer" + * symfony_serializer?: array{ + * format?: scalar|null, // Serialization format for the messenger.transport.symfony_serializer service (which is not the serializer used by default). // Default: "json" + * context?: array, + * }, + * }, + * transports?: array, + * failure_transport?: scalar|null, // Transport name to send failed messages to (after all retries have failed). // Default: null + * retry_strategy?: string|array{ + * service?: scalar|null, // Service id to override the retry strategy entirely. // Default: null + * max_retries?: int, // Default: 3 + * delay?: int, // Time in ms to delay (or the initial value when multiplier is used). // Default: 1000 + * multiplier?: float, // If greater than 1, delay will grow exponentially for each retry: this delay = (delay * (multiple ^ retries)). // Default: 2 + * max_delay?: int, // Max time in ms that a retry should ever be delayed (0 = infinite). // Default: 0 + * jitter?: float, // Randomness to apply to the delay (between 0 and 1). // Default: 0.1 + * }, + * rate_limiter?: scalar|null, // Rate limiter name to use when processing messages. // Default: null + * }>, + * failure_transport?: scalar|null, // Transport name to send failed messages to (after all retries have failed). // Default: null + * stop_worker_on_signals?: list, + * default_bus?: scalar|null, // Default: null + * buses?: array, + * }>, + * }>, + * }, + * scheduler?: bool|array{ // Scheduler configuration + * enabled?: bool, // Default: false + * }, + * disallow_search_engine_index?: bool, // Enabled by default when debug is enabled. // Default: true + * http_client?: bool|array{ // HTTP Client configuration + * enabled?: bool, // Default: true + * max_host_connections?: int, // The maximum number of connections to a single host. + * default_options?: array{ + * headers?: array, + * vars?: array, + * max_redirects?: int, // The maximum number of redirects to follow. + * http_version?: scalar|null, // The default HTTP version, typically 1.1 or 2.0, leave to null for the best version. + * resolve?: array, + * proxy?: scalar|null, // The URL of the proxy to pass requests through or null for automatic detection. + * no_proxy?: scalar|null, // A comma separated list of hosts that do not require a proxy to be reached. + * timeout?: float, // The idle timeout, defaults to the "default_socket_timeout" ini parameter. + * max_duration?: float, // The maximum execution time for the request+response as a whole. + * bindto?: scalar|null, // A network interface name, IP address, a host name or a UNIX socket to bind to. + * verify_peer?: bool, // Indicates if the peer should be verified in a TLS context. + * verify_host?: bool, // Indicates if the host should exist as a certificate common name. + * cafile?: scalar|null, // A certificate authority file. + * capath?: scalar|null, // A directory that contains multiple certificate authority files. + * local_cert?: scalar|null, // A PEM formatted certificate file. + * local_pk?: scalar|null, // A private key file. + * passphrase?: scalar|null, // The passphrase used to encrypt the "local_pk" file. + * ciphers?: scalar|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|null, // The minimum version of TLS to accept; must be one of STREAM_CRYPTO_METHOD_TLSv*_CLIENT constants. + * extra?: array, + * rate_limiter?: scalar|null, // Rate limiter name to use for throttling requests. // Default: null + * caching?: bool|array{ // Caching configuration. + * enabled?: bool, // Default: false + * cache_pool?: string, // The taggable cache pool to use for storing the responses. // Default: "cache.http_client" + * shared?: bool, // Indicates whether the cache is shared (public) or private. // Default: true + * max_ttl?: int, // The maximum TTL (in seconds) allowed for cached responses. Null means no cap. // Default: null + * }, + * retry_failed?: bool|array{ + * enabled?: bool, // Default: false + * retry_strategy?: scalar|null, // service id to override the retry strategy. // Default: null + * http_codes?: array, + * }>, + * max_retries?: int, // Default: 3 + * delay?: int, // Time in ms to delay (or the initial value when multiplier is used). // Default: 1000 + * multiplier?: float, // If greater than 1, delay will grow exponentially for each retry: delay * (multiple ^ retries). // Default: 2 + * max_delay?: int, // Max time in ms that a retry should ever be delayed (0 = infinite). // Default: 0 + * jitter?: float, // Randomness in percent (between 0 and 1) to apply to the delay. // Default: 0.1 + * }, + * }, + * mock_response_factory?: scalar|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, // The maximum number of redirects to follow. + * http_version?: scalar|null, // The default HTTP version, typically 1.1 or 2.0, leave to null for the best version. + * resolve?: array, + * proxy?: scalar|null, // The URL of the proxy to pass requests through or null for automatic detection. + * no_proxy?: scalar|null, // A comma separated list of hosts that do not require a proxy to be reached. + * timeout?: float, // The idle timeout, defaults to the "default_socket_timeout" ini parameter. + * max_duration?: float, // The maximum execution time for the request+response as a whole. + * bindto?: scalar|null, // A network interface name, IP address, a host name or a UNIX socket to bind to. + * verify_peer?: bool, // Indicates if the peer should be verified in a TLS context. + * verify_host?: bool, // Indicates if the host should exist as a certificate common name. + * cafile?: scalar|null, // A certificate authority file. + * capath?: scalar|null, // A directory that contains multiple certificate authority files. + * local_cert?: scalar|null, // A PEM formatted certificate file. + * local_pk?: scalar|null, // A private key file. + * passphrase?: scalar|null, // The passphrase used to encrypt the "local_pk" file. + * ciphers?: scalar|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|null, // The minimum version of TLS to accept; must be one of STREAM_CRYPTO_METHOD_TLSv*_CLIENT constants. + * extra?: array, + * rate_limiter?: scalar|null, // Rate limiter name to use for throttling requests. // Default: null + * caching?: bool|array{ // Caching configuration. + * enabled?: bool, // Default: false + * cache_pool?: string, // The taggable cache pool to use for storing the responses. // Default: "cache.http_client" + * shared?: bool, // Indicates whether the cache is shared (public) or private. // Default: true + * max_ttl?: int, // The maximum TTL (in seconds) allowed for cached responses. Null means no cap. // Default: null + * }, + * retry_failed?: bool|array{ + * enabled?: bool, // Default: false + * retry_strategy?: scalar|null, // service id to override the retry strategy. // Default: null + * http_codes?: array, + * }>, + * max_retries?: int, // Default: 3 + * delay?: int, // Time in ms to delay (or the initial value when multiplier is used). // Default: 1000 + * multiplier?: float, // If greater than 1, delay will grow exponentially for each retry: delay * (multiple ^ retries). // Default: 2 + * max_delay?: int, // Max time in ms that a retry should ever be delayed (0 = infinite). // Default: 0 + * jitter?: float, // Randomness in percent (between 0 and 1) to apply to the delay. // Default: 0.1 + * }, + * }>, + * }, + * mailer?: bool|array{ // Mailer configuration + * enabled?: bool, // Default: false + * message_bus?: scalar|null, // The message bus to use. Defaults to the default bus if the Messenger component is installed. // Default: null + * dsn?: scalar|null, // Default: null + * transports?: array, + * envelope?: array{ // Mailer Envelope configuration + * sender?: scalar|null, + * recipients?: list, + * allowed_recipients?: list, + * }, + * headers?: array, + * dkim_signer?: bool|array{ // DKIM signer configuration + * enabled?: bool, // Default: false + * key?: scalar|null, // Key content, or path to key (in PEM format with the `file://` prefix) // Default: "" + * domain?: scalar|null, // Default: "" + * select?: scalar|null, // Default: "" + * passphrase?: scalar|null, // The private key passphrase // Default: "" + * options?: array, + * }, + * smime_signer?: bool|array{ // S/MIME signer configuration + * enabled?: bool, // Default: false + * key?: scalar|null, // Path to key (in PEM format) // Default: "" + * certificate?: scalar|null, // Path to certificate (in PEM format without the `file://` prefix) // Default: "" + * passphrase?: scalar|null, // The private key passphrase // Default: null + * extra_certificates?: scalar|null, // Default: null + * sign_options?: int, // Default: null + * }, + * smime_encrypter?: bool|array{ // S/MIME encrypter configuration + * enabled?: bool, // Default: false + * repository?: scalar|null, // S/MIME certificate repository service. This service shall implement the `Symfony\Component\Mailer\EventListener\SmimeCertificateRepositoryInterface`. // Default: "" + * cipher?: int, // A set of algorithms used to encrypt the message // Default: null + * }, + * }, + * secrets?: bool|array{ + * enabled?: bool, // Default: true + * vault_directory?: scalar|null, // Default: "%kernel.project_dir%/config/secrets/%kernel.runtime_environment%" + * local_dotenv_file?: scalar|null, // Default: "%kernel.project_dir%/.env.%kernel.runtime_environment%.local" + * decryption_env_var?: scalar|null, // Default: "base64:default::SYMFONY_DECRYPTION_SECRET" + * }, + * notifier?: bool|array{ // Notifier configuration + * enabled?: bool, // Default: false + * message_bus?: scalar|null, // The message bus to use. Defaults to the default bus if the Messenger component is installed. // Default: null + * chatter_transports?: array, + * texter_transports?: array, + * notification_on_failed_messages?: bool, // Default: false + * channel_policy?: array>, + * admin_recipients?: list, + * }, + * rate_limiter?: bool|array{ // Rate limiter configuration + * enabled?: bool, // Default: false + * limiters?: array, + * limit?: int, // The maximum allowed hits in a fixed interval or burst. + * interval?: scalar|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|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, // Amount of tokens to add each interval. // Default: 1 + * }, + * }>, + * }, + * uid?: bool|array{ // Uid configuration + * enabled?: bool, // Default: false + * default_uuid_version?: 7|6|4|1, // Default: 7 + * name_based_uuid_version?: 5|3, // Default: 5 + * name_based_uuid_namespace?: scalar|null, + * time_based_uuid_version?: 7|6|1, // Default: 7 + * time_based_uuid_node?: scalar|null, + * }, + * html_sanitizer?: bool|array{ // HtmlSanitizer configuration + * enabled?: bool, // Default: false + * sanitizers?: array, + * block_elements?: list, + * drop_elements?: list, + * allow_attributes?: array, + * drop_attributes?: array, + * force_attributes?: array>, + * force_https_urls?: bool, // 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, // 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, // 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, // The maximum length allowed for the sanitized input. // Default: 0 + * }>, + * }, + * webhook?: bool|array{ // Webhook configuration + * enabled?: bool, // Default: false + * message_bus?: scalar|null, // The message bus to use. // Default: "messenger.default_bus" + * routing?: array, + * }, + * remote-event?: bool|array{ // RemoteEvent configuration + * enabled?: bool, // Default: false + * }, + * json_streamer?: bool|array{ // JSON streamer configuration + * enabled?: bool, // Default: false + * }, + * } + * @psalm-type TwigConfig = array{ + * form_themes?: list, + * globals?: array, + * autoescape_service?: scalar|null, // Default: null + * autoescape_service_method?: scalar|null, // Default: null + * cache?: scalar|null, // Default: true + * charset?: scalar|null, // Default: "%kernel.charset%" + * debug?: bool, // Default: "%kernel.debug%" + * strict_variables?: bool, // Default: "%kernel.debug%" + * auto_reload?: scalar|null, + * optimizations?: int, + * default_path?: scalar|null, // The default path used to load templates. // Default: "%kernel.project_dir%/templates" + * file_name_pattern?: list, + * paths?: array, + * date?: array{ // The default format options used by the date filter. + * format?: scalar|null, // Default: "F j, Y H:i" + * interval_format?: scalar|null, // Default: "%d days" + * timezone?: scalar|null, // The timezone used when formatting dates, when set to null, the timezone returned by date_default_timezone_get() is used. // Default: null + * }, + * number_format?: array{ // The default format options for the number_format filter. + * decimals?: int, // Default: 0 + * decimal_point?: scalar|null, // Default: "." + * thousands_separator?: scalar|null, // Default: "," + * }, + * mailer?: array{ + * html_to_text_converter?: scalar|null, // A service implementing the "Symfony\Component\Mime\HtmlToTextConverter\HtmlToTextConverterInterface". // Default: null + * }, + * } + * @psalm-type SecurityConfig = array{ + * access_denied_url?: scalar|null, // Default: null + * session_fixation_strategy?: "none"|"migrate"|"invalidate", // Default: "migrate" + * expose_security_errors?: \Symfony\Component\Security\Http\Authentication\ExposeSecurityLevel::None|\Symfony\Component\Security\Http\Authentication\ExposeSecurityLevel::AccountStatus|\Symfony\Component\Security\Http\Authentication\ExposeSecurityLevel::All, // Default: "none" + * erase_credentials?: bool, // Default: true + * access_decision_manager?: array{ + * strategy?: "affirmative"|"consensus"|"unanimous"|"priority", + * service?: scalar|null, + * strategy_service?: scalar|null, + * allow_if_all_abstain?: bool, // Default: false + * allow_if_equal_granted_denied?: bool, // Default: true + * }, + * password_hashers?: array, + * hash_algorithm?: scalar|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|null, // Default: 40 + * ignore_case?: bool, // Default: false + * encode_as_base64?: bool, // Default: true + * iterations?: scalar|null, // Default: 5000 + * cost?: int, // Default: null + * memory_cost?: scalar|null, // Default: null + * time_cost?: scalar|null, // Default: null + * id?: scalar|null, + * }>, + * providers?: array, + * }, + * memory?: array{ + * users?: array, + * }>, + * }, + * ldap?: array{ + * service: scalar|null, + * base_dn: scalar|null, + * search_dn?: scalar|null, // Default: null + * search_password?: scalar|null, // Default: null + * extra_fields?: list, + * default_roles?: list, + * role_fetcher?: scalar|null, // Default: null + * uid_key?: scalar|null, // Default: "sAMAccountName" + * filter?: scalar|null, // Default: "({uid_key}={user_identifier})" + * password_attribute?: scalar|null, // Default: null + * }, + * entity?: array{ + * class: scalar|null, // The full entity class name of your user class. + * property?: scalar|null, // Default: null + * manager_name?: scalar|null, // Default: null + * }, + * }>, + * firewalls: array, + * security?: bool, // Default: true + * user_checker?: scalar|null, // The UserChecker to use when authenticating users in this firewall. // Default: "security.user_checker" + * request_matcher?: scalar|null, + * access_denied_url?: scalar|null, + * access_denied_handler?: scalar|null, + * entry_point?: scalar|null, // An enabled authenticator name or a service id that implements "Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface". + * provider?: scalar|null, + * stateless?: bool, // Default: false + * lazy?: bool, // Default: false + * context?: scalar|null, + * logout?: array{ + * enable_csrf?: bool|null, // Default: null + * csrf_token_id?: scalar|null, // Default: "logout" + * csrf_parameter?: scalar|null, // Default: "_csrf_token" + * csrf_token_manager?: scalar|null, + * path?: scalar|null, // Default: "/logout" + * target?: scalar|null, // Default: "/" + * invalidate_session?: bool, // Default: true + * clear_site_data?: list<"*"|"cache"|"cookies"|"storage"|"executionContexts">, + * delete_cookies?: array, + * }, + * switch_user?: array{ + * provider?: scalar|null, + * parameter?: scalar|null, // Default: "_switch_user" + * role?: scalar|null, // Default: "ROLE_ALLOWED_TO_SWITCH" + * target_route?: scalar|null, // Default: null + * }, + * required_badges?: list, + * custom_authenticators?: list, + * login_throttling?: array{ + * limiter?: scalar|null, // A service id implementing "Symfony\Component\HttpFoundation\RateLimiter\RequestRateLimiterInterface". + * max_attempts?: int, // Default: 5 + * interval?: scalar|null, // Default: "1 minute" + * lock_factory?: scalar|null, // The service ID of the lock factory used by the login rate limiter (or null to disable locking). // Default: null + * cache_pool?: string, // The cache pool to use for storing the limiter state // Default: "cache.rate_limiter" + * storage_service?: string, // The service ID of a custom storage implementation, this precedes any configured "cache_pool" // Default: null + * }, + * x509?: array{ + * provider?: scalar|null, + * user?: scalar|null, // Default: "SSL_CLIENT_S_DN_Email" + * credentials?: scalar|null, // Default: "SSL_CLIENT_S_DN" + * user_identifier?: scalar|null, // Default: "emailAddress" + * }, + * remote_user?: array{ + * provider?: scalar|null, + * user?: scalar|null, // Default: "REMOTE_USER" + * }, + * login_link?: array{ + * check_route: scalar|null, // Route that will validate the login link - e.g. "app_login_link_verify". + * check_post_only?: scalar|null, // If true, only HTTP POST requests to "check_route" will be handled by the authenticator. // Default: false + * signature_properties: list, + * lifetime?: int, // The lifetime of the login link in seconds. // Default: 600 + * max_uses?: int, // Max number of times a login link can be used - null means unlimited within lifetime. // Default: null + * used_link_cache?: scalar|null, // Cache service id used to expired links of max_uses is set. + * success_handler?: scalar|null, // A service id that implements Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface. + * failure_handler?: scalar|null, // A service id that implements Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface. + * provider?: scalar|null, // The user provider to load users from. + * secret?: scalar|null, // Default: "%kernel.secret%" + * always_use_default_target_path?: bool, // Default: false + * default_target_path?: scalar|null, // Default: "/" + * login_path?: scalar|null, // Default: "/login" + * target_path_parameter?: scalar|null, // Default: "_target_path" + * use_referer?: bool, // Default: false + * failure_path?: scalar|null, // Default: null + * failure_forward?: bool, // Default: false + * failure_path_parameter?: scalar|null, // Default: "_failure_path" + * }, + * form_login?: array{ + * provider?: scalar|null, + * remember_me?: bool, // Default: true + * success_handler?: scalar|null, + * failure_handler?: scalar|null, + * check_path?: scalar|null, // Default: "/login_check" + * use_forward?: bool, // Default: false + * login_path?: scalar|null, // Default: "/login" + * username_parameter?: scalar|null, // Default: "_username" + * password_parameter?: scalar|null, // Default: "_password" + * csrf_parameter?: scalar|null, // Default: "_csrf_token" + * csrf_token_id?: scalar|null, // Default: "authenticate" + * enable_csrf?: bool, // Default: false + * post_only?: bool, // Default: true + * form_only?: bool, // Default: false + * always_use_default_target_path?: bool, // Default: false + * default_target_path?: scalar|null, // Default: "/" + * target_path_parameter?: scalar|null, // Default: "_target_path" + * use_referer?: bool, // Default: false + * failure_path?: scalar|null, // Default: null + * failure_forward?: bool, // Default: false + * failure_path_parameter?: scalar|null, // Default: "_failure_path" + * }, + * form_login_ldap?: array{ + * provider?: scalar|null, + * remember_me?: bool, // Default: true + * success_handler?: scalar|null, + * failure_handler?: scalar|null, + * check_path?: scalar|null, // Default: "/login_check" + * use_forward?: bool, // Default: false + * login_path?: scalar|null, // Default: "/login" + * username_parameter?: scalar|null, // Default: "_username" + * password_parameter?: scalar|null, // Default: "_password" + * csrf_parameter?: scalar|null, // Default: "_csrf_token" + * csrf_token_id?: scalar|null, // Default: "authenticate" + * enable_csrf?: bool, // Default: false + * post_only?: bool, // Default: true + * form_only?: bool, // Default: false + * always_use_default_target_path?: bool, // Default: false + * default_target_path?: scalar|null, // Default: "/" + * target_path_parameter?: scalar|null, // Default: "_target_path" + * use_referer?: bool, // Default: false + * failure_path?: scalar|null, // Default: null + * failure_forward?: bool, // Default: false + * failure_path_parameter?: scalar|null, // Default: "_failure_path" + * service?: scalar|null, // Default: "ldap" + * dn_string?: scalar|null, // Default: "{user_identifier}" + * query_string?: scalar|null, + * search_dn?: scalar|null, // Default: "" + * search_password?: scalar|null, // Default: "" + * }, + * json_login?: array{ + * provider?: scalar|null, + * remember_me?: bool, // Default: true + * success_handler?: scalar|null, + * failure_handler?: scalar|null, + * check_path?: scalar|null, // Default: "/login_check" + * use_forward?: bool, // Default: false + * login_path?: scalar|null, // Default: "/login" + * username_path?: scalar|null, // Default: "username" + * password_path?: scalar|null, // Default: "password" + * }, + * json_login_ldap?: array{ + * provider?: scalar|null, + * remember_me?: bool, // Default: true + * success_handler?: scalar|null, + * failure_handler?: scalar|null, + * check_path?: scalar|null, // Default: "/login_check" + * use_forward?: bool, // Default: false + * login_path?: scalar|null, // Default: "/login" + * username_path?: scalar|null, // Default: "username" + * password_path?: scalar|null, // Default: "password" + * service?: scalar|null, // Default: "ldap" + * dn_string?: scalar|null, // Default: "{user_identifier}" + * query_string?: scalar|null, + * search_dn?: scalar|null, // Default: "" + * search_password?: scalar|null, // Default: "" + * }, + * access_token?: array{ + * provider?: scalar|null, + * remember_me?: bool, // Default: true + * success_handler?: scalar|null, + * failure_handler?: scalar|null, + * realm?: scalar|null, // Default: null + * token_extractors?: list, + * token_handler: string|array{ + * id?: scalar|null, + * oidc_user_info?: string|array{ + * base_uri: scalar|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|null, // Cache service id to use to cache the OIDC discovery configuration. + * }, + * }, + * claim?: scalar|null, // Claim which contains the user identifier (e.g. sub, email, etc.). // Default: "sub" + * client?: scalar|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|null, // Cache service id to use to cache the OIDC discovery configuration. + * }, + * }, + * claim?: scalar|null, // Claim which contains the user identifier (e.g.: sub, email..). // Default: "sub" + * audience: scalar|null, // Audience set in the token, for validation purpose. + * issuers: list, + * algorithms: list, + * keyset?: scalar|null, // JSON-encoded JWKSet used to sign the token (must contain a list of valid public keys). + * encryption?: bool|array{ + * enabled?: bool, // Default: false + * enforce?: bool, // When enabled, the token shall be encrypted. // Default: false + * algorithms: list, + * keyset: scalar|null, // JSON-encoded JWKSet used to decrypt the token (must contain a list of valid private keys). + * }, + * }, + * cas?: array{ + * validation_url: scalar|null, // CAS server validation URL + * prefix?: scalar|null, // CAS prefix // Default: "cas" + * http_client?: scalar|null, // HTTP Client service // Default: null + * }, + * oauth2?: scalar|null, + * }, + * }, + * http_basic?: array{ + * provider?: scalar|null, + * realm?: scalar|null, // Default: "Secured Area" + * }, + * http_basic_ldap?: array{ + * provider?: scalar|null, + * realm?: scalar|null, // Default: "Secured Area" + * service?: scalar|null, // Default: "ldap" + * dn_string?: scalar|null, // Default: "{user_identifier}" + * query_string?: scalar|null, + * search_dn?: scalar|null, // Default: "" + * search_password?: scalar|null, // Default: "" + * }, + * remember_me?: array{ + * secret?: scalar|null, // Default: "%kernel.secret%" + * service?: scalar|null, + * user_providers?: list, + * catch_exceptions?: bool, // Default: true + * signature_properties?: list, + * token_provider?: string|array{ + * service?: scalar|null, // The service ID of a custom remember-me token provider. + * doctrine?: bool|array{ + * enabled?: bool, // Default: false + * connection?: scalar|null, // Default: null + * }, + * }, + * token_verifier?: scalar|null, // The service ID of a custom rememberme token verifier. + * name?: scalar|null, // Default: "REMEMBERME" + * lifetime?: int, // Default: 31536000 + * path?: scalar|null, // Default: "/" + * domain?: scalar|null, // Default: null + * secure?: true|false|"auto", // Default: null + * httponly?: bool, // Default: true + * samesite?: null|"lax"|"strict"|"none", // Default: "lax" + * always_remember_me?: bool, // Default: false + * remember_me_parameter?: scalar|null, // Default: "_remember_me" + * }, + * }>, + * access_control?: list, + * attributes?: array, + * route?: scalar|null, // Default: null + * methods?: list, + * allow_if?: scalar|null, // Default: null + * roles?: list, + * }>, + * role_hierarchy?: array>, + * } + * @psalm-type TwigComponentConfig = array{ + * defaults?: array, + * anonymous_template_directory?: scalar|null, // Defaults to `components` + * profiler?: bool, // Enables the profiler for Twig Component (in debug mode) // Default: "%kernel.debug%" + * controllers_json?: scalar|null, // Deprecated: The "twig_component.controllers_json" config option is deprecated, and will be removed in 3.0. // Default: null + * } + * @psalm-type SyliusTwigHooksConfig = array{ + * enable_autoprefixing?: bool, // Default: false + * hook_name_section_separator?: scalar|null, // Default: false + * supported_hookable_types?: array, + * hooks?: array, + * props?: array, + * configuration?: array, + * priority?: int, // Default: null + * }>>, + * } + * @psalm-type SyliusResourceConfig = array{ + * resources?: array, + * settings?: array{ + * paginate?: mixed, // Default: null + * limit?: mixed, // Default: null + * allowed_paginate?: list, + * default_page_size?: int, // Default: 10 + * default_templates_dir?: scalar|null, // Default: null + * sortable?: bool, // Default: false + * sorting?: mixed, // Default: null + * filterable?: bool, // Default: false + * criteria?: mixed, // Default: null + * state_machine_component?: scalar|null, // Default: null + * }, + * translation?: bool|array{ + * enabled?: bool, // Default: true + * locale_provider?: scalar|null, // Default: "sylius.translation_locale_provider.immutable" + * }, + * drivers?: list<"doctrine/orm"|"doctrine/mongodb-odm"|"doctrine/phpcr-odm">, + * mapping?: array{ + * imports?: list, + * paths?: list, + * }, + * authorization_checker?: scalar|null, // Default: "sylius.resource_controller.authorization_checker.disabled" + * routing_path_bc_layer?: bool, + * path_segment_name_generator?: scalar|null, // Specify a path name generator to use. // Default: "sylius.metadata.path_segment_name_generator.dash" + * } + * @psalm-type SyliusAdminUiConfig = array{ + * routing?: array, + * } + * @psalm-type SyliusGridConfig = array{ + * drivers?: list<"doctrine/orm"|"doctrine/phpcr-odm">, + * templates?: array{ + * filter?: array, + * action?: array, + * bulk_action?: array, + * }, + * grids?: array, + * }, + * sorting?: array, + * limits?: list, + * fields?: array, + * }>, + * filters?: array, + * form_options?: list, + * default_value?: mixed, + * }>, + * actions?: array, + * }>>, + * removals?: array, + * }>, + * } + * @psalm-type DoctrineConfig = array{ + * dbal?: array{ + * default_connection?: scalar|null, + * types?: array, + * driver_schemes?: array, + * connections?: array, + * mapping_types?: array, + * default_table_options?: array, + * schema_manager_factory?: scalar|null, // Default: "doctrine.dbal.default_schema_manager_factory" + * result_cache?: scalar|null, + * replicas?: array, + * }>, + * }, + * orm?: array{ + * default_entity_manager?: scalar|null, + * enable_native_lazy_objects?: bool, // Deprecated: The "enable_native_lazy_objects" option is deprecated and will be removed in DoctrineBundle 4.0, as native lazy objects are now always enabled. // Default: true + * controller_resolver?: bool|array{ + * enabled?: bool, // Default: true + * auto_mapping?: bool, // Deprecated: The "doctrine.orm.controller_resolver.auto_mapping.auto_mapping" option is deprecated and will be removed in DoctrineBundle 4.0, as it only accepts `false` since 3.0. // Set to true to enable using route placeholders as lookup criteria when the primary key doesn't match the argument name // Default: false + * evict_cache?: bool, // Set to true to fetch the entity from the database instead of using the cache, if any // Default: false + * }, + * entity_managers?: array, + * }>, + * }>, + * }, + * connection?: scalar|null, + * class_metadata_factory_name?: scalar|null, // Default: "Doctrine\\ORM\\Mapping\\ClassMetadataFactory" + * default_repository_class?: scalar|null, // Default: "Doctrine\\ORM\\EntityRepository" + * auto_mapping?: scalar|null, // Default: false + * naming_strategy?: scalar|null, // Default: "doctrine.orm.naming_strategy.default" + * quote_strategy?: scalar|null, // Default: "doctrine.orm.quote_strategy.default" + * typed_field_mapper?: scalar|null, // Default: "doctrine.orm.typed_field_mapper.default" + * entity_listener_resolver?: scalar|null, // Default: null + * fetch_mode_subselect_batch_size?: scalar|null, + * repository_factory?: scalar|null, // Default: "doctrine.orm.container_repository_factory" + * schema_ignore_classes?: list, + * validate_xml_mapping?: bool, // Set to "true" to opt-in to the new mapping driver mode that was added in Doctrine ORM 2.14 and will be mandatory in ORM 3.0. See https://github.com/doctrine/orm/pull/6728. // Default: false + * second_level_cache?: array{ + * region_cache_driver?: string|array{ + * type?: scalar|null, // Default: null + * id?: scalar|null, + * pool?: scalar|null, + * }, + * region_lock_lifetime?: scalar|null, // Default: 60 + * log_enabled?: bool, // Default: true + * region_lifetime?: scalar|null, // Default: 3600 + * enabled?: bool, // Default: true + * factory?: scalar|null, + * regions?: array, + * loggers?: array, + * }, + * hydrators?: array, + * mappings?: array, + * dql?: array{ + * string_functions?: array, + * numeric_functions?: array, + * datetime_functions?: array, + * }, + * filters?: array, + * }>, + * identity_generation_preferences?: array, + * }>, + * resolve_target_entities?: array, + * }, + * } + * @psalm-type WhiteOctoberPagerfantaConfig = array{ // Deprecated: The "white_october_pagerfanta" configuration node is deprecated, migrate your configuration to the "babdev_pagerfanta" configuration node. + * exceptions_strategy?: array{ + * out_of_range_page?: scalar|null, // Default: "to_http_not_found" + * not_valid_current_page?: scalar|null, // Default: "to_http_not_found" + * }, + * default_view?: scalar|null, // Default: "default" + * } + * @psalm-type ConfigType = array{ + * imports?: ImportsConfig, + * parameters?: ParametersConfig, + * services?: ServicesConfig, + * framework?: FrameworkConfig, + * twig?: TwigConfig, + * security?: SecurityConfig, + * twig_component?: TwigComponentConfig, + * sylius_twig_hooks?: SyliusTwigHooksConfig, + * sylius_resource?: SyliusResourceConfig, + * sylius_admin_ui?: SyliusAdminUiConfig, + * sylius_grid?: SyliusGridConfig, + * doctrine?: DoctrineConfig, + * white_october_pagerfanta?: WhiteOctoberPagerfantaConfig, + * "when@dev"?: array{ + * imports?: ImportsConfig, + * parameters?: ParametersConfig, + * services?: ServicesConfig, + * framework?: FrameworkConfig, + * twig?: TwigConfig, + * security?: SecurityConfig, + * twig_component?: TwigComponentConfig, + * sylius_twig_hooks?: SyliusTwigHooksConfig, + * sylius_resource?: SyliusResourceConfig, + * sylius_admin_ui?: SyliusAdminUiConfig, + * sylius_grid?: SyliusGridConfig, + * doctrine?: DoctrineConfig, + * }, + * "when@prod"?: array{ + * imports?: ImportsConfig, + * parameters?: ParametersConfig, + * services?: ServicesConfig, + * framework?: FrameworkConfig, + * twig?: TwigConfig, + * security?: SecurityConfig, + * twig_component?: TwigComponentConfig, + * sylius_twig_hooks?: SyliusTwigHooksConfig, + * sylius_resource?: SyliusResourceConfig, + * sylius_admin_ui?: SyliusAdminUiConfig, + * sylius_grid?: SyliusGridConfig, + * doctrine?: DoctrineConfig, + * }, + * "when@test"?: array{ + * imports?: ImportsConfig, + * parameters?: ParametersConfig, + * services?: ServicesConfig, + * framework?: FrameworkConfig, + * twig?: TwigConfig, + * security?: SecurityConfig, + * twig_component?: TwigComponentConfig, + * sylius_twig_hooks?: SyliusTwigHooksConfig, + * sylius_resource?: SyliusResourceConfig, + * sylius_admin_ui?: SyliusAdminUiConfig, + * sylius_grid?: SyliusGridConfig, + * doctrine?: DoctrineConfig, + * }, + * ..., + * }> + * } + */ +final class App +{ + /** + * @param ConfigType $config + * + * @psalm-return ConfigType + */ + public static function config(array $config): array + { + return AppReference::config($config); + } +} + +namespace Symfony\Component\Routing\Loader\Configurator; + +/** + * This class provides array-shapes for configuring the routes of an application. + * + * Example: + * + * ```php + * // config/routes.php + * namespace Symfony\Component\Routing\Loader\Configurator; + * + * return Routes::config([ + * 'controllers' => [ + * 'resource' => 'routing.controllers', + * ], + * ]); + * ``` + * + * @psalm-type RouteConfig = array{ + * path: string|array, + * controller?: string, + * methods?: string|list, + * requirements?: array, + * defaults?: array, + * options?: array, + * host?: string|array, + * schemes?: string|list, + * condition?: string, + * locale?: string, + * format?: string, + * utf8?: bool, + * stateless?: bool, + * } + * @psalm-type ImportConfig = array{ + * resource: string, + * type?: string, + * exclude?: string|list, + * prefix?: string|array, + * name_prefix?: string, + * trailing_slash_on_root?: bool, + * controller?: string, + * methods?: string|list, + * requirements?: array, + * defaults?: array, + * options?: array, + * host?: string|array, + * schemes?: string|list, + * condition?: string, + * locale?: string, + * format?: string, + * utf8?: bool, + * stateless?: bool, + * } + * @psalm-type AliasConfig = array{ + * alias: string, + * deprecated?: array{package:string, version:string, message?:string}, + * } + * @psalm-type RoutesConfig = array{ + * "when@dev"?: array, + * "when@prod"?: array, + * "when@test"?: array, + * ... + * } + */ +final class Routes +{ + /** + * @param RoutesConfig $config + * + * @psalm-return RoutesConfig + */ + public static function config(array $config): array + { + return $config; + } +} diff --git a/src/AdminUi/tests/Functional/.application/config/routes/framework.yaml b/src/AdminUi/tests/Functional/.application/config/routes/framework.yaml index 0fc74bbac..bc1feace1 100644 --- a/src/AdminUi/tests/Functional/.application/config/routes/framework.yaml +++ b/src/AdminUi/tests/Functional/.application/config/routes/framework.yaml @@ -1,4 +1,4 @@ when@dev: _errors: - resource: '@FrameworkBundle/Resources/config/routing/errors.xml' + resource: '@FrameworkBundle/Resources/config/routing/errors.php' prefix: /_error diff --git a/src/AdminUi/tests/Functional/.application/config/routes/sylius_resource.yaml b/src/AdminUi/tests/Functional/.application/config/routes/sylius_resource.yaml index 0c8ade2c2..e4c326435 100644 --- a/src/AdminUi/tests/Functional/.application/config/routes/sylius_resource.yaml +++ b/src/AdminUi/tests/Functional/.application/config/routes/sylius_resource.yaml @@ -2,6 +2,6 @@ sylius_crud_routes: resource: 'sylius.routing.loader.crud_routes_attributes' type: service -sylius_routes: - resource: 'sylius.routing.loader.routes_attributes' +sylius_resource_routes: + resource: 'sylius.symfony.routing.loader.resource' type: service diff --git a/src/BootstrapAdminUi/assets/styles/_body.scss b/src/BootstrapAdminUi/assets/styles/_body.scss index 5bd6d4835..ecce06584 100644 --- a/src/BootstrapAdminUi/assets/styles/_body.scss +++ b/src/BootstrapAdminUi/assets/styles/_body.scss @@ -10,6 +10,9 @@ * { --tblr-breadcrumb-item-active-color: var(--tblr-gray-500); --tblr-breadcrumb-divider-color: var(--tblr-gray-300); + --tblr-code-color: #36393B; + --tblr-blue-rgb: 17, 81, 141; + --tblr-green-rgb: 0, 97, 16; } [data-bs-theme=dark] { diff --git a/src/BootstrapAdminUi/assets/styles/_buttons.scss b/src/BootstrapAdminUi/assets/styles/_buttons.scss new file mode 100644 index 000000000..ef66bfdf9 --- /dev/null +++ b/src/BootstrapAdminUi/assets/styles/_buttons.scss @@ -0,0 +1,4 @@ +.btn:not(.btn-sm) { + min-height: 44px; + min-width: 44px; +} diff --git a/src/BootstrapAdminUi/assets/styles/_form.scss b/src/BootstrapAdminUi/assets/styles/_form.scss index 2496a070c..fed144e8d 100644 --- a/src/BootstrapAdminUi/assets/styles/_form.scss +++ b/src/BootstrapAdminUi/assets/styles/_form.scss @@ -55,6 +55,10 @@ textarea.form-control { } } +.form-control, .form-select { + min-height: 44px; +} + .form-control:focus { box-shadow: none; } diff --git a/src/BootstrapAdminUi/assets/styles/_navbar.scss b/src/BootstrapAdminUi/assets/styles/_navbar.scss index 588164c44..a52c6b283 100644 --- a/src/BootstrapAdminUi/assets/styles/_navbar.scss +++ b/src/BootstrapAdminUi/assets/styles/_navbar.scss @@ -11,18 +11,33 @@ &.navbar-nav { gap: 0.5rem; } +} - &.navbar-brand-image { - height: 4rem; - } +.navbar input { + min-height: 44px; +} + +.navbar-brand-image { + height: 3rem; } .navbar-collapse a.nav-link, .navbar-collapse a.dropdown-item { transition: all .1s; + min-height: 44px; &:hover { color: var(--tblr-primary) !important; background: transparent; } } +.nav-button { + min-height: 44px; + min-width: 44px; +} +.navbar-collapse .nav-link:focus-visible { + outline: solid 2px rgb(153, 200, 255); +} +.navbar-plus-badge{ + width: 50px; +} diff --git a/src/BootstrapAdminUi/assets/styles/main.scss b/src/BootstrapAdminUi/assets/styles/main.scss index 1efcd9974..0f3ca3b16 100644 --- a/src/BootstrapAdminUi/assets/styles/main.scss +++ b/src/BootstrapAdminUi/assets/styles/main.scss @@ -12,6 +12,7 @@ @import "@tabler/core/src/scss/tabler.scss"; @import "body"; +@import "buttons"; @import "accordion"; @import "alert"; @import "datatable"; diff --git a/src/BootstrapAdminUi/composer.json b/src/BootstrapAdminUi/composer.json index 0d35139e1..8f7617725 100644 --- a/src/BootstrapAdminUi/composer.json +++ b/src/BootstrapAdminUi/composer.json @@ -5,30 +5,30 @@ "require": { "php": "^8.1", "pagerfanta/twig": "^4.6", - "sylius/admin-ui": "^0.8", - "sylius/grid-bundle": "^1.13", - "sylius/resource-bundle": "^1.11", - "sylius/twig-extra": "^0.8", - "sylius/twig-hooks": "^0.8", - "symfony/asset": "^6.4 || ^7.0", - "symfony/http-kernel": "^6.4 || ^7.0", - "symfony/http-client": "^6.4 || ^7.0", - "symfony/security-bundle": "^6.4 || ^7.0", + "sylius/admin-ui": "^0.10", + "sylius/grid-bundle": "^1.13 || ^1.15@alpha", + "sylius/resource-bundle": "^1.13 || ^1.14@alpha", + "sylius/twig-extra": "^0.10", + "sylius/twig-hooks": "^0.10", + "symfony/asset": "^6.4 || ^7.4 || ^8.0", + "symfony/http-kernel": "^6.4 || ^7.4 || ^8.0", + "symfony/http-client": "^6.4 || ^7.4 || ^8.0", + "symfony/security-bundle": "^6.4 || ^7.4 || ^8.0", "symfony/ux-autocomplete": "^2.17", "symfony/ux-icons": "^2.20", "symfony/ux-live-component": "^2.17" }, "require-dev": { - "doctrine/doctrine-bundle": "^2.12", - "doctrine/orm": "^2.0", - "phpunit/phpunit": "^9.6", - "symfony/browser-kit": "^6.4 || ^7.0", - "symfony/console": "^6.4 || ^7.0", - "symfony/css-selector": "^6.4 || ^7.0", - "symfony/dotenv": "^6.4 || ^7.0", - "symfony/framework-bundle": "^6.4 || ^7.0", - "symfony/runtime": "^6.4 || ^7.0", - "symfony/yaml": "^6.4 || ^7.0" + "doctrine/doctrine-bundle": "^2.13 || ^3.0 || ^4.0", + "doctrine/orm": "^3.3 || ^4.0", + "phpunit/phpunit": "^10.5", + "symfony/browser-kit": "^6.4 || ^7.4 || ^8.0", + "symfony/console": "^6.4 || ^7.4 || ^8.0", + "symfony/css-selector": "^6.4 || ^7.4 || ^8.0", + "symfony/dotenv": "^6.4 || ^7.4 || ^8.0", + "symfony/framework-bundle": "^6.4 || ^7.4 || ^8.0", + "symfony/runtime": "^6.4 || ^7.4 || ^8.0", + "symfony/yaml": "^6.4 || ^7.4 || ^8.0" }, "autoload": { "psr-4": { @@ -53,13 +53,14 @@ "type": "path", "url": "../**" } + ], "extra": { "symfony": { "require": "7.1.*" }, "branch-alias": { - "dev-main": "0.8-dev" + "dev-main": "0.10-dev" } }, "minimum-stability": "dev", diff --git a/src/BootstrapAdminUi/config/app/twig_hooks/common/create.php b/src/BootstrapAdminUi/config/app/twig_hooks/common/create.php index 4ae3262be..7ebb509e3 100644 --- a/src/BootstrapAdminUi/config/app/twig_hooks/common/create.php +++ b/src/BootstrapAdminUi/config/app/twig_hooks/common/create.php @@ -41,6 +41,10 @@ 'content' => [ 'template' => '@SyliusBootstrapAdminUi/shared/crud/common/content.html.twig', ], + 'footer' => [ + 'template' => '@SyliusBootstrapAdminUi/shared/crud/common/content/footer.html.twig', + 'priority' => -100, + ], ], 'sylius_admin.common.create.content' => [ diff --git a/src/BootstrapAdminUi/config/app/twig_hooks/common/index.php b/src/BootstrapAdminUi/config/app/twig_hooks/common/index.php index 515b25bdf..ba393a3b1 100644 --- a/src/BootstrapAdminUi/config/app/twig_hooks/common/index.php +++ b/src/BootstrapAdminUi/config/app/twig_hooks/common/index.php @@ -53,6 +53,10 @@ 'grid' => [ 'template' => '@SyliusBootstrapAdminUi/shared/crud/index/content/grid.html.twig', ], + 'footer' => [ + 'template' => '@SyliusBootstrapAdminUi/shared/crud/common/content/footer.html.twig', + 'priority' => -100, + ], ], 'sylius_admin.common.index.content.header' => [ diff --git a/src/BootstrapAdminUi/config/app/twig_hooks/common/show.php b/src/BootstrapAdminUi/config/app/twig_hooks/common/show.php index 8f52883ef..4bb26885e 100644 --- a/src/BootstrapAdminUi/config/app/twig_hooks/common/show.php +++ b/src/BootstrapAdminUi/config/app/twig_hooks/common/show.php @@ -41,6 +41,10 @@ 'content' => [ 'template' => '@SyliusBootstrapAdminUi/shared/crud/common/content.html.twig', ], + 'footer' => [ + 'template' => '@SyliusBootstrapAdminUi/shared/crud/common/content/footer.html.twig', + 'priority' => -100, + ], ], 'sylius_admin.common.show.content' => [ diff --git a/src/BootstrapAdminUi/config/app/twig_hooks/common/update.php b/src/BootstrapAdminUi/config/app/twig_hooks/common/update.php index 5ea178680..efa2a2384 100644 --- a/src/BootstrapAdminUi/config/app/twig_hooks/common/update.php +++ b/src/BootstrapAdminUi/config/app/twig_hooks/common/update.php @@ -41,6 +41,10 @@ 'content' => [ 'template' => '@SyliusBootstrapAdminUi/shared/crud/common/content.html.twig', ], + 'footer' => [ + 'template' => '@SyliusBootstrapAdminUi/shared/crud/common/content/footer.html.twig', + 'priority' => -100, + ], ], 'sylius_admin.common.update.content' => [ diff --git a/src/BootstrapAdminUi/phpunit.xml.dist b/src/BootstrapAdminUi/phpunit.xml.dist index 5e1e6bf28..682d7a860 100644 --- a/src/BootstrapAdminUi/phpunit.xml.dist +++ b/src/BootstrapAdminUi/phpunit.xml.dist @@ -5,7 +5,6 @@ xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.5/phpunit.xsd" colors="true" bootstrap="tests/Functional/.application/config/bootstrap.php" - convertDeprecationsToExceptions="true" > diff --git a/src/BootstrapAdminUi/public/app.css b/src/BootstrapAdminUi/public/app.css index 8f3c2a927..773ef909b 100644 --- a/src/BootstrapAdminUi/public/app.css +++ b/src/BootstrapAdminUi/public/app.css @@ -20,7 +20,7 @@ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. - */*{--tblr-breadcrumb-item-active-color:var(--tblr-gray-500);--tblr-breadcrumb-divider-color:var(--tblr-gray-300)}[data-bs-theme=dark],body[data-bs-theme=dark] [data-bs-theme=light]{--tblr-bg-surface:#1e2433}@font-face{font-family:Inter;font-weight:1 999;src:url(fonts/Inter-VariableFont_slnt,wght.853e0197.ttf) format("truetype-variations")}body{--bs-body-bg:#f6f8fb;--bs-tertiary-bg:#f6f8fb;--bs-body-color:#212529;font-feature-settings:"cv03","cv04","cv11"}a{text-underline-offset:.25em}a.link-reset{text-decoration:none}.btn-collapse.collapsed .icon-chevron-right{display:inline-flex}.btn-collapse.collapsed .icon-chevron-down,.btn-collapse:not(.collapsed) .icon-chevron-right{display:none}.btn-collapse:not(.collapsed) .icon-chevron-down{display:inline-flex}.breadcrumb-item a{text-decoration:none}.breadcrumb-item a:hover{color:#22b99a}.switch-collapse,body[data-bs-theme=dark] html[data-bs-theme=light] [data-theme-switch=dark],html[data-bs-theme=dark] [data-theme-switch=dark],html[data-bs-theme=light] [data-theme-switch=light]{display:none}label:has(input:checked)~.switch-collapse{display:block}/*! + */*{--tblr-breadcrumb-item-active-color:var(--tblr-gray-500);--tblr-breadcrumb-divider-color:var(--tblr-gray-300);--tblr-code-color:#36393b;--tblr-blue-rgb:17,81,141;--tblr-green-rgb:0,97,16}[data-bs-theme=dark],body[data-bs-theme=dark] [data-bs-theme=light]{--tblr-bg-surface:#1e2433}@font-face{font-family:Inter;font-weight:1 999;src:url(fonts/Inter-VariableFont_slnt,wght.853e0197.ttf) format("truetype-variations")}body{--bs-body-bg:#f6f8fb;--bs-tertiary-bg:#f6f8fb;--bs-body-color:#212529;font-feature-settings:"cv03","cv04","cv11"}a{text-underline-offset:.25em}a.link-reset{text-decoration:none}.btn-collapse.collapsed .icon-chevron-right{display:inline-flex}.btn-collapse.collapsed .icon-chevron-down,.btn-collapse:not(.collapsed) .icon-chevron-right{display:none}.btn-collapse:not(.collapsed) .icon-chevron-down{display:inline-flex}.breadcrumb-item a{text-decoration:none}.breadcrumb-item a:hover{color:#22b99a}.switch-collapse,body[data-bs-theme=dark] html[data-bs-theme=light] [data-theme-switch=dark],html[data-bs-theme=dark] [data-theme-switch=dark],html[data-bs-theme=light] [data-theme-switch=light]{display:none}label:has(input:checked)~.switch-collapse{display:block}.btn:not(.btn-sm){min-height:44px;min-width:44px}/*! * This file is part of the Sylius package. * * (c) Sylius Sp. z o.o. @@ -62,7 +62,7 @@ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. - */textarea.form-control{height:12rem;min-height:8rem}.accordion-item:has(.accordion-error),.list-group-item.active:has(.tab-error),.list-group-item:has(.tab-error){border-left:2px solid #ff0017}.form-select:disabled{color:var(--tblr-gray-600)}.input-group{box-shadow:none}.input-group .form-check{flex-grow:1}.input-group .input-group-text{color:var(--tblr-gray-600)}.form-control:focus{box-shadow:none}/*! + */textarea.form-control{height:12rem;min-height:8rem}.accordion-item:has(.accordion-error),.list-group-item.active:has(.tab-error),.list-group-item:has(.tab-error){border-left:2px solid #ff0017}.form-select:disabled{color:var(--tblr-gray-600)}.input-group{box-shadow:none}.input-group .form-check{flex-grow:1}.input-group .input-group-text{color:var(--tblr-gray-600)}.form-control,.form-select{min-height:44px}.form-control:focus{box-shadow:none}/*! * This file is part of the Sylius package. * * (c) Sylius Sp. z o.o. @@ -76,7 +76,7 @@ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. - */.sylius.navbar-nav{gap:.5rem}.sylius.navbar-brand-image{height:4rem}.navbar-collapse a.dropdown-item,.navbar-collapse a.nav-link{transition:all .1s}.navbar-collapse a.dropdown-item:hover,.navbar-collapse a.nav-link:hover{background:transparent;color:var(--tblr-primary)!important}svg.icon *{stroke-width:1.5}/*! + */.sylius.navbar-nav{gap:.5rem}.navbar input{min-height:44px}.navbar-brand-image{height:3rem}.navbar-collapse a.dropdown-item,.navbar-collapse a.nav-link{min-height:44px;transition:all .1s}.navbar-collapse a.dropdown-item:hover,.navbar-collapse a.nav-link:hover{background:transparent;color:var(--tblr-primary)!important}.nav-button{min-height:44px;min-width:44px}.navbar-collapse .nav-link:focus-visible{outline:2px solid #99c8ff}.navbar-plus-badge{width:50px}svg.icon *{stroke-width:1.5}/*! * This file is part of the Sylius package. * * (c) Sylius Sp. z o.o. diff --git a/src/BootstrapAdminUi/public/app.rtl.css b/src/BootstrapAdminUi/public/app.rtl.css index 4479da89a..cbd0b8f57 100644 --- a/src/BootstrapAdminUi/public/app.rtl.css +++ b/src/BootstrapAdminUi/public/app.rtl.css @@ -20,7 +20,7 @@ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. - */*{--tblr-breadcrumb-item-active-color:var(--tblr-gray-500);--tblr-breadcrumb-divider-color:var(--tblr-gray-300)}[data-bs-theme=dark],body[data-bs-theme=dark] [data-bs-theme=light]{--tblr-bg-surface:#1e2433}@font-face{font-family:Inter;font-weight:1 999;src:url(fonts/Inter-VariableFont_slnt,wght.853e0197.ttf) format("truetype-variations")}body{--bs-body-bg:#f6f8fb;--bs-tertiary-bg:#f6f8fb;--bs-body-color:#212529;font-feature-settings:"cv03","cv04","cv11"}a{text-underline-offset:.25em}a.link-reset{text-decoration:none}.btn-collapse.collapsed .icon-chevron-right{display:inline-flex}.btn-collapse.collapsed .icon-chevron-down,.btn-collapse:not(.collapsed) .icon-chevron-right{display:none}.btn-collapse:not(.collapsed) .icon-chevron-down{display:inline-flex}.breadcrumb-item a{text-decoration:none}.breadcrumb-item a:hover{color:#22b99a}.switch-collapse,body[data-bs-theme=dark] html[data-bs-theme=light] [data-theme-switch=dark],html[data-bs-theme=dark] [data-theme-switch=dark],html[data-bs-theme=light] [data-theme-switch=light]{display:none}label:has(input:checked)~.switch-collapse{display:block}/*! + */*{--tblr-breadcrumb-item-active-color:var(--tblr-gray-500);--tblr-breadcrumb-divider-color:var(--tblr-gray-300);--tblr-code-color:#36393b;--tblr-blue-rgb:17,81,141;--tblr-green-rgb:0,97,16}[data-bs-theme=dark],body[data-bs-theme=dark] [data-bs-theme=light]{--tblr-bg-surface:#1e2433}@font-face{font-family:Inter;font-weight:1 999;src:url(fonts/Inter-VariableFont_slnt,wght.853e0197.ttf) format("truetype-variations")}body{--bs-body-bg:#f6f8fb;--bs-tertiary-bg:#f6f8fb;--bs-body-color:#212529;font-feature-settings:"cv03","cv04","cv11"}a{text-underline-offset:.25em}a.link-reset{text-decoration:none}.btn-collapse.collapsed .icon-chevron-right{display:inline-flex}.btn-collapse.collapsed .icon-chevron-down,.btn-collapse:not(.collapsed) .icon-chevron-right{display:none}.btn-collapse:not(.collapsed) .icon-chevron-down{display:inline-flex}.breadcrumb-item a{text-decoration:none}.breadcrumb-item a:hover{color:#22b99a}.switch-collapse,body[data-bs-theme=dark] html[data-bs-theme=light] [data-theme-switch=dark],html[data-bs-theme=dark] [data-theme-switch=dark],html[data-bs-theme=light] [data-theme-switch=light]{display:none}label:has(input:checked)~.switch-collapse{display:block}.btn:not(.btn-sm){min-height:44px;min-width:44px}/*! * This file is part of the Sylius package. * * (c) Sylius Sp. z o.o. @@ -62,7 +62,7 @@ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. - */textarea.form-control{height:12rem;min-height:8rem}.accordion-item:has(.accordion-error),.list-group-item.active:has(.tab-error),.list-group-item:has(.tab-error){border-right:2px solid #ff0017}.form-select:disabled{color:var(--tblr-gray-600)}.input-group{box-shadow:none}.input-group .form-check{flex-grow:1}.input-group .input-group-text{color:var(--tblr-gray-600)}.form-control:focus{box-shadow:none}/*! + */textarea.form-control{height:12rem;min-height:8rem}.accordion-item:has(.accordion-error),.list-group-item.active:has(.tab-error),.list-group-item:has(.tab-error){border-right:2px solid #ff0017}.form-select:disabled{color:var(--tblr-gray-600)}.input-group{box-shadow:none}.input-group .form-check{flex-grow:1}.input-group .input-group-text{color:var(--tblr-gray-600)}.form-control,.form-select{min-height:44px}.form-control:focus{box-shadow:none}/*! * This file is part of the Sylius package. * * (c) Sylius Sp. z o.o. @@ -76,7 +76,7 @@ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. - */.sylius.navbar-nav{gap:.5rem}.sylius.navbar-brand-image{height:4rem}.navbar-collapse a.dropdown-item,.navbar-collapse a.nav-link{transition:all .1s}.navbar-collapse a.dropdown-item:hover,.navbar-collapse a.nav-link:hover{background:transparent;color:var(--tblr-primary)!important}svg.icon *{stroke-width:1.5}/*! + */.sylius.navbar-nav{gap:.5rem}.navbar input{min-height:44px}.navbar-brand-image{height:3rem}.navbar-collapse a.dropdown-item,.navbar-collapse a.nav-link{min-height:44px;transition:all .1s}.navbar-collapse a.dropdown-item:hover,.navbar-collapse a.nav-link:hover{background:transparent;color:var(--tblr-primary)!important}.nav-button{min-height:44px;min-width:44px}.navbar-collapse .nav-link:focus-visible{outline:2px solid #99c8ff}.navbar-plus-badge{width:50px}svg.icon *{stroke-width:1.5}/*! * This file is part of the Sylius package. * * (c) Sylius Sp. z o.o. diff --git a/src/BootstrapAdminUi/templates/shared/crud/common/content/footer.html.twig b/src/BootstrapAdminUi/templates/shared/crud/common/content/footer.html.twig new file mode 100644 index 000000000..cc6afca39 --- /dev/null +++ b/src/BootstrapAdminUi/templates/shared/crud/common/content/footer.html.twig @@ -0,0 +1,42 @@ + diff --git a/src/BootstrapAdminUi/templates/shared/crud/common/content/header/title_block/actions.html.twig b/src/BootstrapAdminUi/templates/shared/crud/common/content/header/title_block/actions.html.twig index e911cf720..554329491 100644 --- a/src/BootstrapAdminUi/templates/shared/crud/common/content/header/title_block/actions.html.twig +++ b/src/BootstrapAdminUi/templates/shared/crud/common/content/header/title_block/actions.html.twig @@ -2,8 +2,9 @@
    + {% hook 'actions' %} {% if resources.definition.actionGroups.main is defined %} - {% for action in resources.definition.getEnabledActions('main') %} + {% for action in resources.definition.getEnabledActions('main')|sylius_sort_by('position') %} {{ sylius_grid_render_action(resources, action, null) }} {% endfor %} {% endif %} diff --git a/src/BootstrapAdminUi/templates/shared/crud/common/sidebar/logo.html.twig b/src/BootstrapAdminUi/templates/shared/crud/common/sidebar/logo.html.twig index 2fb171b2e..082c692ad 100644 --- a/src/BootstrapAdminUi/templates/shared/crud/common/sidebar/logo.html.twig +++ b/src/BootstrapAdminUi/templates/shared/crud/common/sidebar/logo.html.twig @@ -1,7 +1,5 @@ -{% set dashboard_path = hookable_metadata.context.routing.dashboard_path|default('/admin') %} -

    - + {% hook 'logo' %}

    diff --git a/src/BootstrapAdminUi/templates/shared/crud/common/sidebar/menu/menu.html.twig b/src/BootstrapAdminUi/templates/shared/crud/common/sidebar/menu/menu.html.twig index 1ec465fa5..24ea978d9 100644 --- a/src/BootstrapAdminUi/templates/shared/crud/common/sidebar/menu/menu.html.twig +++ b/src/BootstrapAdminUi/templates/shared/crud/common/sidebar/menu/menu.html.twig @@ -1,4 +1,4 @@ -{% extends 'knp_menu.html.twig' %} +{% extends '@KnpMenu/menu.html.twig' %} {% block root %}