diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index 08e37fdeb4..d7940c9922 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -89,9 +89,10 @@ jobs:
TOKEN: ${{ secrets.EZROBOT_PAT }}
run: |
curl -H "Authorization: token $TOKEN" -L https://github.com/ibexa/vale-styles/archive/refs/heads/main.zip -o vale.zip
- unzip vale.zip
+ unzip vale.zip -d vale
rm vale.zip
- mv vale-styles-main/* vale-styles-main/.vale.ini .
+ rm -rf vale/vale-styles-main/tests
+ mv vale/vale-styles-main/* vale/vale-styles-main/.vale.ini .
- name: Run Vale.sh
uses: vale-cli/vale-action@v2
diff --git a/.github/workflows/code_samples.yaml b/.github/workflows/code_samples.yaml
index 9764f2ab1a..6609b8dde7 100644
--- a/.github/workflows/code_samples.yaml
+++ b/.github/workflows/code_samples.yaml
@@ -57,6 +57,10 @@ jobs:
continue-on-error: true
run: composer check-rector
+ - name: Run YAML snippet tests
+ continue-on-error: true
+ run: composer check-yaml
+
code-samples-inclusion-check:
name: Check code samples inclusion
runs-on: ubuntu-latest
@@ -83,7 +87,7 @@ jobs:
- name: Log target branch code_samples usage
if: steps.list.outputs.CODE_SAMPLES_CHANGE != ''
run: |
- git fetch origin
+ git fetch origin --depth=1 ${{ github.head_ref }}
git checkout origin/${{ github.head_ref }} -- tools/code_samples/code_samples_usage.php
php tools/code_samples/code_samples_usage.php ${{ steps.list.outputs.CODE_SAMPLES_CHANGE }} > $HOME/code_samples_usage_target.txt
diff --git a/.gitignore b/.gitignore
index d243a010bd..10f1a32e27 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,3 +12,4 @@ auth.json
yarn.lock
docs/css/*.map
.deptrac.cache
+.phpunit.result.cache
diff --git a/code_samples/back_office/components/twig_components.yaml b/code_samples/back_office/components/twig_components.yaml
index 6e0458f4d0..20d8c3caa2 100644
--- a/code_samples/back_office/components/twig_components.yaml
+++ b/code_samples/back_office/components/twig_components.yaml
@@ -12,7 +12,6 @@ ibexa_twig_components:
priority: 0
arguments:
content: 'Hello world!'
- admin-ui-user-menu:
duplicated_user_menu:
type: menu
arguments:
diff --git a/code_samples/back_office/online_editor/config/packages/custom_plugin.yaml b/code_samples/back_office/online_editor/config/packages/custom_plugin.yaml
deleted file mode 100644
index 426b9b75b9..0000000000
--- a/code_samples/back_office/online_editor/config/packages/custom_plugin.yaml
+++ /dev/null
@@ -1,13 +0,0 @@
-ibexa:
- system:
- admin_group:
- fieldtypes:
- ibexa_richtext:
- toolbars:
- paragraph:
- buttons:
- date:
- priority: 0
-ibexa_fieldtype_richtext:
- alloy_editor:
- extra_plugins: [date]
diff --git a/code_samples/front/shop/order-management/config/packages/ibexa.yaml b/code_samples/front/shop/order-management/config/packages/ibexa.yaml
index e17cfbc9e5..310eaca7c3 100644
--- a/code_samples/front/shop/order-management/config/packages/ibexa.yaml
+++ b/code_samples/front/shop/order-management/config/packages/ibexa.yaml
@@ -65,7 +65,7 @@ framework:
to:
- dropped
-// ...
+# ...
ibexa:
repositories:
diff --git a/code_samples/front/shop/payment/src/bundle/Resources/config/services/payment_method.yaml b/code_samples/front/shop/payment/src/bundle/Resources/config/services/payment_method.yaml
index b179640c05..ef3b33c817 100644
--- a/code_samples/front/shop/payment/src/bundle/Resources/config/services/payment_method.yaml
+++ b/code_samples/front/shop/payment/src/bundle/Resources/config/services/payment_method.yaml
@@ -8,7 +8,7 @@ services:
$domain:
tags:
- { name: ibexa.payment.payment_method.type, alias: new_payment_method_type }
-services:
+
App\Payment\PaymentMethod\Voter\NewPaymentMethodTypeVoter:
tags:
- - { name: ibexa.payment.payment_method.voter, type: new_payment_method_type }
\ No newline at end of file
+ - { name: ibexa.payment.payment_method.voter, type: new_payment_method_type }
diff --git a/code_samples/recommendations/config/packages/ibexa_connector_raptor.yaml b/code_samples/recommendations/config/packages/ibexa_connector_raptor.yaml
index 61c90252b1..1898c35aae 100644
--- a/code_samples/recommendations/config/packages/ibexa_connector_raptor.yaml
+++ b/code_samples/recommendations/config/packages/ibexa_connector_raptor.yaml
@@ -3,14 +3,14 @@ ibexa:
:
connector_raptor:
enabled: true
- customer_id: ~ # Required
+ customer_id: "12345" # Required
tracking_type: client # One of: "client" or "server"
# Raptor Recommendations API key
- recommendations_api_key: ~ # Required
+ recommendations_api_key: "your_api_key_here" # Required
- # Raptor Recommendations API URL, optional, set by default
- recommendations_api_url: '%ibexa.connector.raptor.recommendations.api_url%'
+ # Raptor Recommendations API URI, optional, set by default
+ recommendations_api_uri: '%ibexa.connector.raptor.recommendations.api_uri%'
ibexa_connector_raptor:
# When enabled, tracking exceptions are thrown instead of being silently handled
strict_exceptions: true
diff --git a/composer.json b/composer.json
index e9415fc8fd..a2c6f57fb6 100644
--- a/composer.json
+++ b/composer.json
@@ -4,6 +4,9 @@
"type": "library",
"license": "GNU General Public License v2.0",
"autoload-dev": {
+ "psr-4": {
+ "Ibexa\\Tests\\Documentation\\": "tests/"
+ }
},
"repositories": [
{
@@ -15,6 +18,9 @@
"php": "^8.3"
},
"require-dev": {
+ "phpunit/phpunit": "^11.0",
+ "symfony/yaml": "^7.0",
+ "ibexa/connector-gemini": "5.0.x-dev",
"ibexa/automated-translation": "5.0.x-dev",
"ibexa/code-style": "~2.0.0",
"friendsofphp/php-cs-fixer": "^3.30",
@@ -51,7 +57,7 @@
"ibexa/page-builder": "5.0.x-dev",
"ibexa/order-management": "5.0.x-dev",
"ibexa/calendar": "5.0.x-dev",
- "ibexa/payment": "5.0.x-dev",
+ "ibexa/payment": "~5.0.x-dev",
"ibexa/shipping": "5.0.x-dev",
"ibexa/fieldtype-matrix": "5.0.x-dev",
"ibexa/storefront": "5.0.x-dev",
@@ -85,21 +91,33 @@
"ibexa/cdp": "~5.0.x-dev",
"ibexa/connector-raptor": "~5.0.x-dev",
"ibexa/image-editor": "~5.0.x-dev",
- "ibexa/integrated-help": "~5.0.x-dev"
+ "ibexa/integrated-help": "~5.0.x-dev",
+ "ibexa/site-context": "~5.0.x-dev",
+ "ibexa/fieldtype-richtext-rte": "~5.0.x-dev",
+ "ibexa/site-factory": "~5.0.x-dev",
+ "ibexa/ckeditor-premium": "~5.0.x-dev",
+ "ibexa/measurement": "~5.0.x-dev",
+ "ibexa/connector-actito": "~5.0.x-dev",
+ "ibexa/fastly": "~5.0.x-dev"
},
"scripts": {
"fix-cs": "php-cs-fixer fix --config=.php-cs-fixer.php -v --show-progress=dots",
"check-cs": "@fix-cs --dry-run",
"phpstan": "phpstan analyse",
"deptrac": "deptrac analyse",
- "check-rector": "rector process --dry-run --ansi"
+ "check-rector": "rector process --dry-run --ansi",
+ "check-yaml": "phpunit --group yaml",
+ "phpunit": "phpunit --exclude-group=yaml",
+ "yaml-update-baseline": "php tests/generate-yaml-baseline.php"
},
"scripts-descriptions": {
"fix-cs": "Automatically fixes code style in all files",
"check-cs": "Run code style checker for all files",
"phpstan": "Run static code analysis",
"deptrac": "Run Deptrac architecture testing",
- "check-rector": "Check for code refactoring opportunities"
+ "check-rector": "Check for code refactoring opportunities",
+ "check-yaml": "Run PHPUnit tests (YAML validation)",
+ "yaml-update-baseline": "Regenerate tests/yaml-validation-baseline.yaml from current failures"
},
"config": {
"allow-plugins": false
diff --git a/docs/administration/back_office/customize_search_suggestion.md b/docs/administration/back_office/customize_search_suggestion.md
index 158b3384c2..2764115e25 100644
--- a/docs/administration/back_office/customize_search_suggestion.md
+++ b/docs/administration/back_office/customize_search_suggestion.md
@@ -17,8 +17,9 @@ ibexa:
system:
:
search:
- min_query_length: 3
- result_limit: 5
+ suggestion:
+ min_query_length: 3
+ result_limit: 5
```
## Add custom suggestion source
diff --git a/docs/administration/configuration/dynamic_configuration.md b/docs/administration/configuration/dynamic_configuration.md
index 09f9cfbf42..c847e0899d 100644
--- a/docs/administration/configuration/dynamic_configuration.md
+++ b/docs/administration/configuration/dynamic_configuration.md
@@ -18,7 +18,7 @@ parameters:
# Internal configuration
ibexa.site_access.config.default.content.default_ttl: 60
ibexa.site_access.config.site_group.content.default_ttl: 3600
-
+
# Here "myapp" is the namespace, followed by the SiteAccess name as the parameter scope
# Parameter "my_param" will have a different value in site_group and admin_group
myapp.site_group.my_param: value
diff --git a/docs/api/graphql/graphql_customization.md b/docs/api/graphql/graphql_customization.md
index 5ebf97b4f0..be6421d049 100644
--- a/docs/api/graphql/graphql_customization.md
+++ b/docs/api/graphql/graphql_customization.md
@@ -68,9 +68,9 @@ Mutation:
createSomething:
builder: Mutation
builderConfig:
- inputType: CreateSomethingInput
- payloadType: SomethingPayload
- mutateAndGetPayload: '@=mutation('CreateSomething', [value])'
+ inputType: CreateSomethingInput
+ payloadType: SomethingPayload
+ mutateAndGetPayload: "@=mutation('CreateSomething', [value])"
CreateSomethingInput:
type: relay-mutation-input
diff --git a/docs/api/rest_api/rest_api_authentication.md b/docs/api/rest_api/rest_api_authentication.md
index f7f77366f8..82b504a010 100644
--- a/docs/api/rest_api/rest_api_authentication.md
+++ b/docs/api/rest_api/rest_api_authentication.md
@@ -325,10 +325,15 @@ For more information, see [HTTP Authentication: Basic and Digest Access Authenti
If the installation has a dedicated host for REST, you can enable HTTP basic authentication only on this host by setting a firewall like in the following example before the `ibexa_front` one:
```yaml
+security:
+ firewalls:
+ # ...
ibexa_rest:
host: ^api\.example\.com$
http_basic:
realm: Ibexa DXP REST API
+ #ibexa_front:
+ # ...
```
!!! caution "Back office uses REST API"
diff --git a/docs/cdp/cdp_installation.md b/docs/cdp/cdp_installation.md
index 73ba9dd5ae..f84719a45e 100644
--- a/docs/cdp/cdp_installation.md
+++ b/docs/cdp/cdp_installation.md
@@ -27,11 +27,14 @@ Symfony Flex installs and activates the package.
After an installation process is finished, go to `config/packages/security.yaml` and uncomment `ibexa_cdp` rule.
```yaml
-ibexa_cdp:
- pattern: /cdp/webhook
- guard:
- authenticator: 'Ibexa\Cdp\Security\CdpRequestAuthenticator'
- stateless: true
+security:
+ firewalls:
+ # ...
+ ibexa_cdp:
+ request_matcher: Ibexa\Cdp\Security\RequestMatcher
+ custom_authenticators:
+ - 'Ibexa\Cdp\Security\CdpRequestAuthenticator'
+ stateless: true
```
Now, you can configure [[= product_name_cdp =]].
diff --git a/docs/commerce/checkout/reorder.md b/docs/commerce/checkout/reorder.md
index 4d022782e4..96f83c6bed 100644
--- a/docs/commerce/checkout/reorder.md
+++ b/docs/commerce/checkout/reorder.md
@@ -54,11 +54,11 @@ framework:
places:
!php/const Ibexa\OrderManagement\Value\Status::COMPLETED_PLACE:
metadata:
- ...
+ # ...
can_be_reordered: true
!php/const Ibexa\OrderManagement\Value\Status::CANCELLED_PLACE:
metadata:
- ...
+ # ...
can_be_reordered: true
```
diff --git a/docs/commerce/payment/enable_paypal_payments.md b/docs/commerce/payment/enable_paypal_payments.md
index 3bccfdcdc5..2e4b890c25 100644
--- a/docs/commerce/payment/enable_paypal_payments.md
+++ b/docs/commerce/payment/enable_paypal_payments.md
@@ -42,5 +42,4 @@ ibexa:
type:
pp_express_checkout:
name: "Translated PayPal Express Checkout name"
-
```
diff --git a/docs/commerce/payment/enable_stripe_payments.md b/docs/commerce/payment/enable_stripe_payments.md
index 105ed62402..5d03bd8829 100644
--- a/docs/commerce/payment/enable_stripe_payments.md
+++ b/docs/commerce/payment/enable_stripe_payments.md
@@ -43,5 +43,4 @@ ibexa:
type:
strp_checkout:
name: "Translated Stripe Checkout name"
-
```
diff --git a/docs/commerce/payment/payum_integration.md b/docs/commerce/payment/payum_integration.md
index 7da7da20b1..408515f257 100644
--- a/docs/commerce/payment/payum_integration.md
+++ b/docs/commerce/payment/payum_integration.md
@@ -43,7 +43,7 @@ ibexa_connector_payum:
refunded: cancelled
captured: pending
authorized: authorized
-[...]
+# ...
```
## Payment service name translations
diff --git a/docs/commerce/storefront/configure_storefront.md b/docs/commerce/storefront/configure_storefront.md
index bdc96c02c8..5d69a73251 100644
--- a/docs/commerce/storefront/configure_storefront.md
+++ b/docs/commerce/storefront/configure_storefront.md
@@ -94,9 +94,10 @@ Settings for a Storefront user are configured under the `ibexa.system..st
ibexa:
system:
site_group:
- user_settings_groups:
- - location
- - custom_group
+ storefront:
+ user_settings_groups:
+ - location
+ - custom_group
```
By default, only the `location` user settings is provided:
diff --git a/docs/content_management/collaborative_editing/configure_collaborative_editing.md b/docs/content_management/collaborative_editing/configure_collaborative_editing.md
index 482040f3e1..268f774ff4 100644
--- a/docs/content_management/collaborative_editing/configure_collaborative_editing.md
+++ b/docs/content_management/collaborative_editing/configure_collaborative_editing.md
@@ -57,14 +57,15 @@ security:
```yaml
security:
# ...
- ibexa_shareable_link:
- request_matcher: Ibexa\Collaboration\Security\RequestMatcher\ShareableLinkRequestMatcher
- pattern: ^/
- provider: shared
- stateless: true
- user_checker: Ibexa\Core\MVC\Symfony\Security\UserChecker
- custom_authenticators:
- - Ibexa\Collaboration\Security\Authenticator\ShareableLinkAuthenticator
+ firewalls:
+ ibexa_shareable_link:
+ request_matcher: Ibexa\Collaboration\Security\RequestMatcher\ShareableLinkRequestMatcher
+ pattern: ^/
+ provider: shared
+ stateless: true
+ user_checker: Ibexa\Core\MVC\Symfony\Security\UserChecker
+ custom_authenticators:
+ - Ibexa\Collaboration\Security\Authenticator\ShareableLinkAuthenticator
```
### Configuration
diff --git a/docs/content_management/data_migration/managing_migrations.md b/docs/content_management/data_migration/managing_migrations.md
index 072a7a0e6d..c96e078208 100644
--- a/docs/content_management/data_migration/managing_migrations.md
+++ b/docs/content_management/data_migration/managing_migrations.md
@@ -50,7 +50,7 @@ You can configure a different folder by using the following settings:
``` yaml
ibexa_migrations:
- migration_directory: %kernel.project_dir%/src/Migrations/MyMigrations/
+ migration_directory: '%kernel.project_dir%/src/Migrations/MyMigrations/'
migrations_files_subdir: migration_files
```
@@ -64,7 +64,6 @@ ibexa_migrations:
``` yaml
ibexa_migrations:
migration_directory: '%kernel.project_dir%/data/'
- ...
```
Then, when you run the migration command, you must use the [`--siteaccess` option](exporting_data.md#siteaccess) and provide the name of the SiteAccess that you want to migrate.
diff --git a/docs/content_management/field_types/field_type_storage.md b/docs/content_management/field_types/field_type_storage.md
index 34999a91ce..12af0bc85b 100644
--- a/docs/content_management/field_types/field_type_storage.md
+++ b/docs/content_management/field_types/field_type_storage.md
@@ -147,7 +147,7 @@ services:
autoconfigure: true
public: false
- App\FieldType\MyField\Storage\MyFieldStorage: ~
+ App\FieldType\MyField\Storage\MyFieldStorage:
tags:
- {name: ibexa.field_type.storage.external.handler, alias: myfield}
```
diff --git a/docs/content_management/field_types/form_and_template.md b/docs/content_management/field_types/form_and_template.md
index fed81cc459..b5f0eba859 100644
--- a/docs/content_management/field_types/form_and_template.md
+++ b/docs/content_management/field_types/form_and_template.md
@@ -190,7 +190,7 @@ If you don't use the design engine, apply the following configuration:
``` yaml
ibexa:
- systems:
+ system:
admin_group:
field_templates:
- { template: 'adminui/field/custom_field_view.html.twig', priority: 10 }
diff --git a/docs/content_management/images/add_image_asset_from_dam.md b/docs/content_management/images/add_image_asset_from_dam.md
index 8c7c236fd3..52b91bbc7c 100644
--- a/docs/content_management/images/add_image_asset_from_dam.md
+++ b/docs/content_management/images/add_image_asset_from_dam.md
@@ -49,15 +49,15 @@ Next, in `config/packages/ibexa.yaml`, set the `dam.html.twig` template for the
For more information about displaying content, see [Content rendering](render_content.md).
``` yaml
- ibexa:
- system:
- site:
- content_view:
- embed:
- image_dam:
- template: '@ibexadesign/embed/dam.html.twig'
- match:
- Identifier\ContentType:
+ibexa:
+ system:
+ site:
+ content_view:
+ embed:
+ image_dam:
+ template: '@ibexadesign/embed/dam.html.twig'
+ match:
+ Identifier\ContentType:
```
In your [configuration file](configuration.md#configuration-files) add the following configuration:
diff --git a/docs/content_management/taxonomy/taxonomy.md b/docs/content_management/taxonomy/taxonomy.md
index f01ddb75df..118b787a18 100644
--- a/docs/content_management/taxonomy/taxonomy.md
+++ b/docs/content_management/taxonomy/taxonomy.md
@@ -191,10 +191,9 @@ By default, the system returns three suggestions.
You can change the default number if needed by altering the following setting:
``` yaml hl_lines="4"
-ibexa:
- taxonomy:
+ibexa_taxonomy:
text_to_taxonomy:
- default_suggested_taxonomies_limit: 5
+ default_suggested_taxonomies_limit: 5
```
You can also override this setting per AI action by editing its configuration.
diff --git a/docs/content_management/url_management/url_management.md b/docs/content_management/url_management/url_management.md
index 20af85356b..ed56dadc75 100644
--- a/docs/content_management/url_management/url_management.md
+++ b/docs/content_management/url_management/url_management.md
@@ -67,13 +67,13 @@ ibexa:
url_checker:
handlers:
http:
- enabled: true
- batch_size: 64
+ enabled: true
+ batch_size: 64
https:
- enabled: true
- ignore_certificate: false
+ enabled: true
+ ignore_certificate: false
mailto:
- enabled: false
+ enabled: false
```
Available options are protocol-specific.
@@ -128,7 +128,6 @@ Then you must register the service with an `ibexa.url_checker.handler` tag, like
```yaml
app.url_checker.handler.custom:
class: 'App\URLChecker\Handler\CustomHandler'
- ...
tags:
- { name: ibexa.url_checker.handler, scheme: custom }
```
diff --git a/docs/content_management/user_generated_content.md b/docs/content_management/user_generated_content.md
index 4a12ecc3bf..a35612334e 100644
--- a/docs/content_management/user_generated_content.md
+++ b/docs/content_management/user_generated_content.md
@@ -56,14 +56,17 @@ For example, `/content/edit/draft/1/5/eng-GB` enables you to edit draft 5 of con
You can use custom templates for the content editing forms.
-Define the templates under the `ibexa.system..content_edit.templates` [configuration key](configuration.md#configuration-files):
+Define the templates under the `ibexa.system..content_edit_view` [configuration key](configuration.md#configuration-files):
``` yaml
ibexa:
system:
default:
- content_edit:
- templates:
- edit: content/edit/content_edit.html.twig
- create_draft: content/edit/content_create_draft.html.twig
+ content_edit_view:
+ full:
+ :
+ template: content/edit/content_edit.html.twig
+ match: true
+ params:
+ viewbaseLayout: '@ibexadesign/ui/layout.html.twig'
```
diff --git a/docs/customer_management/cp_page_builder.md b/docs/customer_management/cp_page_builder.md
index 69e3bdd6b6..f81510d345 100644
--- a/docs/customer_management/cp_page_builder.md
+++ b/docs/customer_management/cp_page_builder.md
@@ -51,7 +51,7 @@ ibexa:
languages: [ eng-GB ]
content:
tree_root:
- location_id: location_id_of_customer_portal
+ location_id: 12345 # location_id_of_customer_portal
excluded_uri_prefixes: [ /media/, /images/ ]
```
@@ -135,7 +135,7 @@ ibexa:
languages: [ eng-GB ]
content:
tree_root:
- location_id: location_id_of_customer_portals_root_folder
+ location_id: 12345 # location_id_of_customer_portals_root_folder
excluded_uri_prefixes: [ /media/, /images/ ]
```
@@ -233,7 +233,7 @@ ibexa:
page_layout: "@App/my_page_layout.html.twig"
content:
tree_root:
- location_id: location_id_of_customer_portals_root_folder
+ location_id: 12345 #location_id_of_customer_portals_root_folder
excluded_uri_prefixes: [ /media/, /images/ ]
```
diff --git a/docs/infrastructure_and_maintenance/cache/http_cache/content_aware_cache.md b/docs/infrastructure_and_maintenance/cache/http_cache/content_aware_cache.md
index ab84406005..6e93ca73d3 100644
--- a/docs/infrastructure_and_maintenance/cache/http_cache/content_aware_cache.md
+++ b/docs/infrastructure_and_maintenance/cache/http_cache/content_aware_cache.md
@@ -295,7 +295,7 @@ With the same content structure as above, the `[Child]` location is moved below
The new structure is then:
-```yaml
+```text
- [Home] (content-id=52, location-id=2)
ez-all c52 ct42 l2 pl1 p1 p2
|
diff --git a/docs/infrastructure_and_maintenance/cache/http_cache/reverse_proxy.md b/docs/infrastructure_and_maintenance/cache/http_cache/reverse_proxy.md
index 8a743eb240..3cb48b9002 100644
--- a/docs/infrastructure_and_maintenance/cache/http_cache/reverse_proxy.md
+++ b/docs/infrastructure_and_maintenance/cache/http_cache/reverse_proxy.md
@@ -142,6 +142,9 @@ ibexa:
If the Varnish server is protected by Basic Auth, specify the Basic Auth credentials within the `purge_servers` setting using the format:
``` yaml
+ibexa:
+ system:
+ my_siteaccess_group:
http_cache:
purge_servers: [http://myuser:mypasswd@my.varnish.server:8081]
```
diff --git a/docs/multisite/site_factory/site_factory.md b/docs/multisite/site_factory/site_factory.md
index 74bed2fefb..3afd0628e9 100644
--- a/docs/multisite/site_factory/site_factory.md
+++ b/docs/multisite/site_factory/site_factory.md
@@ -204,7 +204,7 @@ Keep in mind that with disabled Site Factory you're unable to add new sites or u
doctrine:
dbal:
connections:
- ...
+ # ...
# This connection is dedicated for SiteFactory to avoid known issues
site_factory:
```
@@ -214,7 +214,7 @@ doctrine:
``` yaml
framework:
cache:
- ...
+ # ...
pools:
# This pool should be used only by SiteFactory bundle
site_factory_pool:
diff --git a/docs/recommendations/raptor_integration/connector_installation_configuration.md b/docs/recommendations/raptor_integration/connector_installation_configuration.md
index dfc2645400..3e133a440f 100644
--- a/docs/recommendations/raptor_integration/connector_installation_configuration.md
+++ b/docs/recommendations/raptor_integration/connector_installation_configuration.md
@@ -35,7 +35,7 @@ To configure the Raptor connector, use the `ibexa.system..connector_rapto
- `client` - tracking is executed in the browser using JavaScript snippets generated by the [Twig functions](recommendations_twig_functions.md) and included in the templates. This approach may be blocked by ad blockers.
- `server` - tracking is handled on the backend, with events sent directly to the tracking API. It's not affected by ad blockers.
- `recommendations_api_key` - an API key used to authenticate requests to the Recommendations API. This key allows the connector to retrieve personalized recommendations from the recommendation engine. You can find this value as ["API key"](connector_installation_configuration.md#recommendations-api-key) in Raptor Control Panel.
-- `recommendations_api_url` (optional) - overrides the default Raptor address, do not set it unless a custom endpoint is required.
+- `recommendations_api_uri` (optional) - overrides the default Raptor address, do not set it unless a custom endpoint is required.
By default, `tracking_type` is set to `client` as client-side tracking is the standard Raptor mode.
To understand the differences between client and server tracking types, including their advantages and disadvantages, refer to the [Raptor documentation](https://content.raptorservices.com/help-center/client-side-vs.-server-side-tracking).
diff --git a/docs/release_notes/ez_platform_v2.4.md b/docs/release_notes/ez_platform_v2.4.md
index 90562b8ada..16d92cbdb2 100644
--- a/docs/release_notes/ez_platform_v2.4.md
+++ b/docs/release_notes/ez_platform_v2.4.md
@@ -197,18 +197,18 @@ The biggest benefit of this feature is saving load time on complex landing pages
2\. Add the following configuration to `/app/config/config.yml`
``` yaml
- lexik_jwt_authentication:
- secret_key: '%secret%'
- encoder:
+ lexik_jwt_authentication:
+ secret_key: '%secret%'
+ encoder:
signature_algorithm: HS256
# Disabled by default, because Page Builder uses custom extractor
- token_extractors:
- authorization_header:
- enabled: false
- cookie:
- enabled: false
- query_parameter:
- enabled: false
+ token_extractors:
+ authorization_header:
+ enabled: false
+ cookie:
+ enabled: false
+ query_parameter:
+ enabled: false
```
By default `HS256` is used as signature algorithm for generated token but we strongly recommend switching to SSH keys.
@@ -218,23 +218,23 @@ The biggest benefit of this feature is saving load time on complex landing pages
3\. Add `EzSystems\EzPlatformPageBuilder\Security\EditorialMode\TokenAuthenticator` authentication provider to `ezpublish_front` firewall before `form_login` in `app/config/security.yml`:
``` yaml
- security:
+ security:
# ...
- firewalls:
- ezpublish_front:
- # ...
- simple_preauth:
- authenticator: 'EzSystems\EzPlatformPageBuilder\Security\EditorialMode\TokenAuthenticator'
- form_login:
- require_previous_session: false
- # ...
+ firewalls:
+ ezpublish_front:
+ # ...
+ simple_preauth:
+ authenticator: 'EzSystems\EzPlatformPageBuilder\Security\EditorialMode\TokenAuthenticator'
+ form_login:
+ require_previous_session: false
+ # ...
```
4\. Make sure that parameter `page_builder.token_authenticator.enabled` has value `true`. If the parameter isn't present, add it to `/app/config/config.yml`:
``` yaml
- # ...
- parameters:
+ # ...
+ parameters:
# ...
page_builder.token_authenticator.enabled: true
```
diff --git a/docs/search/search_engines/elasticsearch/configure_elasticsearch.md b/docs/search/search_engines/elasticsearch/configure_elasticsearch.md
index 3e05c45665..38c9cf33bb 100644
--- a/docs/search/search_engines/elasticsearch/configure_elasticsearch.md
+++ b/docs/search/search_engines/elasticsearch/configure_elasticsearch.md
@@ -182,7 +182,7 @@ If your Elasticsearch server is protected by HTTP authentication, you must provi
In the basic authentication, you must pass the following parameters:
``` yaml
-
+:
# ...
authentication:
type: basic
@@ -377,14 +377,14 @@ Index names use the following pattern:
You can create index templates with settings that apply to a specific language only, for example, to eliminate stop words from the index, or help divide concatenations.
You use patterns to identify index templates that contain settings specific for a given language:
- ``` yaml
- ibexa_elasticsearch:
+``` yaml
+ibexa_elasticsearch:
# ...
index_templates:
default_en_us:
patterns: ['default_*', '*eng_us*']
- # ...
- ```
+ # ...
+```
- `settings` - Settings under this key control all aspects related to an index.
@@ -392,21 +392,21 @@ For more information and a list of available settings, see [Elasticsearch docume
For example, you can define settings that convert text into a format that is optimized for search, like a normalizer that changes a case of all phrases in the index:
- ``` yaml
- ibexa_elasticsearch:
- # ...
- index_templates:
- default:
- # ...
- settings:
- analysis:
- normalizer:
- lowercase_normalizer:
- type: custom
- char_filter: []
- filter: lowercase
- # ...
- ```
+``` yaml
+ibexa_elasticsearch:
+ # ...
+ index_templates:
+ default:
+ # ...
+ settings:
+ analysis:
+ normalizer:
+ lowercase_normalizer:
+ type: custom
+ char_filter: []
+ filter: lowercase
+ # ...
+```
- `mappings` - Settings under this key define mapping for fields in the index.
diff --git a/docs/search/search_engines/solr_search_engine/install_solr.md b/docs/search/search_engines/solr_search_engine/install_solr.md
index edf2a76c0c..b7de523435 100644
--- a/docs/search/search_engines/solr_search_engine/install_solr.md
+++ b/docs/search/search_engines/solr_search_engine/install_solr.md
@@ -160,9 +160,9 @@ The Solr Search Engine Bundle can be configured in many ways.
The config further below assumes you have parameters set up for Solr DSN and search engine *(however both are optional)*, for example:
``` yaml
- env(SEARCH_ENGINE): solr
- env(SOLR_DSN): 'http://localhost:8983/solr'
- env(SOLR_CORE): collection1
+env(SEARCH_ENGINE): solr
+env(SOLR_DSN): 'http://localhost:8983/solr'
+env(SOLR_CORE): collection1
```
### Configure Solr version
diff --git a/docs/templating/image_variations.md b/docs/templating/image_variations.md
index cdcb8d9046..e4718dadfb 100644
--- a/docs/templating/image_variations.md
+++ b/docs/templating/image_variations.md
@@ -26,7 +26,9 @@ ibexa:
:
reference: null
filters:
- :
+ filter_name:
+ - parameter1
+ - parameter2
```
Variation name must be unique.
diff --git a/docs/templating/templates/view_matcher_reference.md b/docs/templating/templates/view_matcher_reference.md
index 687569c488..4e706fbeb1 100644
--- a/docs/templating/templates/view_matcher_reference.md
+++ b/docs/templating/templates/view_matcher_reference.md
@@ -235,7 +235,7 @@ match:
``` yaml
match:
- '@Ibexa\Taxonomy\View\Matcher\TaxonomyEntryBased\Id': [1, 2, 3]'
+ '@Ibexa\Taxonomy\View\Matcher\TaxonomyEntryBased\Id': [1, 2, 3]
```
## Taxonomy entry identifier
@@ -264,4 +264,4 @@ match:
``` yaml
match:
'@Ibexa\Taxonomy\View\Matcher\TaxonomyEntryBased\Taxonomy': 'product_category'
-```
\ No newline at end of file
+```
diff --git a/docs/users/invitations.md b/docs/users/invitations.md
index 446e852902..ddcebdd860 100644
--- a/docs/users/invitations.md
+++ b/docs/users/invitations.md
@@ -30,13 +30,13 @@ If the SiteAccess isn't set, it falls back to the default `site` value.
For example, use the following [configuration](configuration.md#configuration-files):
```yaml
- ibexa:
- system:
- :
- user_invitation:
- hash_expiration_time: P7D
- templates:
- mail: "@@App/invitation/mail.html.twig"
+ibexa:
+ system:
+ :
+ user_invitation:
+ hash_expiration_time: P7D
+ templates:
+ mail: "@@App/invitation/mail.html.twig"
```
Here, you can specify which template should be used for the invitation mail, and what should be the expiration time for the invitation link included in that mail.
diff --git a/docs/users/oauth_server.md b/docs/users/oauth_server.md
index 45ac506920..bc12a9889f 100644
--- a/docs/users/oauth_server.md
+++ b/docs/users/oauth_server.md
@@ -89,7 +89,7 @@ In `config/packages/security.yaml`, uncomment the three following lines under th
```yaml
security:
#…
- firewall:
+ firewalls:
#…
# Uncomment oauth2_token firewall if you wish to use product as an OAuth2 Server.
diff --git a/docs/users/user_authentication.md b/docs/users/user_authentication.md
index d2b7280301..9ec9e79c5d 100644
--- a/docs/users/user_authentication.md
+++ b/docs/users/user_authentication.md
@@ -66,7 +66,7 @@ services:
App\EventListener\InteractiveLoginListener:
arguments: ['@ibexa.api.service.user']
tags:
- - { name: kernel.event_subscriber }
+ - { name: kernel.event_subscriber }
```
Don't mix `MVCEvents::INTERACTIVE_LOGIN` event (specific to [[= product_name =]]) and `SecurityEvents::INTERACTIVE_LOGIN` event (fired by Symfony security component).
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
new file mode 100644
index 0000000000..e084420e57
--- /dev/null
+++ b/phpunit.xml.dist
@@ -0,0 +1,14 @@
+
+
+
+
+ tests/
+
+
+
diff --git a/tests/ConfigurationProvider.php b/tests/ConfigurationProvider.php
new file mode 100644
index 0000000000..4dfbe49523
--- /dev/null
+++ b/tests/ConfigurationProvider.php
@@ -0,0 +1,178 @@
+container = $this->buildContainer();
+ }
+
+ public function hasExtension(string $alias): bool
+ {
+ return $this->container->hasExtension($alias);
+ }
+
+ public function createConfiguration(string $alias): ConfigurationInterface
+ {
+ return $this->container->getExtension($alias)->getConfiguration([], $this->container);
+ }
+
+ /**
+ * Recursively resolves %parameter% placeholders using the container's
+ * parameter bag, mirroring what the real Symfony kernel does before
+ * passing config to the Config component. Unknown parameters (custom app
+ * params not present in the test container) are left as-is.
+ *
+ * @param array $config
+ *
+ * @return array
+ */
+ public function resolveParameters(array $config): array
+ {
+ /** @var array $result */
+ $result = $this->resolveValue($this->container->getParameterBag(), $config);
+
+ return $result;
+ }
+
+ private function resolveValue(ParameterBagInterface $bag, mixed $value): mixed
+ {
+ if (is_array($value)) {
+ return array_map(fn (mixed $v): mixed => $this->resolveValue($bag, $v), $value);
+ }
+
+ if (!is_string($value)) {
+ return $value;
+ }
+
+ try {
+ return $bag->resolveValue($value);
+ } catch (ParameterNotFoundException) {
+ return $value;
+ }
+ }
+
+ private function buildContainer(): ContainerBuilder
+ {
+ $container = new ContainerBuilder();
+ $container->setParameter('kernel.debug', false);
+ $container->setParameter('kernel.bundles', []);
+ $container->setParameter('kernel.bundles_metadata', []);
+ $container->setParameter('kernel.project_dir', sys_get_temp_dir());
+ $container->setParameter('kernel.environment', 'test');
+
+ $bundles = self::discoverBundles();
+
+ // Register all extensions before calling build() on any bundle,
+ // because some bundles call $container->getExtension('ibexa') during build().
+ foreach ($bundles as $bundle) {
+ try {
+ $extension = $bundle->getContainerExtension();
+ if ($extension !== null) {
+ $container->registerExtension($extension);
+ }
+ } catch (\Throwable) {
+ // Skip bundles whose extension cannot be instantiated.
+ }
+ }
+
+ // build() registers parsers/factories into the extensions.
+ foreach ($bundles as $bundle) {
+ try {
+ $bundle->build($container);
+ } catch (\Throwable) {
+ // Skip bundles whose build() fails (e.g. missing sibling extensions).
+ }
+ }
+
+ return $container;
+ }
+
+ /**
+ * Returns all installed bundles with SecurityBundle and IbexaCoreBundle
+ * guaranteed first (other bundles may call getExtension('ibexa') or
+ * getExtension('security') during their build()).
+ *
+ * @return list
+ */
+ private static function discoverBundles(): array
+ {
+ // These must be registered before any bundle that calls
+ // $container->getExtension('ibexa'/'security') inside build().
+ $bundles = [
+ new SecurityBundle(),
+ new IbexaCoreBundle(),
+ ];
+
+ $seen = [SecurityBundle::class, IbexaCoreBundle::class];
+
+ $vendorBase = __DIR__ . '/../vendor';
+ $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($vendorBase));
+
+ foreach ($iterator as $file) {
+ if (!$file->isFile() || !preg_match('/\w+Bundle\.php$/', $file->getFilename())) {
+ continue;
+ }
+
+ $content = file_get_contents($file->getPathname());
+ preg_match('/^namespace (.+);/m', $content, $nsMatch);
+ preg_match('/^(?:(?:final|abstract)\s+)?class (\w+Bundle)\b/m', $content, $clsMatch);
+
+ if (empty($nsMatch[1]) || empty($clsMatch[1])) {
+ continue;
+ }
+
+ $fqcn = $nsMatch[1] . '\\' . $clsMatch[1];
+
+ if (!class_exists($fqcn) || in_array($fqcn, $seen, true)) {
+ continue;
+ }
+
+ $reflection = new \ReflectionClass($fqcn);
+ if ($reflection->isAbstract() || !$reflection->implementsInterface(BundleInterface::class)) {
+ continue;
+ }
+
+ $seen[] = $fqcn;
+
+ try {
+ $bundles[] = new $fqcn();
+ } catch (\Throwable) {
+ // Skip bundles that cannot be instantiated without arguments.
+ }
+ }
+
+ return $bundles;
+ }
+}
diff --git a/tests/Markdown/MarkdownYamlExtractor.php b/tests/Markdown/MarkdownYamlExtractor.php
new file mode 100644
index 0000000000..699c59853e
--- /dev/null
+++ b/tests/Markdown/MarkdownYamlExtractor.php
@@ -0,0 +1,78 @@
+ *)```\s*yaml[^\n]*\n(?P.*?)\n(?P=indent)```/ms';
+
+ private const string SKIP_PATTERN = '/include_(?:file|code)\s*\(|--8<--/';
+
+ /**
+ * @return iterable
+ */
+ public function extract(string $content): iterable
+ {
+ if (!preg_match_all(self::FENCE_PATTERN, $content, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) {
+ return;
+ }
+
+ foreach ($matches as $match) {
+ $body = $match['body'][0];
+ $offset = $match['body'][1];
+
+ if (preg_match(self::SKIP_PATTERN, $body)) {
+ continue;
+ }
+
+ $indent = $match['indent'][0];
+ if ($indent !== '') {
+ $body = $this->stripIndentation($body, strlen($indent));
+ }
+
+ $line = substr_count(substr($content, 0, $offset), "\n") + 1;
+
+ yield ['body' => $body, 'line' => $line];
+ }
+ }
+
+ private function stripIndentation(string $body, int $spaces): string
+ {
+ $prefix = str_repeat(' ', $spaces);
+ $lines = explode("\n", $body);
+ $stripped = array_map(
+ static fn (string $line): string => str_starts_with($line, $prefix)
+ ? substr($line, $spaces)
+ : $line,
+ $lines
+ );
+
+ return implode("\n", $stripped);
+ }
+}
diff --git a/tests/Markdown/MarkdownYamlExtractorTest.php b/tests/Markdown/MarkdownYamlExtractorTest.php
new file mode 100644
index 0000000000..0575ae51df
--- /dev/null
+++ b/tests/Markdown/MarkdownYamlExtractorTest.php
@@ -0,0 +1,224 @@
+extractor = new MarkdownYamlExtractor();
+ }
+
+ public function testExtractsNothing(): void
+ {
+ self::assertEmpty(iterator_to_array($this->extractor->extract('No code blocks here.')));
+ self::assertEmpty(iterator_to_array($this->extractor->extract('')));
+ }
+
+ public function testIgnoresNonYamlFences(): void
+ {
+ $content = <<<'MD'
+ ```php
+ $x = 1;
+ ```
+
+ ```json
+ {"key": "value"}
+ ```
+ MD;
+
+ self::assertEmpty(iterator_to_array($this->extractor->extract($content)));
+ }
+
+ public function testExtractsSingleBlock(): void
+ {
+ $content = <<<'MD'
+ Some text.
+
+ ```yaml
+ foo: bar
+ ```
+
+ More text.
+ MD;
+
+ $blocks = iterator_to_array($this->extractor->extract($content));
+
+ self::assertCount(1, $blocks);
+ self::assertSame('foo: bar', $blocks[0]['body']);
+ }
+
+ public function testExtractsMultipleBlocks(): void
+ {
+ $content = <<<'MD'
+ ```yaml
+ first: 1
+ ```
+
+ ```yaml
+ second: 2
+ ```
+ MD;
+
+ $blocks = iterator_to_array($this->extractor->extract($content));
+
+ self::assertCount(2, $blocks);
+ self::assertSame('first: 1', $blocks[0]['body']);
+ self::assertSame('second: 2', $blocks[1]['body']);
+ }
+
+ public function testReportsCorrectLineNumber(): void
+ {
+ $content = "line1\nline2\nline3\n```yaml\nfoo: bar\n```\n";
+
+ $blocks = iterator_to_array($this->extractor->extract($content));
+
+ self::assertCount(1, $blocks);
+ // The body starts on line 5 (after 4 preceding newlines inside the fence open)
+ self::assertSame(5, $blocks[0]['line']);
+ }
+
+ public function testAcceptsSpaceBeforeLanguageTag(): void
+ {
+ $content = "``` yaml\nfoo: bar\n```\n";
+
+ $blocks = iterator_to_array($this->extractor->extract($content));
+
+ self::assertCount(1, $blocks);
+ self::assertSame('foo: bar', $blocks[0]['body']);
+ }
+
+ public function testAcceptsTrailingAnnotations(): void
+ {
+ $content = "```yaml hl_lines=\"1 2\"\nfoo: bar\n```\n";
+
+ $blocks = iterator_to_array($this->extractor->extract($content));
+
+ self::assertCount(1, $blocks);
+ self::assertSame('foo: bar', $blocks[0]['body']);
+ }
+
+ public function testStripsAdmonitionIndentation(): void
+ {
+ $content = <<<'MD'
+ !!! note
+
+ ```yaml
+ foo: bar
+ baz: qux
+ ```
+ MD;
+
+ $blocks = iterator_to_array($this->extractor->extract($content));
+
+ self::assertCount(1, $blocks);
+ self::assertSame("foo: bar\nbaz: qux", $blocks[0]['body']);
+ }
+
+ public function testSkipsBlocksWithIncludeFile(): void
+ {
+ $content = <<<'MD'
+ ```yaml
+ [[= include_file('some/file.yaml') =]]
+ ```
+ MD;
+
+ self::assertEmpty(iterator_to_array($this->extractor->extract($content)));
+ }
+
+ public function testSkipsBlocksWithIncludeCode(): void
+ {
+ $content = <<<'MD'
+ ```yaml
+ [[= include_code('some/file.yaml', 1, 10) =]]
+ ```
+ MD;
+
+ self::assertEmpty(iterator_to_array($this->extractor->extract($content)));
+ }
+
+ public function testSkipsBlocksWithSnippetMarker(): void
+ {
+ $content = <<<'MD'
+ ```yaml
+ --8<--
+ some/file.yaml
+ --8<--
+ ```
+ MD;
+
+ self::assertEmpty(iterator_to_array($this->extractor->extract($content)));
+ }
+
+ public function testSkipsOnlyMatchingBlocksWhenMixed(): void
+ {
+ $content = <<<'MD'
+ ```yaml
+ [[= include_file('foo.yaml') =]]
+ ```
+
+ ```yaml
+ [[= include_code('bar.yaml', 1, 5) =]]
+ ```
+
+ ```yaml
+ real: config
+ ```
+ MD;
+
+ $blocks = iterator_to_array($this->extractor->extract($content));
+
+ self::assertCount(1, $blocks);
+ self::assertSame('real: config', $blocks[0]['body']);
+ }
+
+ /**
+ * @param array $expected
+ */
+ #[DataProvider('provideMultilineBlocks')]
+ public function testExtractsMultilineBody(string $content, array $expected): void
+ {
+ $blocks = iterator_to_array($this->extractor->extract($content));
+
+ self::assertCount(count($expected), $blocks);
+ foreach ($expected as $i => $exp) {
+ self::assertSame($exp['body'], $blocks[$i]['body'], "body at index $i");
+ self::assertSame($exp['line'], $blocks[$i]['line'], "line at index $i");
+ }
+ }
+
+ /**
+ * @return iterable}>
+ */
+ public static function provideMultilineBlocks(): iterable
+ {
+ yield 'nested mapping' => [
+ "```yaml\nparent:\n child: value\n```\n",
+ [['body' => "parent:\n child: value", 'line' => 2]],
+ ];
+
+ yield 'sequence' => [
+ "```yaml\nlist:\n - a\n - b\n```\n",
+ [['body' => "list:\n - a\n - b", 'line' => 2]],
+ ];
+
+ yield 'two blocks with correct lines' => [
+ "```yaml\nfoo: 1\n```\n\nsome text\n\n```yaml\nbar: 2\n```\n",
+ [
+ ['body' => 'foo: 1', 'line' => 2],
+ ['body' => 'bar: 2', 'line' => 8],
+ ],
+ ];
+ }
+}
diff --git a/tests/ValidationBaseline.php b/tests/ValidationBaseline.php
new file mode 100644
index 0000000000..223669e8a1
--- /dev/null
+++ b/tests/ValidationBaseline.php
@@ -0,0 +1,75 @@
+|null */
+ private ?array $entries = null;
+
+ public function __construct(
+ private readonly string $baselineFile,
+ private readonly string $repoRoot,
+ ) {
+ }
+
+ public function isInBaseline(string $relativePath, ?string $bodyHash, string $errorMessage): bool
+ {
+ foreach ($this->getEntries() as $entry) {
+ $entryPath = $entry['path'] ?? '';
+
+ // Path: exact match or trailing-suffix match (allows glob-like partial paths)
+ if ($relativePath !== $entryPath && !str_ends_with($relativePath, ltrim($entryPath, '/'))) {
+ continue;
+ }
+
+ // Hash (optional): must match exactly when provided; line is ignored for matching
+ if (isset($entry['hash']) && $bodyHash !== null && $entry['hash'] !== $bodyHash) {
+ continue;
+ }
+
+ // Message (optional): treated as a regex pattern
+ if (isset($entry['message']) && !preg_match($entry['message'], $errorMessage)) {
+ continue;
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * @return list
+ */
+ private function getEntries(): array
+ {
+ if ($this->entries !== null) {
+ return $this->entries;
+ }
+
+ if (!file_exists($this->baselineFile)) {
+ return $this->entries = [];
+ }
+
+ $parsed = \Symfony\Component\Yaml\Yaml::parseFile($this->baselineFile);
+
+ return $this->entries = $parsed['ignoreErrors'] ?? [];
+ }
+}
diff --git a/tests/Yaml/CodeSample.php b/tests/Yaml/CodeSample.php
new file mode 100644
index 0000000000..3d582da9b7
--- /dev/null
+++ b/tests/Yaml/CodeSample.php
@@ -0,0 +1,16 @@
+bodyHash = hash('sha256', $body);
+ }
+}
diff --git a/tests/Yaml/YamlSamplesProvider.php b/tests/Yaml/YamlSamplesProvider.php
new file mode 100644
index 0000000000..5dbc63da45
--- /dev/null
+++ b/tests/Yaml/YamlSamplesProvider.php
@@ -0,0 +1,96 @@
+
+ */
+ public function getCodeSampleYaml(): iterable
+ {
+ yield from $this->iterateCodeSampleYaml();
+ yield from $this->iterateMarkdownYamlBlocks();
+ }
+
+ /**
+ * Yields every .yaml file found recursively under code_samples/.
+ *
+ * @return iterable
+ */
+ private function iterateCodeSampleYaml(): iterable
+ {
+ $iterator = new RecursiveIteratorIterator(
+ new RecursiveDirectoryIterator(self::CODE_SAMPLES_DIR, RecursiveDirectoryIterator::SKIP_DOTS)
+ );
+
+ /** @var \SplFileInfo $file */
+ foreach ($iterator as $file) {
+ if (!$file->isFile() || $file->getExtension() !== 'yaml') {
+ continue;
+ }
+
+ $body = file_get_contents($file->getRealPath());
+
+ if ($body === false) {
+ continue;
+ }
+
+ yield new CodeSample($file->getRealPath(), 0, $body);
+ }
+ }
+
+ /**
+ * Yields every fenced YAML block found in .md files under docs/.
+ *
+ * @return iterable
+ */
+ private function iterateMarkdownYamlBlocks(): iterable
+ {
+ $extractor = new MarkdownYamlExtractor();
+ $iterator = new RecursiveIteratorIterator(
+ new RecursiveDirectoryIterator(self::DOCS_DIR, RecursiveDirectoryIterator::SKIP_DOTS)
+ );
+
+ /** @var \SplFileInfo $file */
+ foreach ($iterator as $file) {
+ if (!$file->isFile() || $file->getExtension() !== 'md') {
+ continue;
+ }
+
+ $path = $file->getRealPath();
+ $content = file_get_contents($path);
+
+ if ($content === false) {
+ continue;
+ }
+
+ foreach ($extractor->extract($content) as $block) {
+ yield new CodeSample($path, $block['line'], $block['body']);
+ }
+ }
+ }
+}
diff --git a/tests/Yaml/YamlTest.php b/tests/Yaml/YamlTest.php
new file mode 100644
index 0000000000..54b781bca8
--- /dev/null
+++ b/tests/Yaml/YamlTest.php
@@ -0,0 +1,170 @@
+isInBaseline($filePath, $bodyHash, $e->getMessage())) {
+ self::markTestSkipped(sprintf(
+ 'Known baseline issue in %s at line %d: %s',
+ $filePath,
+ $line,
+ $e->getMessage(),
+ ));
+ }
+
+ self::fail(sprintf(
+ 'YAML parse error in %s at line %d [hash:%s]: %s',
+ $filePath,
+ $line,
+ $bodyHash,
+ $e->getMessage(),
+ ));
+ }
+
+ $this->addToAssertionCount(1);
+ }
+
+ /**
+ * @param int $line Starting line of the config block (0 for standalone YAML files).
+ */
+ #[DataProvider('provideBundleConfigs')]
+ public function testBundleConfigurationIsValid(
+ string $extensionName,
+ mixed $config,
+ string $filePath,
+ int $line,
+ string $bodyHash
+ ): void {
+ $configuration = self::configurationProvider()->createConfiguration($extensionName);
+ $processor = new Processor();
+
+ $config = self::configurationProvider()->resolveParameters(is_array($config) ? $config : []);
+
+ try {
+ $processor->processConfiguration($configuration, [$config]);
+ } catch (\Exception $e) {
+ if (self::baseline()->isInBaseline($filePath, $bodyHash, $e->getMessage())) {
+ self::markTestSkipped(sprintf(
+ 'Known baseline issue for "%s" in %s:%d: %s',
+ $extensionName,
+ $filePath,
+ $line,
+ $e->getMessage(),
+ ));
+ }
+
+ self::fail(sprintf(
+ 'Invalid configuration for "%s" in %s:%d [hash:%s] — %s',
+ $extensionName,
+ $filePath,
+ $line,
+ $bodyHash,
+ $e->getMessage(),
+ ));
+ }
+
+ $this->addToAssertionCount(1);
+ }
+
+ /**
+ * Yields all standalone YAML files from code_samples/ plus every fenced
+ * YAML block extracted from docs Markdown files.
+ *
+ * @return iterable
+ */
+ public static function provideYamlSources(): iterable
+ {
+ foreach (self::samplesProvider()->getCodeSampleYaml() as $item) {
+ yield self::makeLabel($item->path, $item->line) => [$item->path, $item->line, $item->body, $item->bodyHash];
+ }
+ }
+
+ /**
+ * Yields one entry per (extension, config) pair found in YAML files and
+ * in fenced YAML blocks from docs Markdown files.
+ *
+ * @return iterable
+ */
+ public static function provideBundleConfigs(): iterable
+ {
+ foreach (self::provideYamlSources() as [$filePath, $line, $body, $bodyHash]) {
+ $path = self::relativePath($filePath);
+ try {
+ $parsed = Yaml::parse($body, Yaml::PARSE_CUSTOM_TAGS);
+ } catch (\Throwable) {
+ continue;
+ }
+
+ if (!is_array($parsed)) {
+ continue;
+ }
+
+ foreach ($parsed as $extensionName => $config) {
+ if (!is_string($extensionName) || !self::configurationProvider()->hasExtension($extensionName)) {
+ continue;
+ }
+
+ yield sprintf('%s (%s)', $extensionName, self::makeLabel($path, $line)) => [$extensionName, $config, $path, $line, $bodyHash];
+ }
+ }
+ }
+
+ private static function configurationProvider(): ConfigurationProvider
+ {
+ static $provider = null;
+
+ return $provider ??= new ConfigurationProvider();
+ }
+
+ private static function samplesProvider(): YamlSamplesProvider
+ {
+ static $provider = null;
+
+ return $provider ??= new YamlSamplesProvider();
+ }
+
+ private static function baseline(): ValidationBaseline
+ {
+ static $baseline = null;
+
+ return $baseline ??= new ValidationBaseline(self::BASELINE_FILE, realpath(self::REPO_ROOT));
+ }
+
+ private static function makeLabel(string $absolutePath, int $lineNumber): string
+ {
+ return ltrim(str_replace(realpath(self::REPO_ROOT), '', $absolutePath), '/') . ':' . $lineNumber;
+ }
+
+ private static function relativePath(string $absolutePath): string
+ {
+ return ltrim(str_replace(realpath(self::REPO_ROOT), '', $absolutePath), '/');
+ }
+}
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
new file mode 100644
index 0000000000..9374e6f3a2
--- /dev/null
+++ b/tests/bootstrap.php
@@ -0,0 +1,40 @@
+isDir() && $item->getFilename() === 'src') {
+ $loader->addPsr4('App\\', $item->getRealPath());
+ }
+}
diff --git a/tests/generate-yaml-baseline.php b/tests/generate-yaml-baseline.php
new file mode 100644
index 0000000000..4cee77e254
--- /dev/null
+++ b/tests/generate-yaml-baseline.php
@@ -0,0 +1,151 @@
+#!/usr/bin/env php
+/dev/null',
+ escapeshellarg($phpunitBin),
+ escapeshellarg($tmpLog),
+);
+
+passthru($cmd);
+
+if (!file_exists($tmpLog)) {
+ if ($previousBaseline !== null) {
+ file_put_contents($outFile, $previousBaseline);
+ }
+ fwrite(STDERR, "ERROR: PHPUnit did not produce a JUnit log file.\n");
+ exit(1);
+}
+
+$xml = simplexml_load_file($tmpLog);
+unlink($tmpLog);
+
+if ($xml === false) {
+ fwrite(STDERR, "ERROR: Could not parse JUnit XML.\n");
+ exit(1);
+}
+
+$repoRoot = dirname(__DIR__);
+$entries = [];
+
+// Collect all failures/errors at any nesting depth using XPath
+/** @var \SimpleXMLElement[] $testcases */
+$testcases = $xml->xpath('//testcase[failure or error]') ?: [];
+
+foreach ($testcases as $testcase) {
+ $failure = $testcase->failure ?? $testcase->error ?? null;
+ if ($failure === null) {
+ continue;
+ }
+
+ $message = (string) $failure;
+ // Extract file path and line from failure message
+ // Patterns:
+ // "Invalid configuration for "X" in path/to/file.yaml: error"
+ // "Invalid configuration for "X" in path/to/file.md:123 — error"
+ // "YAML parse error in path/to/file.md at line 123: error"
+ $path = null;
+ $line = null;
+ $bodyHash = null;
+ $errorMessage = null;
+
+ if (preg_match('/Invalid configuration for "[^"]*" in ([^\n:]+?):(\d+) \[hash:([a-f0-9]+)\] — (.+)/s', $message, $m)) {
+ $path = trim($m[1]);
+ $line = (int) $m[2];
+ $bodyHash = $m[3];
+ $errorMessage = trim(explode("\n", $m[4])[0]);
+ } elseif (preg_match('/Invalid configuration for "[^"]*" in ([^\n:]+?): (.+)/s', $message, $m)) {
+ $path = trim($m[1]);
+ $errorMessage = trim(explode("\n", $m[2])[0]);
+ } elseif (preg_match('/YAML parse error in ([^\n]+?) at line (\d+) \[hash:([a-f0-9]+)\]: (.+)/s', $message, $m)) {
+ $path = trim($m[1]);
+ $line = (int) $m[2];
+ $bodyHash = $m[3];
+ $errorMessage = trim(explode("\n", $m[4])[0]);
+ }
+
+ if ($path === null) {
+ continue;
+ }
+
+ // Convert absolute path to relative
+ if (str_starts_with($path, $repoRoot)) {
+ $path = ltrim(substr($path, strlen($repoRoot)), '/');
+ }
+
+ $entry = ['path' => $path];
+ if ($line !== null) {
+ $entry['line'] = $line;
+ }
+ if ($bodyHash !== null) {
+ $entry['hash'] = $bodyHash;
+ }
+ if ($errorMessage !== null) {
+ // Store as a regex: escape special chars, keep it readable
+ $entry['message'] = '~' . preg_quote($errorMessage, '~') . '~';
+ }
+
+ $key = $path . ':' . ($bodyHash ?? $line ?? '');
+ $entries[$key] = $entry;
+}
+
+ksort($entries);
+
+// Render as YAML manually (keep it readable without needing a YAML library)
+$lines = [];
+$lines[] = '# Auto-generated by `composer phpunit-update-baseline`. Do not edit manually.';
+$lines[] = '# To suppress a failure: regenerate this file after confirming it is expected.';
+$lines[] = '# To fix a suppressed failure: fix the doc error and regenerate.';
+$lines[] = 'ignoreErrors:';
+
+foreach ($entries as $entry) {
+ $lines[] = ' -';
+ $lines[] = sprintf(' path: %s', $entry['path']);
+ if (isset($entry['line'])) {
+ $lines[] = sprintf(' line: %d', $entry['line']);
+ }
+ if (isset($entry['hash'])) {
+ $lines[] = sprintf(' hash: %s', $entry['hash']);
+ }
+ if (isset($entry['message'])) {
+ // Wrap message in single quotes, escaping internal single quotes
+ $msg = str_replace("'", "''", $entry['message']);
+ $lines[] = sprintf(" message: '%s'", $msg);
+ }
+}
+
+$content = implode("\n", $lines) . "\n";
+$outFile = __DIR__ . '/yaml-validation-baseline.yaml';
+file_put_contents($outFile, $content);
+
+$count = count($entries);
+echo "Baseline written to tests/yaml-validation-baseline.yaml ({$count} entries)\n";
+if ($count === 0) {
+ echo "No failures found — baseline is empty. All tests pass!\n";
+}
diff --git a/tests/yaml-validation-baseline.yaml b/tests/yaml-validation-baseline.yaml
new file mode 100644
index 0000000000..ac021c0d51
--- /dev/null
+++ b/tests/yaml-validation-baseline.yaml
@@ -0,0 +1,394 @@
+# Auto-generated by `composer yaml-update-baseline`. Do not edit manually.
+# To suppress a failure: regenerate this file after confirming it is expected.
+# To fix a suppressed failure: fix the doc error and regenerate.
+ignoreErrors:
+ -
+ path: code_samples/forms/custom_form_attribute/config/custom_services.yaml
+ line: 0
+ hash: 9d00873c2b34dc269f7846d4f4462a2a9d0308566818ca845c802d0778d91469
+ message: '~Unable to parse at line 1 \(near " App\\FormBuilder\\FieldType\\Field\\Mapper\\CheckboxWithRichtextDescriptionFieldMapper\:"\)\.~'
+ -
+ path: code_samples/workflow/custom_workflow/config/packages/workflows.yaml
+ line: 0
+ hash: 48b8c1ee7a1f481c5c83d1d90ed43723cc8dd59fac24adc798f0492d7fac7a89
+ message: '~The child config "stages" under "ibexa\.system\.default\.workflows\.quick_review" must be configured\.~'
+ -
+ path: docs/administration/back_office/back_office_elements/extending_thumbnails.md
+ line: 109
+ hash: 65a44481f05186cf38bb2efd4583c49faf2b58dc4c197d40a611cd8610a77d97
+ message: '~Unable to parse at line 1 \(near " App\\Thumbnails\\FieldValueUrl\:"\)\.~'
+ -
+ path: docs/administration/back_office/configure_product_tour.md
+ line: 26
+ hash: 4cfc5ac559eb8db38edb6c8a29872883a95886b61cdae2b343a5ebaf4a9bca8e
+ message: '~The value "\" is not allowed for path "ibexa\.system\.\\>\.product_tour\.\\.type"\. Permissible values\: "general", "targetable"\.~'
+ -
+ path: docs/cdp/cdp_data_customization.md
+ line: 29
+ hash: bb2ecc0b96ca61cd004b9c1806e78d6afc3197a68acb723db7a52cfd5bdc89d6
+ message: '~Unable to parse at line 1 \(near " App\\Export\\User\\DateOfBirthUserItemProcessor\:"\)\.~'
+ -
+ path: docs/commerce/checkout/customize_checkout.md
+ line: 130
+ hash: fb889cba0ffc081e7d0b0387d32835ae8e5d3c033f26144cb4d54b3e121f428a
+ message: '~Invalid configuration for path "framework\.workflows\.workflows\.ibexa_checkout"\: "supports" or "support_strategy" should be configured\.~'
+ -
+ path: docs/commerce/order_management/configure_order_management.md
+ line: 51
+ hash: 30a04aef21c825d6b59c938c2b5bb4078abd7156526a7f9f1b7b13203ff56ed7
+ message: '~The child config "transitions" under "framework\.workflows\.workflows\.ibexa_order" must be configured\.~'
+ -
+ path: docs/commerce/payment/enable_paypal_payments.md
+ line: 40
+ hash: bc714d63a541c63d24e2a882cc8c67b685fcee24feb0d80a327f02a4c3b0bef8
+ message: '~Unrecognized option "payment_method" under "ibexa"\. Available options are "http_cache", "image_placeholder", "imagemagick", "locale_conversion", "orm", "repositories", "router", "siteaccess", "system", "ui", "url_alias", "url_wildcards"\.~'
+ -
+ path: docs/commerce/payment/enable_stripe_payments.md
+ line: 41
+ hash: 09750e5c042fe1d6abc20d4088674195d78ca70ce113f8869818a76de1a8023a
+ message: '~Unrecognized option "payment_method" under "ibexa"\. Available options are "http_cache", "image_placeholder", "imagemagick", "locale_conversion", "orm", "repositories", "router", "siteaccess", "system", "ui", "url_alias", "url_wildcards"\.~'
+ -
+ path: docs/commerce/payment/payum_integration.md
+ line: 55
+ hash: c3f14ff441a95545c360aaab308050a2629a81f07d76d4fb65133ab89a6ad30d
+ message: '~Unrecognized option "payment_method" under "ibexa"\. Available options are "http_cache", "image_placeholder", "imagemagick", "locale_conversion", "orm", "repositories", "router", "siteaccess", "system", "ui", "url_alias", "url_wildcards"\.~'
+ -
+ path: docs/commerce/transactional_emails/extend_transactional_emails.md
+ line: 17
+ hash: f7073192f3b22201fe8d86bb71c76b4026f14efbfe6bb28ddbed657d93eca29c
+ message: '~The child config "transitions" under "framework\.workflows\.workflows\.ibexa_payment" must be configured\.~'
+ -
+ path: docs/content_management/collaborative_editing/configure_collaborative_editing.md
+ line: 48
+ hash: 629d37dcc688c9b11daec8de8c601522830f3b2175ac6ac692db3f17b89a01d7
+ message: '~The child config "firewalls" under "security" must be configured\.~'
+ -
+ path: docs/content_management/collaborative_editing/configure_collaborative_editing.md
+ line: 78
+ hash: c6488fad354e039350fd0c18bdd0b6dc2a1ff7300dbae9151155490278b840a0
+ message: '~Invalid type for path "ibexa\.repositories\.\\.collaboration\.participants\.auto_invite"\. Expected "bool", but got "string"\.~'
+ -
+ path: docs/content_management/data_migration/data_migration_actions.md
+ line: 156
+ hash: 023288ce392e2e051ac32ea3525bdecebc301a860789c8d2128042885122bfa9
+ message: '~Unable to parse at line 1 \(near " actions\:"\)\.~'
+ -
+ path: docs/content_management/data_migration/data_migration_actions.md
+ line: 102
+ hash: 29c25d5ed7cc6b7c435b3d2b6f4e64946ea4f37a27b1e32330a205757a74e56e
+ message: '~Unable to parse at line 1 \(near " actions\:"\)\.~'
+ -
+ path: docs/content_management/data_migration/data_migration_actions.md
+ line: 136
+ hash: 379c053c8d40a701b0931b2e753d1256ece028d3c2e96a86fad3c884d480a48f
+ message: '~Unable to parse at line 1 \(near " actions\:"\)\.~'
+ -
+ path: docs/content_management/data_migration/data_migration_actions.md
+ line: 78
+ hash: 6ebbedc8bf843bdc28aa23e5604ad9795153e5abcf808839229b08a37386f7eb
+ message: '~Unable to parse at line 1 \(near " actions\:"\)\.~'
+ -
+ path: docs/content_management/data_migration/data_migration_actions.md
+ line: 178
+ hash: 93777839119c361b8b3a2708d47240128ef27643dce015decc1bf4edf0e8cb8f
+ message: '~Unable to parse at line 1 \(near " actions\:"\)\.~'
+ -
+ path: docs/content_management/data_migration/data_migration_actions.md
+ line: 114
+ hash: c730d746b624933585bd7f985fc773625750b8385a3755e7735d50e49e4964b2
+ message: '~Unable to parse at line 1 \(near " actions\:"\)\.~'
+ -
+ path: docs/content_management/data_migration/data_migration_actions.md
+ line: 86
+ hash: d330a24eee42024868c98fcffb2c446706570fea05d05ec3b7309297ba49e3b4
+ message: '~Unable to parse at line 1 \(near " actions\:"\)\.~'
+ -
+ path: docs/content_management/data_migration/data_migration_actions.md
+ line: 96
+ hash: e4d25af95785fe407cc135a608c778ff3232db0ce36e0bf5ce2039b249e29dc7
+ message: '~Unable to parse at line 1 \(near " actions\:"\)\.~'
+ -
+ path: docs/content_management/data_migration/importing_data.md
+ line: 200
+ hash: 5fff20465655d9e52a258e3c1b1347a62b0f558d9e733bb377c003d5021822eb
+ message: '~Unable to parse at line 1 \(near " \- fieldDefIdentifier\: show_children"\)\.~'
+ -
+ path: docs/content_management/data_migration/importing_data.md
+ line: 228
+ hash: 7cec0a6d723dcb0d3d87904910e4870f05b885764116e447b8a4fe19025b8289
+ message: '~Unable to parse at line 1 \(near " \- fieldDefIdentifier\: project_directory"\)\.~'
+ -
+ path: docs/content_management/data_migration/importing_data.md
+ line: 220
+ hash: a08babaebc91ceb742a9a81a04be4d9e975291019794a0658bef1f99e62dccea
+ message: '~Unable to parse at line 1 \(near " \- fieldDefIdentifier\: some_field"\)\.~'
+ -
+ path: docs/content_management/data_migration/importing_data.md
+ line: 313
+ hash: c94bdd7b27818ae0138b8c5bdba1ff33d77e25918609c1741f2c4d4236dcaff1
+ message: '~Unable to parse at line 1 \(near " \- fieldDefIdentifier\: image"\)\.~'
+ -
+ path: docs/content_management/taxonomy/taxonomy.md
+ line: 194
+ hash: 968846dbeb558b9eb914194845153634b45d8a993977bc321fc9df8e3d29d9cb
+ message: '~The child config "default_embedding_max_tokens" under "ibexa_taxonomy\.text_to_taxonomy" must be configured\: Maximum number of tokens sent when generating embeddings~'
+ -
+ path: docs/content_management/workflow/workflow.md
+ line: 135
+ hash: 0fdf8c632c4f5abed2f8af4fa177bf8afd9b129a48ff1fb611923135b856f865
+ message: '~The child config "matcher_value_templates" under "ibexa\.system\.default\.workflows_config" must be configured\: Matcher templates configuration\.~'
+ -
+ path: docs/customer_management/cp_page_builder.md
+ line: 35
+ hash: 6915e3e4985cf6b8d99ae25c59ddf2f735fa927fdddb4b2a9e095475b86d02db
+ message: '~The child config "default_siteaccess" under "ibexa\.siteaccess" must be configured\: Name of the default siteaccess~'
+ -
+ path: docs/customer_management/cp_page_builder.md
+ line: 119
+ hash: 833557b1406f91474d3420c809f8cf7ffc7ebefcf12aa37e66d31c291c73a2e6
+ message: '~The child config "default_siteaccess" under "ibexa\.siteaccess" must be configured\: Name of the default siteaccess~'
+ -
+ path: docs/customer_management/cp_page_builder.md
+ line: 207
+ hash: 98de256c0b149da15bea944ff56da3dad8bf968f5aad2d2882855d8844541dc1
+ message: '~The child config "list" under "ibexa\.siteaccess" must be configured\: Available SiteAccess list~'
+ -
+ path: docs/discounts/extend_discounts.md
+ line: 229
+ hash: 06089de0d522e5dc8e3a4973eeba57034d001867540768e1805211242768f23a
+ message: '~Unable to parse at line 1 \(near " App\\Discounts\\RecentDiscountPrioritizationStrategy\:"\)\.~'
+ -
+ path: docs/discounts/extend_discounts.md
+ line: 64
+ hash: 24653d7f59ac0fca6cb86161ebf94516d356563f36bc69a65d1e210a13cba6e2
+ message: '~Unable to parse at line 1 \(near " App\\Discounts\\ExpressionProvider\\CurrentUserRegistrationDateResolver\:"\)\.~'
+ -
+ path: docs/discounts/extend_discounts.md
+ line: 187
+ hash: 415de44b6fd825b157660aec16d415842cb0dadb636b1c0014ba167fc1115973
+ message: '~Unable to parse at line 1 \(near " App\\Discounts\\Rule\\PurchasingPowerParityRuleFactory\:"\)\.~'
+ -
+ path: docs/discounts/extend_discounts.md
+ line: 211
+ hash: 8459adf86ffd3f1fe5cccf1c12d887d783c6b6e85be7dfc77ea6c3229ba9569e
+ message: '~Unable to parse at line 1 \(near " App\\Discounts\\Rule\\PurchaseParityValueFormatter\:"\)\.~'
+ -
+ path: docs/discounts/extend_discounts.md
+ line: 82
+ hash: 9bf9dc0f20ec840e7c362adf00680d1e67d66f9a1fe6788d32950b3dd250feae
+ message: '~Unable to parse at line 1 \(near " App\\Discounts\\ExpressionProvider\\IsAnniversaryResolver\:"\)\.~'
+ -
+ path: docs/discounts/extend_discounts.md
+ line: 139
+ hash: e049b49c97fe7c51ab27f584e15f9283a83664ed3b4d8b2fcae1866a15d46c72
+ message: '~Unable to parse at line 1 \(near " App\\Discounts\\Condition\\IsAccountAnniversaryConditionFactory\:"\)\.~'
+ -
+ path: docs/discounts/extend_discounts_wizard.md
+ line: 142
+ hash: 2e79e1bc175f608e6b74a9e28e9145367f4210f44e254c4f188911df6d368645
+ message: '~Unable to parse at line 1 \(near " App\\Form\\FormMapper\\PurchasingPowerParityValueMapper\: \~"\)\.~'
+ -
+ path: docs/getting_started/first_steps.md
+ line: 120
+ hash: d6956fc656de2808d27e294db9f4a5a1a66630f046ea21b8e75da2f658984f99
+ message: '~The child config "default_siteaccess" under "ibexa\.siteaccess" must be configured\: Name of the default siteaccess~'
+ -
+ path: docs/infrastructure_and_maintenance/cache/http_cache/reverse_proxy.md
+ line: 145
+ hash: 15a7e49dc68c59afa39e10276a448edf6f9e501374b2a75d43c0edaa7882f5e2
+ message: '~The child config "fastly" under "ibexa\.system\.my_siteaccess_group\.http_cache" must be configured\.~'
+ -
+ path: docs/infrastructure_and_maintenance/cache/http_cache/reverse_proxy.md
+ line: 128
+ hash: 68ea24e38b23f9efbfe4d689cffb5eb147ea6f332dc334fecc3f25f0117c4fb8
+ message: '~The child config "fastly" under "ibexa\.system\.my_siteaccess_group\.http_cache" must be configured\.~'
+ -
+ path: docs/infrastructure_and_maintenance/security/development_security.md
+ line: 20
+ hash: 0521cbcc27827c7ee2160a512158a19c89f1d51eb64a69d1b4600eb45a2505dd
+ message: '~Unrecognized option "require_previous_session" under "security\.firewalls\.ibexa_front\.form_login"\. Available options are "always_use_default_target_path", "check_path", "csrf_parameter", "csrf_token_id", "default_target_path", "enable_csrf", "failure_forward", "failure_handler", "failure_path", "failure_path_parameter", "form_only", "login_path", "password_parameter", "post_only", "provider", "remember_me", "success_handler", "target_path_parameter", "use_forward", "use_referer", "username_parameter"\.~'
+ -
+ path: docs/infrastructure_and_maintenance/security/security_checklist.md
+ line: 142
+ hash: 65986b42415e23ed46aec11a3fb7398b138665b9fb75432e1457c7ebf89d18ec
+ message: '~The child config "firewalls" under "security" must be configured\.~'
+ -
+ path: docs/multisite/languages/languages.md
+ line: 78
+ hash: 7d34665cd5af51823e88b55935ea40dfa550684bf2c3af7eba20349adfbb6be4
+ message: '~The child config "match" under "ibexa\.siteaccess" must be configured\: Siteaccess match configuration\. First key is the matcher class, value is passed to the matcher\. Key can be a service identifier \(prepended by "@"\), or a FQ class name \(prepended by "\\"\)~'
+ -
+ path: docs/multisite/languages/languages.md
+ line: 135
+ hash: 85fc7f295269781688143689f90a32b0f08231b74037ba73bf15d088108492d8
+ message: '~The child config "match" under "ibexa\.siteaccess" must be configured\: Siteaccess match configuration\. First key is the matcher class, value is passed to the matcher\. Key can be a service identifier \(prepended by "@"\), or a FQ class name \(prepended by "\\"\)~'
+ -
+ path: docs/multisite/multisite_configuration.md
+ line: 57
+ hash: 15dee03480731709d21c89105139a95add1377abb22ea78061b47fff1e013bba
+ message: '~The child config "list" under "ibexa\.siteaccess" must be configured\: Available SiteAccess list~'
+ -
+ path: docs/multisite/multisite_configuration.md
+ line: 28
+ hash: 306a74d1cc587b13c8d3378a9b05815d16cfe0a0e10f9766685e8f39e81f98fd
+ message: '~The child config "list" under "ibexa\.siteaccess" must be configured\: Available SiteAccess list~'
+ -
+ path: docs/multisite/multisite_configuration.md
+ line: 69
+ hash: c5a5cb7578b56189855285f63f16910ef708767ddc9389730d537f97725b5a89
+ message: '~The child config "list" under "ibexa\.siteaccess" must be configured\: Available SiteAccess list~'
+ -
+ path: docs/multisite/set_up_translation_siteaccess.md
+ line: 43
+ hash: 4038aedf899c1fc78ce0a57750c6c8dc31a317d0178ea3e612554da1e4bbe564
+ message: '~The child config "default_siteaccess" under "ibexa\.siteaccess" must be configured\: Name of the default siteaccess~'
+ -
+ path: docs/multisite/site_factory/site_factory.md
+ line: 58
+ hash: a1f0951071e5dec4f362284872b59167d59d3ff180e607e089c8a84488fb2c3d
+ message: '~The child config "list" under "ibexa\.siteaccess" must be configured\: Available SiteAccess list~'
+ -
+ path: docs/multisite/site_factory/site_factory.md
+ line: 40
+ hash: e51a407b99ce4b87b710537f7caf45ad7528f720e4f2a1caf3c67ad647cc83f1
+ message: '~The child config "list" under "ibexa\.siteaccess" must be configured\: Available SiteAccess list~'
+ -
+ path: docs/multisite/site_factory/site_factory_configuration.md
+ line: 81
+ hash: 8721257146510878c104fbcddf27e0bf14afb71aba069a5a38b31f86b8d90db2
+ message: '~The child config "siteaccess_group" under "ibexa_site_factory\.templates\.\" must be configured\.~'
+ -
+ path: docs/multisite/siteaccess/siteaccess_matching.md
+ line: 72
+ hash: 1713ad609895c28fec2118385b9ec1605a7a299f4909c2fbd5c1363006fdb8ed
+ message: '~The child config "list" under "ibexa\.siteaccess" must be configured\: Available SiteAccess list~'
+ -
+ path: docs/multisite/siteaccess/siteaccess_matching.md
+ line: 178
+ hash: 1b1fddbc3630d435957596321f59276f8fbb9faba14e3bee49749173eddefdc2
+ message: '~The child config "list" under "ibexa\.siteaccess" must be configured\: Available SiteAccess list~'
+ -
+ path: docs/multisite/siteaccess/siteaccess_matching.md
+ line: 32
+ hash: 2a4a8871fdaf5ba2c788513e81ef70506902f198baa2bc451e28c0e1e20f4c67
+ message: '~The child config "list" under "ibexa\.siteaccess" must be configured\: Available SiteAccess list~'
+ -
+ path: docs/multisite/siteaccess/siteaccess_matching.md
+ line: 91
+ hash: 691944c34e02c072fbca08e59c43ede5fbd019d1cefc776cd18a512927892d47
+ message: '~The child config "list" under "ibexa\.siteaccess" must be configured\: Available SiteAccess list~'
+ -
+ path: docs/multisite/siteaccess/siteaccess_matching.md
+ line: 123
+ hash: 78630816bedadf40ecf85287910909a1cfc6b6beebd398807581ae4c3d06ca4a
+ message: '~The child config "list" under "ibexa\.siteaccess" must be configured\: Available SiteAccess list~'
+ -
+ path: docs/multisite/siteaccess/siteaccess_matching.md
+ line: 108
+ hash: 8946bcd67708bd70885f51c5731ee749a649eb66844f1d69cdc18cb81705083a
+ message: '~The child config "list" under "ibexa\.siteaccess" must be configured\: Available SiteAccess list~'
+ -
+ path: docs/multisite/siteaccess/siteaccess_matching.md
+ line: 194
+ hash: a1f0951071e5dec4f362284872b59167d59d3ff180e607e089c8a84488fb2c3d
+ message: '~The child config "list" under "ibexa\.siteaccess" must be configured\: Available SiteAccess list~'
+ -
+ path: docs/multisite/siteaccess/siteaccess_matching.md
+ line: 140
+ hash: ac6a06d9a33e4dcd9c73d2077eca6c6b3398b93cfda729ec7b18f24a45dcc976
+ message: '~The child config "list" under "ibexa\.siteaccess" must be configured\: Available SiteAccess list~'
+ -
+ path: docs/multisite/siteaccess/siteaccess_matching.md
+ line: 161
+ hash: bca418b7db9ad6c24b726a098611f793080df90e6a77bd71b378b7c2827b8026
+ message: '~The child config "list" under "ibexa\.siteaccess" must be configured\: Available SiteAccess list~'
+ -
+ path: docs/multisite/siteaccess/siteaccess_matching.md
+ line: 237
+ hash: e6259973305f42d67138e6b99f919b28b817968c03743bc8533540a5d3453d12
+ message: '~The child config "list" under "ibexa\.siteaccess" must be configured\: Available SiteAccess list~'
+ -
+ path: docs/permissions/limitation_reference.md
+ line: 28
+ hash: c989b346cda18ba902f44d6dfb2aa9129a1da96c87fcd7be9df0f84710155ebc
+ message: '~Unable to parse at line 3 \(near " ibexa\.api\.role\.limitation_type\.function_list\:"\)\.~'
+ -
+ path: docs/personalization/attribute_search_in_elasticsearch.md
+ line: 16
+ hash: 4109336477914bb243c21073b32efd591f427b50f63f9305c39386055b915cd4
+ message: '~Invalid output type in \{"\"\:\{"title"\:"\"\}\}\. Output type id should be type of int\.~'
+ -
+ path: docs/product_catalog/enable_purchasing_products.md
+ line: 106
+ hash: 86fafb34287eaa9ce0fc52bec555e34c8e91f61778421cff734c8d42eb40e300
+ message: '~Unable to parse at line 1 \(near " none\:"\)\.~'
+ -
+ path: docs/product_catalog/product_catalog_configuration.md
+ line: 77
+ hash: 86fafb34287eaa9ce0fc52bec555e34c8e91f61778421cff734c8d42eb40e300
+ message: '~Unable to parse at line 1 \(near " none\:"\)\.~'
+ -
+ path: docs/recommendations/raptor_integration/tracking_functions.md
+ line: 33
+ hash: c2988244a58202f4c2260adddb526aee1315ea134e47dc4cef1fec2aa4bded02
+ message: '~Duplicate key "connector_raptor" detected at line 6 \(near " tracking_type\: ''client'' \# Returns \