Resolve conflicts between 2.1 and 2.2

This commit is contained in:
TheMilek 2026-06-02 12:43:00 +02:00
commit 197648f0ed
No known key found for this signature in database
GPG key ID: A3F43F426B5286AA
400 changed files with 4433 additions and 1453 deletions

View file

@ -78,7 +78,7 @@ jobs:
name: "Notify about build status" name: "Notify about build status"
needs: [static-checks, e2e-mariadb, e2e-mysql, e2e-pgsql, frontend, packages] needs: [static-checks, e2e-mariadb, e2e-mysql, e2e-pgsql, frontend, packages]
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 5 timeout-minutes: 15
steps: steps:
- name: "Process data" - name: "Process data"

View file

@ -88,7 +88,7 @@ jobs:
name: "Notify about build status" name: "Notify about build status"
needs: [static-checks, e2e-mariadb, e2e-mysql, e2e-pgsql, e2e-js, frontend, packages] needs: [static-checks, e2e-mariadb, e2e-mysql, e2e-pgsql, e2e-js, frontend, packages]
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 5 timeout-minutes: 15
steps: steps:
- name: "Process data" - name: "Process data"

View file

@ -88,7 +88,7 @@ jobs:
name: "Notify about build status" name: "Notify about build status"
needs: [static-checks, e2e-mariadb, e2e-mysql, e2e-pgsql, e2e-js, frontend, packages] needs: [static-checks, e2e-mariadb, e2e-mysql, e2e-pgsql, e2e-js, frontend, packages]
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 5 timeout-minutes: 15
steps: steps:
- name: "Process data" - name: "Process data"

View file

@ -45,7 +45,7 @@ jobs:
needs: get-matrix needs: get-matrix
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: "PHPUnit, CLI, API, PHP ${{ matrix.php }}, Symfony ${{ matrix.symfony }}, MariaDB ${{ matrix.mariadb }}, State Machine Adapter ${{ matrix.state_machine_adapter }}${{ matrix.api_platform && format(', ApiPlatform {0}', matrix.api_platform) || '' }}" name: "PHPUnit, CLI, API, PHP ${{ matrix.php }}, Symfony ${{ matrix.symfony }}, MariaDB ${{ matrix.mariadb }}, State Machine Adapter ${{ matrix.state_machine_adapter }}${{ matrix.api_platform && format(', ApiPlatform {0}', matrix.api_platform) || '' }}"
timeout-minutes: 45 timeout-minutes: 15
strategy: strategy:
fail-fast: false fail-fast: false
matrix: ${{ fromJson(needs.get-matrix.outputs.matrix) }} matrix: ${{ fromJson(needs.get-matrix.outputs.matrix) }}
@ -158,7 +158,7 @@ jobs:
needs: get-matrix needs: get-matrix
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: "UI, PHP ${{ matrix.php }}, Symfony ${{ matrix.symfony }}, MariaDB ${{ matrix.mariadb }}, State Machine Adapter ${{ matrix.state_machine_adapter }}${{ matrix.api_platform && format(', ApiPlatform {0}', matrix.api_platform) || '' }}" name: "UI, PHP ${{ matrix.php }}, Symfony ${{ matrix.symfony }}, MariaDB ${{ matrix.mariadb }}, State Machine Adapter ${{ matrix.state_machine_adapter }}${{ matrix.api_platform && format(', ApiPlatform {0}', matrix.api_platform) || '' }}"
timeout-minutes: 45 timeout-minutes: 15
strategy: strategy:
fail-fast: false fail-fast: false
matrix: ${{ fromJson(needs.get-matrix.outputs.matrix) }} matrix: ${{ fromJson(needs.get-matrix.outputs.matrix) }}

View file

@ -45,7 +45,7 @@ jobs:
needs: get-matrix needs: get-matrix
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: "PHPUnit, CLI, API, PHP ${{ matrix.php }}, Symfony ${{ matrix.symfony }}, MySQL ${{ matrix.mysql }}" name: "PHPUnit, CLI, API, PHP ${{ matrix.php }}, Symfony ${{ matrix.symfony }}, MySQL ${{ matrix.mysql }}"
timeout-minutes: 45 timeout-minutes: 15
strategy: strategy:
fail-fast: false fail-fast: false
matrix: ${{ fromJson(needs.get-matrix.outputs.matrix) }} matrix: ${{ fromJson(needs.get-matrix.outputs.matrix) }}
@ -144,7 +144,7 @@ jobs:
needs: get-matrix needs: get-matrix
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: "UI, PHP ${{ matrix.php }}, Symfony ${{ matrix.symfony }}, MySQL ${{ matrix.mysql }}" name: "UI, PHP ${{ matrix.php }}, Symfony ${{ matrix.symfony }}, MySQL ${{ matrix.mysql }}"
timeout-minutes: 45 timeout-minutes: 15
strategy: strategy:
fail-fast: false fail-fast: false
matrix: ${{ fromJson(needs.get-matrix.outputs.matrix) }} matrix: ${{ fromJson(needs.get-matrix.outputs.matrix) }}

View file

@ -45,7 +45,7 @@ jobs:
needs: get-matrix needs: get-matrix
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: "PHPUnit, CLI, API, PHP ${{ matrix.php }}, Symfony ${{ matrix.symfony }}, PostgreSQL ${{ matrix.postgres }}" name: "PHPUnit, CLI, API, PHP ${{ matrix.php }}, Symfony ${{ matrix.symfony }}, PostgreSQL ${{ matrix.postgres }}"
timeout-minutes: 45 timeout-minutes: 15
strategy: strategy:
fail-fast: false fail-fast: false
matrix: ${{ fromJson(needs.get-matrix.outputs.matrix) }} matrix: ${{ fromJson(needs.get-matrix.outputs.matrix) }}
@ -132,7 +132,7 @@ jobs:
needs: get-matrix needs: get-matrix
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: "UI, PHP ${{ matrix.php }}, Symfony ${{ matrix.symfony }}, PostgreSQL ${{ matrix.postgres }}" name: "UI, PHP ${{ matrix.php }}, Symfony ${{ matrix.symfony }}, PostgreSQL ${{ matrix.postgres }}"
timeout-minutes: 45 timeout-minutes: 15
strategy: strategy:
fail-fast: false fail-fast: false
matrix: ${{ fromJson(needs.get-matrix.outputs.matrix) }} matrix: ${{ fromJson(needs.get-matrix.outputs.matrix) }}

View file

@ -17,7 +17,7 @@ jobs:
behat-no-js-unstable: behat-no-js-unstable:
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: "Non-JS, PHP ${{ matrix.php }}, Symfony ${{ matrix.symfony }}, MySQL ${{ matrix.mysql }} (Unstable Dependencies)" name: "Non-JS, PHP ${{ matrix.php }}, Symfony ${{ matrix.symfony }}, MySQL ${{ matrix.mysql }} (Unstable Dependencies)"
timeout-minutes: 45 timeout-minutes: 15
continue-on-error: true continue-on-error: true
strategy: strategy:
fail-fast: false fail-fast: false
@ -98,7 +98,7 @@ jobs:
behat-ui-js-chromedriver-unstable: behat-ui-js-chromedriver-unstable:
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: "JS with Chromedriver, PHP ${{ matrix.php }}, Symfony ${{ matrix.symfony }} (${{ matrix.env || 'test_cached' }}), MySQL ${{ matrix.mysql }} (Unstable Dependencies)" name: "JS with Chromedriver, PHP ${{ matrix.php }}, Symfony ${{ matrix.symfony }} (${{ matrix.env || 'test_cached' }}), MySQL ${{ matrix.mysql }} (Unstable Dependencies)"
timeout-minutes: 45 timeout-minutes: 15
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@ -180,7 +180,7 @@ jobs:
behat-ui-js-panther-unstable: behat-ui-js-panther-unstable:
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: "JS with Panther, PHP ${{ matrix.php }}, Symfony ${{ matrix.symfony }} (${{ matrix.env || 'test_cached' }}), MySQL ${{ matrix.mysql }} (Unstable Dependencies)" name: "JS with Panther, PHP ${{ matrix.php }}, Symfony ${{ matrix.symfony }} (${{ matrix.env || 'test_cached' }}), MySQL ${{ matrix.mysql }} (Unstable Dependencies)"
timeout-minutes: 45 timeout-minutes: 15
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:

View file

@ -53,7 +53,7 @@ jobs:
needs: get-matrix needs: get-matrix
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: "[${{ matrix.group }}] [PHP ${{ matrix.php }}, Symfony ${{ matrix.symfony }}, MySQL ${{ matrix.mysql }}]" name: "[${{ matrix.group }}] [PHP ${{ matrix.php }}, Symfony ${{ matrix.symfony }}, MySQL ${{ matrix.mysql }}]"
timeout-minutes: 10 timeout-minutes: 15
strategy: strategy:
fail-fast: false fail-fast: false
matrix: ${{ fromJson(needs.get-matrix.outputs.matrix) }} matrix: ${{ fromJson(needs.get-matrix.outputs.matrix) }}
@ -134,7 +134,7 @@ jobs:
needs: get-matrix needs: get-matrix
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: "Panther, PHP ${{ matrix.php }}, Symfony ${{ matrix.symfony }}, MySQL ${{ matrix.mysql }}" name: "Panther, PHP ${{ matrix.php }}, Symfony ${{ matrix.symfony }}, MySQL ${{ matrix.mysql }}"
timeout-minutes: 10 timeout-minutes: 15
strategy: strategy:
fail-fast: false fail-fast: false
matrix: ${{ fromJson(needs.get-matrix.outputs.panther-matrix) }} matrix: ${{ fromJson(needs.get-matrix.outputs.panther-matrix) }}

View file

@ -21,7 +21,7 @@ jobs:
test_unstable: test_unstable:
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: "PHP ${{ matrix.php }}, Symfony ${{ matrix.symfony }} (Unstable Dependencies)" name: "PHP ${{ matrix.php }}, Symfony ${{ matrix.symfony }} (Unstable Dependencies)"
timeout-minutes: 25 timeout-minutes: 15
strategy: strategy:
fail-fast: false fail-fast: false

View file

@ -45,7 +45,7 @@ jobs:
needs: get-matrix needs: get-matrix
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: "PHP ${{ matrix.php }}, Symfony ${{ matrix.symfony }}" name: "PHP ${{ matrix.php }}, Symfony ${{ matrix.symfony }}"
timeout-minutes: 25 timeout-minutes: 15
strategy: strategy:
fail-fast: false fail-fast: false

View file

@ -8,7 +8,7 @@
}, },
{ {
"php": "8.4", "php": "8.4",
"symfony": "~7.3.0" "symfony": "~7.4.0"
}, },
{ {
"php": "8.5", "php": "8.5",
@ -125,7 +125,7 @@
}, },
{ {
"php": "8.5", "php": "8.5",
"symfony": "~7.3.0" "symfony": "~7.4.0"
} }
] ]
} }
@ -139,7 +139,6 @@
], ],
"symfony": [ "symfony": [
"~6.4.0", "~6.4.0",
"~7.3.0",
"~7.4.0" "~7.4.0"
] ]
}, },
@ -151,7 +150,6 @@
], ],
"symfony": [ "symfony": [
"~6.4.0", "~6.4.0",
"~7.3.0",
"~7.4.0" "~7.4.0"
], ],
"mariadb": [ "mariadb": [
@ -163,7 +161,7 @@
"include": [ "include": [
{ {
"php": "8.3", "php": "8.3",
"symfony": "~7.3.0", "symfony": "~7.4.0",
"mariadb": "11.4.7", "mariadb": "11.4.7",
"state_machine_adapter": "winzou_state_machine" "state_machine_adapter": "winzou_state_machine"
} }
@ -176,7 +174,6 @@
], ],
"symfony": [ "symfony": [
"~6.4.0", "~6.4.0",
"~7.3.0",
"~7.4.0" "~7.4.0"
], ],
"mysql": [ "mysql": [
@ -188,7 +185,7 @@
"include": [ "include": [
{ {
"php": "8.3", "php": "8.3",
"symfony": "~7.3.0", "symfony": "~7.4.0",
"mysql": "8.0", "mysql": "8.0",
"twig": "^3.3" "twig": "^3.3"
} }
@ -250,7 +247,6 @@
], ],
"symfony": [ "symfony": [
"~6.4.0", "~6.4.0",
"~7.3.0",
"~7.4.0" "~7.4.0"
], ],
"postgres": [ "postgres": [
@ -273,7 +269,6 @@
], ],
"symfony": [ "symfony": [
"~6.4.0", "~6.4.0",
"~7.3.0",
"~7.4.0" "~7.4.0"
] ]
} }

View file

@ -16,7 +16,7 @@ jobs:
name: "Coding standard refactor" name: "Coding standard refactor"
timeout-minutes: 5 timeout-minutes: 15
if: github.repository == 'Sylius/Sylius' if: github.repository == 'Sylius/Sylius'

View file

@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.repository == 'Sylius/Sylius' if: github.repository == 'Sylius/Sylius'
name: "Upmerge PR" name: "Upmerge PR"
timeout-minutes: 5 timeout-minutes: 15
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:

View file

@ -1,18 +1,51 @@
# AI Contribution Guidelines # AI Contribution Guidelines
Welcome, 🤖 AI assistant! Please follow these guidelines when contributing to this Sylius: Guidelines for AI assistants contributing to Sylius.
## Reference Files
When working on specific areas, check these files for patterns:
### Entities & Models
- Entity pattern: `src/Sylius/Component/Core/Model/Product.php`
- Interface pattern: `src/Sylius/Component/Core/Model/ProductInterface.php`
- Doctrine mapping: `src/Sylius/Bundle/CoreBundle/Resources/config/doctrine/model/`
### API Platform 4.x
- Resource definitions: `src/Sylius/Bundle/ApiBundle/Resources/config/api_platform/resources/`
- Properties/serialization: `src/Sylius/Bundle/ApiBundle/Resources/config/api_platform/properties/`
- Admin resources: `resources/admin/Product.xml`
- Shop resources: `resources/shop/Product.xml`
### Services & Configuration
- Service definitions: `src/Sylius/Bundle/CoreBundle/Resources/config/services.xml`
- Bundle config: `src/Sylius/Bundle/*/Resources/config/`
### Templates & Hooks
- Admin templates: `src/Sylius/Bundle/AdminBundle/templates/`
- Shop templates: `src/Sylius/Bundle/ShopBundle/templates/`
- Twig hooks: check existing hooks in templates for naming patterns
### Tests
- PHPUnit functional: `tests/Functional/`
- PHPUnit API: `tests/Api/`
- Behat contexts: `src/Sylius/Behat/Context/`
### Migrations
- Location: `src/Sylius/Bundle/CoreBundle/Migrations/`
- **ALWAYS create TWO migrations**: one for MySQL, one for PostgreSQL
- MySQL: extend `Sylius\Bundle\CoreBundle\Doctrine\Migrations\AbstractMigration`
- PostgreSQL: extend `Sylius\Bundle\CoreBundle\Doctrine\Migrations\AbstractPostgreSQLMigration`
- When reviewing PRs that touch migrations, verify both versions exist
## General Guidelines ## General Guidelines
### Project Structure & Design ### Project Structure & Design
- This is Sylius: e-commerce framework - Sylius is an e-commerce framework built on **Symfony**
- Sylius is built on top of **Symfony** - Bundles and Components can be used independently
- Sylius must be easily extendable in the end application
- Sylius contains Bundles and Components that can be used independently
- Sylius is designed to be modular and flexible
- Sylius is designed to be fast and efficient
- Follow the Sylius Backward Compatibility (BC) policy - Follow the Sylius Backward Compatibility (BC) policy
- When changing interfaces, always provide BC layer
### Compatibility & Security ### Compatibility & Security
@ -80,10 +113,29 @@ Welcome, 🤖 AI assistant! Please follow these guidelines when contributing to
## PHPUnit ## PHPUnit
- Place unit tests in `tests/Unit/`
- Place functional tests in `tests/Functional/`
- Place API tests in `tests/Api/`
- Test class names must end with `Test` suffix
- Use `#[Covers]` attribute for unit tests
- For API tests, extend `ApiTestCase` and use `assertResponse*` methods
- Run specific test: `vendor/bin/phpunit --filter TestClassName`
## Behat ## Behat
- Place feature files in `features/` directory
- Use existing contexts from `src/Sylius/Behat/Context/`
- Follow Given-When-Then pattern strictly
- Use `@ui` tag for UI tests, `@api` for API tests
- Page objects are in `src/Sylius/Behat/Page/`
## JavaScript ## JavaScript
- Use TypeScript where possible
- Use Stimulus controllers for interactive components
- Place controllers in `assets/admin/controllers/` or `assets/shop/controllers/`
- Follow existing naming conventions for controller files
## CSS ## CSS
- Use SCSS (`.scss`) syntax plain CSS files are not allowed - Use SCSS (`.scss`) syntax plain CSS files are not allowed
@ -94,3 +146,12 @@ Welcome, 🤖 AI assistant! Please follow these guidelines when contributing to
- Avoid `!important` unless absolutely necessary - Avoid `!important` unless absolutely necessary
- Prefer `rem` over `px` for spacing, font size, etc. - Prefer `rem` over `px` for spacing, font size, etc.
- Use `mixins/` for reusable logic (e.g., `@include icon-size(24px)`) - Use `mixins/` for reusable logic (e.g., `@include icon-size(24px)`)
## Common Mistakes to Avoid
- **BC breaks**: Never change method signatures in interfaces without deprecation layer
- **Missing Doctrine mapping**: New entity properties need XML mapping in `Resources/config/doctrine/`
- **Hardcoded strings**: Always use translation keys in templates
- **Missing serialization groups**: API properties need proper groups in `properties/*.xml`
- **Wrong namespace**: Components have no Symfony dependency, Bundles can
- **Forgetting tests**: API changes need PHPUnit tests in `tests/Api/`

115
CHANGELOG-2.2.md Normal file
View file

@ -0,0 +1,115 @@
# CHANGELOG FOR `2.2.X`
## v2.2.5 (2026-04-10)
#### Details
- [#18579](https://github.com/Sylius/Sylius/pull/18579) Fix problem with empty taxon product index ([@tomkalon](https://github.com/tomkalon))
- [#18932](https://github.com/Sylius/Sylius/pull/18932) TASK: improve german translations for CH, DE, and AT ([@crydotsnake](https://github.com/crydotsnake))
- [#18933](https://github.com/Sylius/Sylius/pull/18933) fix: add default filter to breadcrumbs configuration title to prevent ScalarDataBag exception ([@camilleislasse](https://github.com/camilleislasse))
- [#18943](https://github.com/Sylius/Sylius/pull/18943) Fix build after APIPlatform 4.3.2 release ([@TheMilek](https://github.com/TheMilek))
- [#18941](https://github.com/Sylius/Sylius/pull/18941) Add payment enabled in channel validation ([@marekrzytki](https://github.com/marekrzytki))
- [#18926](https://github.com/Sylius/Sylius/pull/18926) [BUGFIX] Allow updating provinceName when provinceCode is null in API ([@rust-le](https://github.com/rust-le))
- [#18953](https://github.com/Sylius/Sylius/pull/18953) [BUGFIX] Make mailer services public to allow usage in state machine callbacks ([@rust-le](https://github.com/rust-le))
- [#18961](https://github.com/Sylius/Sylius/pull/18961) Resolve flash alert correctly without breaking translation ([@TheMilek](https://github.com/TheMilek))
- [#18940](https://github.com/Sylius/Sylius/pull/18940) Restore missing page titles for admin resource show pages ([@marekrzytki](https://github.com/marekrzytki))
- [#18922](https://github.com/Sylius/Sylius/pull/18922) Unified name translations handling in admin grid index ([@pbalcerzak](https://github.com/pbalcerzak))
- [#18965](https://github.com/Sylius/Sylius/pull/18965) Allow choosing different payment method with skip payment step when it gets disabled ([@TheMilek](https://github.com/TheMilek))
- [#18969](https://github.com/Sylius/Sylius/pull/18969) Add missing status code mapping for OrderItemNotFoundException ([@marekrzytki](https://github.com/marekrzytki))
- [#18970](https://github.com/Sylius/Sylius/pull/18970) Add email validation constraints to cart update ([@marekrzytki](https://github.com/marekrzytki))
- [#18972](https://github.com/Sylius/Sylius/pull/18972) Fix admin templates ([@loic425](https://github.com/loic425))
- [#18958](https://github.com/Sylius/Sylius/pull/18958) [API] Make API Platform resource command classes overridable via container parameters ([@Prometee](https://github.com/Prometee))
- [#18974](https://github.com/Sylius/Sylius/pull/18974) Unify Tests directory with tests ([@TheMilek](https://github.com/TheMilek))
## v2.2.4 (2026-03-18)
#### Details
- [#18904](https://github.com/sylius/sylius/issues/18904) [BUGFIX] remove redundant `object` from PHPDoc union types
- [#18899](https://github.com/sylius/sylius/issues/18899) [CS][DX] Refactor
- [#18898](https://github.com/sylius/sylius/issues/18898) [CS][DX] Refactor
- [#18895](https://github.com/sylius/sylius/issues/18895) [Admin] Fix product taxon grid `enabled` field always showing `true`
- [#18911](https://github.com/sylius/sylius/issues/18911) [BUGFIX] fix build errors
- [#18920](https://github.com/sylius/sylius/issues/18920) Telemetry improvements 2.1
## v2.2.3 (2026-03-09)
#### Details
- [#18747](https://github.com/Sylius/Sylius/pull/18747) Fix panther build ([@TheMilek](https://github.com/TheMilek))
- [#18758](https://github.com/Sylius/Sylius/pull/18758) Remove duplicated serialization group field ([@TheMilek](https://github.com/TheMilek))
- [#18785](https://github.com/Sylius/Sylius/pull/18785) Try to fix build after ResourceBundle release ([@TheMilek](https://github.com/TheMilek))
- [#18742](https://github.com/Sylius/Sylius/pull/18742) [Admin] Fix order history address fields not displaying empty values ([@Wojdylak](https://github.com/Wojdylak))
- [#18806](https://github.com/Sylius/Sylius/pull/18806) Fix after new release of PayumBundle ([@TheMilek](https://github.com/TheMilek))
- [#18836](https://github.com/Sylius/Sylius/pull/18836) Upgrade BuildTestAppAction from v3.0.1 to v4 ([@TheMilek](https://github.com/TheMilek))
- [#18832](https://github.com/Sylius/Sylius/pull/18832) [Admin] Fix images not being emitted with Webpack 5.105+ ([@GSadee](https://github.com/GSadee))
- [#18841](https://github.com/Sylius/Sylius/pull/18841) Fix Dutch translation for payment method ([@JordiDekker](https://github.com/JordiDekker))
- [#18871](https://github.com/Sylius/Sylius/pull/18871) Add conflict to api-platform/serializer 4.2.17 ([@TheMilek](https://github.com/TheMilek))
- [#18888](https://github.com/Sylius/Sylius/pull/18888) Remove redundant check with apip4.1.7 in matrix ([@TheMilek](https://github.com/TheMilek))
- [#18887](https://github.com/Sylius/Sylius/pull/18887) [BUGFIX] Make GitHub actions green again 2.1 ([@rust-le](https://github.com/rust-le))
- [#18844](https://github.com/Sylius/Sylius/pull/18844) Fix formatting in UPGRADE-2.0.md ([@LucaGallinari](https://github.com/LucaGallinari))
## v2.2.2 (2026-01-20)
#### Details
- [#18691](https://github.com/Sylius/Sylius/pull/18691) [CS][DX] Refactor
- [#18690](https://github.com/Sylius/Sylius/pull/18690) [CS][DX] Refactor
- [#18689](https://github.com/Sylius/Sylius/pull/18689) [CS][DX] Refactor
- [#18693](https://github.com/Sylius/Sylius/pull/18693) [CI] Standardize job timeouts to 15 minutes ([@Rafikooo](https://github.com/Rafikooo))
- [#18703](https://github.com/Sylius/Sylius/pull/18703) [Deps] Allow psr/http-message ^2.0 ([@Rafikooo](https://github.com/Rafikooo))
- [#18716](https://github.com/Sylius/Sylius/pull/18716) Try to fix build with PHP 8.5 and 8.4 ([@TheMilek](https://github.com/TheMilek))
- [#18707](https://github.com/Sylius/Sylius/pull/18707) [DX] Update branch aliases to 1.14-dev ([@Rafikooo](https://github.com/Rafikooo))
- [#18721](https://github.com/Sylius/Sylius/pull/18721) Fix tests application error templates ([@loic425](https://github.com/loic425))
- [#16892](https://github.com/Sylius/Sylius/pull/16892) [FIXTURES] Fix menu taxon code ([@TheMilek](https://github.com/TheMilek))
- [#18366](https://github.com/Sylius/Sylius/pull/18366) Fix ro PayumBundle Translation ([@revoltek-daniel](https://github.com/revoltek-daniel))
- [#18725](https://github.com/Sylius/Sylius/pull/18725) Bugfix/merged overrides missing operations ([@TheMilek](https://github.com/TheMilek))
- [#18702](https://github.com/Sylius/Sylius/pull/18702) [Composer] Remove outdated twig/twig conflicts from bundles ([@Rafikooo](https://github.com/Rafikooo))
- [#18722](https://github.com/Sylius/Sylius/pull/18722) [API] Payment Request fix default action when IRI is given ([@Prometee](https://github.com/Prometee))
- [#18732](https://github.com/Sylius/Sylius/pull/18732) Make PostgreSQL telemetry migration extend dedicated abstract ([@TheMilek](https://github.com/TheMilek))
- [#18369](https://github.com/Sylius/Sylius/pull/18369) Add form help rendering to forms ([@tomkalon](https://github.com/tomkalon))
- [#18735](https://github.com/Sylius/Sylius/pull/18735) [CS][DX] Refactor
- [#18726](https://github.com/Sylius/Sylius/pull/18726) [Channel] Resolve localhost equivalents when matching channel by hostname ([@Rafikooo](https://github.com/Rafikooo))
- [#18708](https://github.com/Sylius/Sylius/pull/18708) [DX] Improve AI contribution guidelines ([@Rafikooo](https://github.com/Rafikooo))
- [#18711](https://github.com/Sylius/Sylius/pull/18711) Fix typo in first_name.html.twig ([@LucaGallinari](https://github.com/LucaGallinari))
- [#18736](https://github.com/Sylius/Sylius/pull/18736) TASK: [2.2] [UiBundle] adjust translations for admin password reset page ([@crydotsnake](https://github.com/crydotsnake))
- [#18565](https://github.com/Sylius/Sylius/pull/18565) Add ternary operator to fix related with empty key ([@michalkaczmarek-bitbag](https://github.com/michalkaczmarek-bitbag))
- [#18572](https://github.com/Sylius/Sylius/pull/18572) Bugfix/fix autocomplete in admin to be case insensitive ([@michalkaczmarek-bitbag](https://github.com/michalkaczmarek-bitbag))
## v2.2.1 (2025-12-18)
#### Details
- [#18680](https://github.com/Sylius/Sylius/pull/18680) [Telemetry] Fixes and improvements 1.14 ([@TheMilek](https://github.com/TheMilek))
## v2.2.0 (2025-12-17)
#### Details
- [#18365](https://github.com/Sylius/Sylius/pull/18365) [Feature] Prioritize default locale in TranslationLocaleProvider output ([@tomkalon](https://github.com/tomkalon))
- [#18412](https://github.com/Sylius/Sylius/pull/18412) [Maintenance] Raise minimal version of `api-platform/symfony` to `^4.2.1` ([@tomkalon](https://github.com/tomkalon))
- [#18410](https://github.com/Sylius/Sylius/pull/18410) [SHOP][FEATURE] Introduce address form modifiers in checkout component ([@tomkalon](https://github.com/tomkalon))
- [#18438](https://github.com/Sylius/Sylius/pull/18438) Remove phpspec/prophecy-phpunit dependency ([@Rafikooo](https://github.com/Rafikooo))
- [#18374](https://github.com/Sylius/Sylius/pull/18374) [CORE] Add priority to Zone ([@tomkalon](https://github.com/tomkalon))
- [#18546](https://github.com/Sylius/Sylius/pull/18546) [API] Return 422 instead of 400 for missing required fields ([@Rafikooo](https://github.com/Rafikooo))
- [#18544](https://github.com/Sylius/Sylius/pull/18544) [FEATURE] Add estimated delivery time functionality ([@Cholin2000](https://github.com/Cholin2000))
- [#18508](https://github.com/Sylius/Sylius/pull/18508) FEATURE: [2.2] [AdminBundle] make search menu in admin sidebar component hookable ([@crydotsnake](https://github.com/crydotsnake))
- [#18583](https://github.com/Sylius/Sylius/pull/18583) Mark method as deprecated because it is not used already ([@michalkaczmarek-bitbag](https://github.com/michalkaczmarek-bitbag))
- [#18614](https://github.com/Sylius/Sylius/pull/18614) [CS][DX] Refactor
- [#18594](https://github.com/Sylius/Sylius/pull/18594) [Maintenance] Bump Symfony to `^6.4 || ^7.4` ([@Rafikooo](https://github.com/Rafikooo))
- [#18568](https://github.com/Sylius/Sylius/pull/18568) [API][PaymentRequest] Rename payment request operations for shop context ([@Rafikooo](https://github.com/Rafikooo))
- [#18554](https://github.com/Sylius/Sylius/pull/18554) [AdminBundle] Allowing to use translation domain on menu ([@loic425](https://github.com/loic425))
- [#18454](https://github.com/Sylius/Sylius/pull/18454) FEAT: [2.2] [CoreBundle] extend sylius:install:setup command to set admin first/last name ([@crydotsnake](https://github.com/crydotsnake))
- [#15889](https://github.com/Sylius/Sylius/pull/15889) FEATURE: [2.2] [AdminBundle] Introduce sylius:admin-user:delete CLI Command ([@crydotsnake](https://github.com/crydotsnake))
- [#15946](https://github.com/Sylius/Sylius/pull/15946) FEATURE: [2.2] [AdminBundle] Introduce sylius:admin-user:list command ([@crydotsnake](https://github.com/crydotsnake))
- [#18646](https://github.com/Sylius/Sylius/pull/18646) [Maintenance] validation fixes ([@Cholin2000](https://github.com/Cholin2000))
- [#18654](https://github.com/Sylius/Sylius/pull/18654) [CS][DX] Refactor
- [#18647](https://github.com/Sylius/Sylius/pull/18647) [Composer] Bump dependencies + remove unused dev dependencies ([@GSadee](https://github.com/GSadee))
- [#18655](https://github.com/Sylius/Sylius/pull/18655) Add missing upgrade note to the 2.1 ([@GSadee](https://github.com/GSadee))
- [#18652](https://github.com/Sylius/Sylius/pull/18652) [CS][DX] Refactor
- [#18659](https://github.com/Sylius/Sylius/pull/18659) Add missing index to Order XML mapping ([@GSadee](https://github.com/GSadee))
- [#18661](https://github.com/Sylius/Sylius/pull/18661) Add config/reference.php to .gitignore ([@GSadee](https://github.com/GSadee))
- [#18662](https://github.com/Sylius/Sylius/pull/18662) Make new Zone priority field sortable by Gedmo ([@GSadee](https://github.com/GSadee))
- [#18669](https://github.com/Sylius/Sylius/pull/18669) Fix migrations skip commands 1.14 ([@TheMilek](https://github.com/TheMilek))
- [#18670](https://github.com/Sylius/Sylius/pull/18670) Remove Gedmo sortable-position from new Zone priority field ([@GSadee](https://github.com/GSadee))
- [#18672](https://github.com/Sylius/Sylius/pull/18672) Fix migrations skip commands 2.1 ([@TheMilek](https://github.com/TheMilek))

View file

@ -1,6 +1,66 @@
# UPGRADE FROM `2.1` TO `2.2` # UPGRADE FROM `2.1` TO `2.2`
### Deprecations ## Telemetry
Sylius 2.2.0 introduces anonymous telemetry to help us understand how Sylius is used and improve the platform.
**What data is collected:**
- Anonymous installation ID (hashed, non-reversible)
- Sylius and PHP versions, default locale
- Aggregated statistics as segments (broad ranges, not exact values):
- Customers/products/variants count (e.g., "1K-10K", "100K-1M")
- GMV and AOV ranges per month (e.g., "100K-500K", "50-100")
**No sensitive data is ever collected** - no customer information, no order details, no personal data.
**Configuration:**
Telemetry is enabled by default and uses a default salt for hashing the installation ID.
To disable telemetry, set the following environment variable in your `.env` file:
```dotenv
SYLIUS_TELEMETRY_ENABLED=0
```
To change the salt, set the `SYLIUS_TELEMETRY_SALT` environment variable:
```dotenv
SYLIUS_TELEMETRY_SALT=your-custom-salt
```
## Deprecations
1. Not injecting a `tagged_iterator` with the tag `sylius_shop.modifier.address_form_values` into the constructor of `Sylius\Bundle\ShopBundle\Twig\Component\Checkout\Address\FormComponent` is deprecated since Sylius 2.2 and will be required in Sylius 3.0.
This change enables extending the checkout address form with custom fields or logic by registering services tagged with `sylius_shop.modifier.address_form_values`, which implement the `AddressFormValuesModifierInterface`.
```php
public function __construct(
OrderRepositoryInterface $repository,
FormFactoryInterface $formFactory,
string $resourceClass,
string $formClass,
protected readonly CustomerContextInterface $customerContext,
protected readonly UserRepositoryInterface $shopUserRepository,
protected readonly AddressRepositoryInterface $addressRepository,
+ protected readonly ?iterable $addressFormValuesModifiers = null,
)
```
1. Direct usage of `loader.svg` and `loader.gif` assets is deprecated. 1. Direct usage of `loader.svg` and `loader.gif` assets is deprecated.
Use `@SyliusAdmin/shared/helper/loader.html.twig` or `@SyliusShop/shared/macro/loader.html.twig` instead. Use `@SyliusAdmin/shared/helper/loader.html.twig` or `@SyliusShop/shared/macro/loader.html.twig` instead.
1. The `Sylius\Bundle\ReviewBundle\Updater\ReviewableRatingUpdaterInterface::updateFromReview()` method has been deprecated and will be removed in Sylius 3.0. Use state machine mechanism implemented by Symfony Workflow instead.
## Admin UI
1. A new `modal-portal.js` script has been added to `AdminBundle`.
It moves Bootstrap modal elements to `<body>` before they are displayed,
preventing them from being rendered behind the Bootstrap backdrop when nested inside a CSS stacking context (e.g. the sticky `.page-header`).
## Translations
1. The `TranslationLocaleProvider` now ensures that the default locale (configured as `locale` in `config/parameters.yaml`)
is always placed at the beginning of the returned locales array.
Other locales remain in the same order as returned by the repository.

70
UPGRADE-API-2.2.md Normal file
View file

@ -0,0 +1,70 @@
# UPGRADE FROM `2.1` TO `2.2`
## Modified routes
1. The routes for payment requests resource have been renamed to follow the shop API naming convention.
| Old route | New route |
|----------------------------------------|----------------------------------------|
| `sylius_api_show_payment_request_get` | `sylius_api_shop_payment_request_get` |
| `sylius_api_show_payment_request_post` | `sylius_api_shop_payment_request_post` |
| `sylius_api_show_payment_request_put` | `sylius_api_shop_payment_request_put` |
## HTTP Status Code Changes
### Missing Required Fields Validation
The HTTP status code for missing required fields in API requests has been changed from `400 Bad Request` to `422 Unprocessable Content` to follow REST API best practices and RFC 9110 semantics.
Additionally, the redundant `code` field has been removed from the error response body, as the status code is already available in the HTTP response headers.
**Before:**
```json
{
"@context": "/api/v2/contexts/Error",
"@type": "hydra:Error",
"status": 400,
"detail": "Request does not have the following required fields specified: email."
}
```
**After:**
```json
{
"@context": "/api/v2/contexts/Error",
"@type": "hydra:Error",
"status": 422,
"detail": "Request does not have the following required fields specified: email."
}
```
**Breaking changes:**
1. HTTP status code changed: `400``422`
2. Response body field `code` removed (was redundant with HTTP status header)
**Affected endpoints:** All POST/PATCH endpoints that validate required fields (e.g., `/api/v2/shop/customers`, `/api/v2/shop/orders/{token}/items`, etc.)
**Migration guide:**
- If your API client checks `response.status === 400` for missing fields, change it to `response.status === 422`
- If your API client reads `response.data.code`, use `response.status` (HTTP header) instead
**References:** RFC 9110 (400 = syntactic errors, 422 = semantic/validation errors)
## API Platform resource classes as container parameters
The following API Platform resource classes are now defined as container parameters, allowing you to override them:
| Parameter | Default class |
|---|---|
| `sylius_api.command.account.reset_password.class` | `Sylius\Bundle\ApiBundle\Command\Account\ResetPassword` |
| `sylius_api.command.account.verify_shop_user.class` | `Sylius\Bundle\ApiBundle\Command\Account\VerifyShopUser` |
| `sylius_api.command.admin.account.reset_password.class` | `Sylius\Bundle\ApiBundle\Command\Admin\Account\ResetPassword` |
| `sylius_api.command.send_contact_request.class` | `Sylius\Bundle\ApiBundle\Command\SendContactRequest` |
To override a class, define the parameter in your configuration:
```yaml
parameters:
sylius_api.command.account.reset_password.class: App\Api\Command\CustomResetPassword
```

View file

@ -1,7 +1,7 @@
{ {
"name": "sylius/sylius", "name": "sylius/sylius",
"type": "library", "type": "library",
"version": "v2.1.16-dev", "version": "v2.2.6-dev",
"description": "E-Commerce platform for PHP, based on Symfony framework.", "description": "E-Commerce platform for PHP, based on Symfony framework.",
"homepage": "https://sylius.com", "homepage": "https://sylius.com",
"license": "MIT", "license": "MIT",
@ -31,10 +31,10 @@
"ext-json": "*", "ext-json": "*",
"ext-simplexml": "*", "ext-simplexml": "*",
"ext-sodium": "*", "ext-sodium": "*",
"api-platform/doctrine-orm": "^4.1.7", "api-platform/doctrine-orm": "^4.2.1",
"api-platform/state": "^4.1.7", "api-platform/state": "^4.2.1",
"api-platform/symfony": "^4.1.7", "api-platform/symfony": "^4.2.1",
"babdev/pagerfanta-bundle": "^4.4", "babdev/pagerfanta-bundle": "^4.6",
"behat/transliterator": "^1.5", "behat/transliterator": "^1.5",
"doctrine/collections": "^2.2", "doctrine/collections": "^2.2",
"doctrine/common": "^3.2", "doctrine/common": "^3.2",
@ -44,12 +44,12 @@
"doctrine/event-manager": "^2.0", "doctrine/event-manager": "^2.0",
"doctrine/inflector": "^2.0", "doctrine/inflector": "^2.0",
"doctrine/migrations": "^3.8", "doctrine/migrations": "^3.8",
"doctrine/orm": "^2.18 || ^3.3", "doctrine/orm": "^2.18 || ^3.5",
"doctrine/persistence": "^3.3", "doctrine/persistence": "^3.3",
"egulias/email-validator": "^4.0", "egulias/email-validator": "^4.0",
"fakerphp/faker": "^1.23", "fakerphp/faker": "^1.23",
"friendsofphp/proxy-manager-lts": "^1.0", "friendsofphp/proxy-manager-lts": "^1.0",
"gedmo/doctrine-extensions": "^3.17.1", "gedmo/doctrine-extensions": "^3.20",
"guzzlehttp/guzzle": "^7.9", "guzzlehttp/guzzle": "^7.9",
"guzzlehttp/psr7": "^2.5", "guzzlehttp/psr7": "^2.5",
"knplabs/gaufrette": "^0.11", "knplabs/gaufrette": "^0.11",
@ -59,7 +59,7 @@
"laminas/laminas-stdlib": "^3.19", "laminas/laminas-stdlib": "^3.19",
"league/flysystem-bundle": "^3.3", "league/flysystem-bundle": "^3.3",
"lexik/jwt-authentication-bundle": "^3.1", "lexik/jwt-authentication-bundle": "^3.1",
"liip/imagine-bundle": "^2.13", "liip/imagine-bundle": "^2.15",
"pagerfanta/pagerfanta": "^4.0", "pagerfanta/pagerfanta": "^4.0",
"paragonie/halite": "^5.0", "paragonie/halite": "^5.0",
"payum/offline": "^1.7.5", "payum/offline": "^1.7.5",
@ -85,65 +85,65 @@
"sylius/resource": "^1.12", "sylius/resource": "^1.12",
"sylius/resource-bundle": "^1.12", "sylius/resource-bundle": "^1.12",
"sylius/theme-bundle": "^2.4", "sylius/theme-bundle": "^2.4",
"sylius/twig-extra": "^0.8", "sylius/twig-extra": "^0.9",
"sylius/twig-hooks": "^0.8", "sylius/twig-hooks": "^0.9",
"symfony/asset": "^6.4 || ^7.2", "symfony/asset": "^6.4 || ^7.4",
"symfony/cache-contracts": "^3.5", "symfony/cache-contracts": "^3.5",
"symfony/clock": "^6.4 || ^7.2", "symfony/clock": "^6.4 || ^7.4",
"symfony/config": "^6.4 || ^7.2", "symfony/config": "^6.4 || ^7.4",
"symfony/console": "^6.4.1 || ^7.2", "symfony/console": "^6.4.1 || ^7.4",
"symfony/dependency-injection": "^6.4.1 || ^7.2", "symfony/dependency-injection": "^6.4.1 || ^7.4",
"symfony/deprecation-contracts": "^3.5", "symfony/deprecation-contracts": "^3.5",
"symfony/doctrine-bridge": "^6.4 || ^7.2", "symfony/doctrine-bridge": "^6.4 || ^7.4",
"symfony/doctrine-messenger": "^6.4.1 || ^7.2", "symfony/doctrine-messenger": "^6.4.1 || ^7.4",
"symfony/event-dispatcher": "^6.4 || ^7.2", "symfony/event-dispatcher": "^6.4 || ^7.4",
"symfony/expression-language": "^6.4 || ^7.2", "symfony/expression-language": "^6.4 || ^7.4",
"symfony/filesystem": "^6.4 || ^7.2", "symfony/filesystem": "^6.4 || ^7.4",
"symfony/finder": "^6.4 || ^7.2", "symfony/finder": "^6.4 || ^7.4",
"symfony/form": "^6.4.1 || ^7.2", "symfony/form": "^6.4.1 || ^7.4",
"symfony/framework-bundle": "^6.4.1 || ^7.2", "symfony/framework-bundle": "^6.4.1 || ^7.4",
"symfony/http-client": "^6.4 || ^7.2", "symfony/http-client": "^6.4 || ^7.4",
"symfony/http-foundation": "^6.4 || ^7.2", "symfony/http-foundation": "^6.4 || ^7.4",
"symfony/http-kernel": "^6.4.1 || ^7.2", "symfony/http-kernel": "^6.4.1 || ^7.4",
"symfony/intl": "^6.4 || ^7.2", "symfony/intl": "^6.4 || ^7.4",
"symfony/mailer": "^6.4 || ^7.2", "symfony/mailer": "^6.4 || ^7.4",
"symfony/messenger": "^6.4 || ^7.2", "symfony/messenger": "^6.4 || ^7.4",
"symfony/monolog-bundle": "^3.8.0", "symfony/monolog-bundle": "^3.8.0",
"symfony/options-resolver": "^6.4 || ^7.2", "symfony/options-resolver": "^6.4 || ^7.4",
"symfony/password-hasher": "^6.4 || ^7.2", "symfony/password-hasher": "^6.4 || ^7.4",
"symfony/polyfill-iconv": "^1.31", "symfony/polyfill-iconv": "^1.31",
"symfony/polyfill-intl-icu": "^1.31", "symfony/polyfill-intl-icu": "^1.31",
"symfony/polyfill-mbstring": "^1.31", "symfony/polyfill-mbstring": "^1.31",
"symfony/process": "^6.4 || ^7.2", "symfony/process": "^6.4 || ^7.4",
"symfony/property-access": "^6.4 || ^7.2", "symfony/property-access": "^6.4 || ^7.4",
"symfony/property-info": "^6.4 || ^7.2", "symfony/property-info": "^6.4 || ^7.4",
"symfony/proxy-manager-bridge": "^6.4 || ^7.2", "symfony/proxy-manager-bridge": "^6.4 || ^7.4",
"symfony/routing": "^6.4.1 || ^7.2", "symfony/routing": "^6.4.1 || ^7.4",
"symfony/security-bundle": "^6.4 || ^7.2", "symfony/security-bundle": "^6.4 || ^7.4",
"symfony/security-core": "^6.4 || ^7.2", "symfony/security-core": "^6.4 || ^7.4",
"symfony/security-csrf": "^6.4 || ^7.2", "symfony/security-csrf": "^6.4 || ^7.4",
"symfony/security-http": "^6.4 || ^7.2", "symfony/security-http": "^6.4 || ^7.4",
"symfony/serializer": "^6.4 || ^7.2", "symfony/serializer": "^6.4 || ^7.4",
"symfony/service-contracts": "^3.5", "symfony/service-contracts": "^3.5",
"symfony/stimulus-bundle": "^2.20", "symfony/stimulus-bundle": "^2.25",
"symfony/string": "^6.4 || ^7.2", "symfony/string": "^6.4 || ^7.4",
"symfony/translation": "^6.4 || ^7.2", "symfony/translation": "^6.4 || ^7.4",
"symfony/translation-contracts": "^3.3", "symfony/translation-contracts": "^3.3",
"symfony/twig-bundle": "^6.4 || ^7.2", "symfony/twig-bundle": "^6.4 || ^7.4",
"symfony/uid": "^6.4 || ^7.2", "symfony/uid": "^6.4 || ^7.4",
"symfony/ux-autocomplete": "^2.20", "symfony/ux-autocomplete": "^2.25",
"symfony/ux-icons": "^2.20", "symfony/ux-icons": "^2.25",
"symfony/ux-live-component": "^2.20", "symfony/ux-live-component": "^2.25",
"symfony/ux-twig-component": "^2.20", "symfony/ux-twig-component": "^2.25",
"symfony/validator": "^6.4 || ^7.2", "symfony/validator": "^6.4 || ^7.4",
"symfony/webpack-encore-bundle": "^2.2", "symfony/webpack-encore-bundle": "^2.2",
"symfony/workflow": "^6.4 || ^7.2", "symfony/workflow": "^6.4 || ^7.4",
"symfony/yaml": "^6.4 || ^7.2", "symfony/yaml": "^6.4 || ^7.4",
"symfonycasts/dynamic-forms": "^0.1", "symfonycasts/dynamic-forms": "^0.1",
"twig/extra-bundle": "^3.13", "twig/extra-bundle": "^3.16",
"twig/intl-extra": "^3.13", "twig/intl-extra": "^3.16",
"twig/string-extra": "^3.13", "twig/string-extra": "^3.16",
"twig/twig": "^3.14", "twig/twig": "^3.16",
"webmozart/assert": "^1.11" "webmozart/assert": "^1.11"
}, },
"replace": { "replace": {
@ -206,15 +206,11 @@
"friends-of-behat/symfony-extension": "^2.6.2", "friends-of-behat/symfony-extension": "^2.6.2",
"friends-of-behat/variadic-extension": "^1.6", "friends-of-behat/variadic-extension": "^1.6",
"hwi/oauth-bundle": "^2.2", "hwi/oauth-bundle": "^2.2",
"jangregor/phpstan-prophecy": "^2.0",
"lchrusciel/api-test-case": "^5.3", "lchrusciel/api-test-case": "^5.3",
"matthiasnoback/symfony-config-test": "^6.0", "matthiasnoback/symfony-config-test": "^6.0",
"matthiasnoback/symfony-dependency-injection-test": "^6.0", "matthiasnoback/symfony-dependency-injection-test": "^6.0",
"mikey179/vfsstream": "^1.6",
"mockery/mockery": "^1.6",
"nyholm/psr7": "^1.8", "nyholm/psr7": "^1.8",
"phparkitect/phparkitect": "^0.6", "phparkitect/phparkitect": "^0.6",
"phpspec/prophecy-phpunit": "^2.2",
"phpstan/extension-installer": "^1.4", "phpstan/extension-installer": "^1.4",
"phpstan/phpstan": "^2.0", "phpstan/phpstan": "^2.0",
"phpstan/phpstan-doctrine": "^2.0", "phpstan/phpstan-doctrine": "^2.0",
@ -226,12 +222,12 @@
"robertfausk/behat-panther-extension": "^1.1", "robertfausk/behat-panther-extension": "^1.1",
"sylius-labs/coding-standard": "^4.4", "sylius-labs/coding-standard": "^4.4",
"sylius-labs/suite-tags-extension": "~0.2", "sylius-labs/suite-tags-extension": "~0.2",
"symfony/browser-kit": "^6.4 || ^7.2", "symfony/browser-kit": "^6.4 || ^7.4",
"symfony/debug-bundle": "^6.4 || ^7.2", "symfony/debug-bundle": "^6.4 || ^7.4",
"symfony/dotenv": "^6.4 || ^7.2", "symfony/dotenv": "^6.4 || ^7.4",
"symfony/flex": "^2.4", "symfony/flex": "^2.4",
"symfony/runtime": "^6.4 || ^7.2", "symfony/runtime": "^6.4 || ^7.4",
"symfony/web-profiler-bundle": "^6.4 || ^7.2", "symfony/web-profiler-bundle": "^6.4 || ^7.4",
"symplify/monorepo-builder": "^11.0" "symplify/monorepo-builder": "^11.0"
}, },
"suggest": { "suggest": {
@ -263,10 +259,10 @@
"extra": { "extra": {
"symfony": { "symfony": {
"allow-contrib": false, "allow-contrib": false,
"require": "^7.2" "require": "^7.4"
}, },
"branch-alias": { "branch-alias": {
"dev-main": "2.0-dev" "dev-main": "2.2-dev"
} }
}, },
"autoload": { "autoload": {

View file

@ -67,3 +67,13 @@ Feature: Editing a zone
And I save my changes And I save my changes
Then I should be notified that it has been successfully edited Then I should be notified that it has been successfully edited
And this zone name should be "EU" And this zone name should be "EU"
@api @ui
Scenario: Setting zone to the highest priority
Given the store has a zone "European Union" with code "EU" and priority 0
And it has the "France" country member
When I want to modify the zone named "European Union"
And I set its priority to "5"
And I save my changes
Then I should be notified that it has been successfully edited
And the "European Union" zone should have priority 5

View file

@ -0,0 +1,26 @@
@managing_zones
Feature: Editing a zone
In order to change my my tax and shipping configuration
As an Administrator
I want to be able to edit a zone
Background:
Given the store also has country "France"
And the store has a zone "North America" with code "NA" and priority 2
And the store has a zone "South America" with code "SA" and priority 1
And the store has a zone "Australia" with code "AU" and priority 0
And I am logged in as an administrator
@api @ui
Scenario: Zones are sorted by priority in descending order by default
When I want to see all zones in store
Then I should see 3 zones in the list
Then the first zone on the list should have name "North America"
Then the last zone on the list should have name "Australia"
@api @ui
Scenario: Zone's default priority is 0 which puts it at the bottom of the list
Given the store has a zone "European Union" with code "EU"
When I want to see all zones in store
Then I should see 4 zones in the list
And the last zone on the list should have name "European Union"

View file

@ -0,0 +1,24 @@
@managing_shipping_methods
Feature: Validating delivery time range on shipping method form
In order to avoid invalid delivery time configuration
As an Administrator
I want to be prevented from saving a shipping method when maximum delivery time is less than minimum
Background:
Given the store operates on a channel named "Web-US" in "USD" currency
And the store is available in "English (United States)"
And the store has a zone "United States" with code "US"
And I am logged in as an administrator
@ui @mink:chromedriver
Scenario: Trying to add a shipping method with invalid delivery time range
When I want to create a new shipping method
And I specify its code as "DELIVERY_TIME_INVALID"
And I name it "Delivery Time Invalid" in "English (United States)"
And I define it for the zone named "United States"
And I choose "Flat rate per shipment" calculator
And I specify its amount as 10 for "Web-US" channel
And I fill in "Minimum delivery time (days)" with "5"
And I fill in "Maximum delivery time (days)" with "3"
And I try to add it
Then I should be notified that Maximum delivery time must be greater than or equal to the minimum.

View file

@ -0,0 +1,49 @@
@applying_taxes
Feature: Apply taxes based on zone priority
In order to pay correct taxes when a country belongs to multiple zones
As a Customer
I want the zone with higher priority to be used for tax calculation
Background:
Given the store operates on a single channel
Given the store operates in "France" and "Belgium"
And there is a zone "The Rest of the World" containing all other countries
And the store has a zone "European Union" with code "EU" and priority 2
And it has the "France" country member
And it also has the "Belgium" country member
And default tax zone is "RoW"
And the store has "No tax" tax rate of 0% for "Clothes" for the rest of the world
And the store has "VAT" tax rate of 23% for "Clothes" within the "European Union" zone
And the store has a product "T-Shirt" priced at "$100.00"
And it belongs to "Clothes" tax category
And I am a logged in customer
@api @ui
Scenario: Applying tax from the higher priority zone
Given I added product "T-Shirt" to the cart
And I addressed the cart to "Belgium"
When I check the details of my cart
Then my cart total should be "$123.00"
And my cart taxes should be "$23.00"
@api @ui
Scenario: Applying tax from new zone zone when it has higher priority
Given the store has a zone "Benelux countries" with code "BC" and priority 3
And it also has the "Belgium" country member
And the store has "VAT" tax rate of 0% for "Clothes" within the "Benelux countries" zone
And I added product "T-Shirt" to the cart
And I addressed the cart to "Belgium"
When I check the details of my cart
Then my cart total should be "$100.00"
And there should be no taxes charged
@api @ui
Scenario: Applying tax from current zone when the new one has lower priority
Given the store has a zone "Benelux countries" with code "GU" and priority 1
And it also has the "Belgium" country member
And the store has "VAT" tax rate of 0% for "Clothes" within the "Benelux countries" zone
And I added product "T-Shirt" to the cart
And I addressed the cart to "Belgium"
When I check the details of my cart
Then my cart total should be "$123.00"
And my cart taxes should be "$23.00"

View file

@ -21,9 +21,9 @@
}, },
"require": { "require": {
"php": "^8.2", "php": "^8.2",
"symfony/dependency-injection": "^6.4.1 || ^7.2", "symfony/dependency-injection": "^6.4.1 || ^7.4",
"symfony/http-kernel": "^6.4.1 || ^7.2", "symfony/http-kernel": "^6.4.1 || ^7.4",
"symfony/workflow": "^6.4 || ^7.2" "symfony/workflow": "^6.4 || ^7.4"
}, },
"suggest": { "suggest": {
"twig/twig": "Access the state machine in your twig templates (^2.12|^3.3)", "twig/twig": "Access the state machine in your twig templates (^2.12|^3.3)",
@ -37,10 +37,10 @@
}, },
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-main": "2.0-dev" "dev-main": "2.2-dev"
}, },
"symfony": { "symfony": {
"require": "^7.2" "require": "^7.4"
} }
} }
} }

View file

@ -93,6 +93,8 @@ interface ResponseCheckerInterface
public function hasViolationWithMessage(Response $response, string $message, ?string $property = null): bool; public function hasViolationWithMessage(Response $response, string $message, ?string $property = null): bool;
public function appendError(Response $response): ResponseCheckerInterface;
public function cleanErrors(): void; public function cleanErrors(): void;
/** @return array{ /** @return array{

View file

@ -129,6 +129,14 @@ final readonly class ManagingZonesContext implements Context
$this->client->addRequestData('scope', $scope); $this->client->addRequestData('scope', $scope);
} }
/**
* @When I set its priority to :priority
*/
public function iSetsItsPriorityTo(int $priority): void
{
$this->client->addRequestData('priority', $priority);
}
/** /**
* @When I (try to) add it * @When I (try to) add it
*/ */
@ -496,6 +504,37 @@ final readonly class ManagingZonesContext implements Context
); );
} }
/**
* @Then /^the (first|last) zone on the list should have ([^"]+) "([^"]+)"$/
*/
public function theFirstZoneOnTheListShouldHave(string $togglePosition, string $field, string $value): void
{
$items = $this->responseChecker->getValue($this->client->getLastResponse(), 'hydra:member');
if ('first' === $togglePosition) {
$item = reset($items);
} else {
$item = end($items);
}
Assert::same($item[$field], $value);
}
/**
* @Then the :zone zone should have priority :priority
*/
public function theZoneShouldHavePriority(ZoneInterface $zone, int $priority): void
{
Assert::true(
$this->responseChecker->hasItemWithValues(
$this->client->index(Resources::ZONES),
[
'name' => $zone->getName(),
'priority' => $priority,
],
),
);
}
private function removeZoneMember(CountryInterface|ProvinceInterface|ZoneInterface $objectToRemove): void private function removeZoneMember(CountryInterface|ProvinceInterface|ZoneInterface $objectToRemove): void
{ {
$members = $this->client->getContent()['members']; $members = $this->client->getContent()['members'];

View file

@ -22,6 +22,7 @@ use Sylius\Behat\Context\Ui\Admin\Helper\SecurePasswordTrait;
use Sylius\Behat\Service\SharedStorageInterface; use Sylius\Behat\Service\SharedStorageInterface;
use Sylius\Component\Core\Model\CustomerInterface; use Sylius\Component\Core\Model\CustomerInterface;
use Symfony\Component\HttpFoundation\Request as HttpRequest; use Symfony\Component\HttpFoundation\Request as HttpRequest;
use Symfony\Component\HttpFoundation\Response;
use Webmozart\Assert\Assert; use Webmozart\Assert\Assert;
final class RegistrationContext implements Context final class RegistrationContext implements Context
@ -206,7 +207,7 @@ final class RegistrationContext implements Context
$content['hydra:description'], $content['hydra:description'],
'Request does not have the following required fields specified: ' . implode(', ', $fields) . '.', 'Request does not have the following required fields specified: ' . implode(', ', $fields) . '.',
); );
Assert::same($this->shopClient->getLastResponse()->getStatusCode(), 400); Assert::same($this->shopClient->getLastResponse()->getStatusCode(), Response::HTTP_UNPROCESSABLE_ENTITY);
} }
/** /**

View file

@ -43,6 +43,8 @@ final class InstallerContext implements Context
'locale' => 'en_US', 'locale' => 'en_US',
'e-mail' => 'test@email.com', 'e-mail' => 'test@email.com',
'username' => 'test', 'username' => 'test',
'firstName' => '',
'lastName' => '',
'password' => 'pswd', 'password' => 'pswd',
'confirmation' => 'pswd', 'confirmation' => 'pswd',
]; ];
@ -146,6 +148,8 @@ final class InstallerContext implements Context
{ {
$this->inputChoices['e-mail'] = 'test@admin.com'; $this->inputChoices['e-mail'] = 'test@admin.com';
$this->inputChoices['username'] = 'test'; $this->inputChoices['username'] = 'test';
$this->inputChoices['firstName'] = 'John';
$this->inputChoices['lastName'] = 'Doe';
$this->inputChoices['password'] = 'pswd1$'; $this->inputChoices['password'] = 'pswd1$';
$this->inputChoices['confirmation'] = $this->inputChoices['password']; $this->inputChoices['confirmation'] = $this->inputChoices['password'];
} }

View file

@ -84,10 +84,14 @@ final readonly class ZoneContext implements Context
* @Given the store has (also) a zone :zoneName * @Given the store has (also) a zone :zoneName
* @Given the store has a zone :zoneName with code :code * @Given the store has a zone :zoneName with code :code
* @Given the store also has a zone :zoneName with code :code * @Given the store also has a zone :zoneName with code :code
* @Given the store has a zone :zoneName with code :code and priority :priority
*/ */
public function theStoreHasAZoneWithCode(string $zoneName, ?string $code = null): void public function theStoreHasAZoneWithCode(string $zoneName, ?string $code = null, ?int $priority = 0): void
{ {
$this->saveZone($this->createZone($zoneName, $code, Scope::ALL), 'zone'); $zone = $this->createZone($zoneName, $code, Scope::ALL);
$zone->setPriority($priority);
$this->saveZone($zone, 'zone');
} }
/** /**

View file

@ -14,6 +14,7 @@ declare(strict_types=1);
namespace Sylius\Behat\Context\Ui\Admin; namespace Sylius\Behat\Context\Ui\Admin;
use Behat\Behat\Context\Context; use Behat\Behat\Context\Context;
use Behat\Step\When;
use Sylius\Behat\Element\Admin\ShippingMethod\FormElementInterface; use Sylius\Behat\Element\Admin\ShippingMethod\FormElementInterface;
use Sylius\Behat\Page\Admin\ShippingMethod\CreatePageInterface; use Sylius\Behat\Page\Admin\ShippingMethod\CreatePageInterface;
use Sylius\Behat\Page\Admin\ShippingMethod\IndexPageInterface; use Sylius\Behat\Page\Admin\ShippingMethod\IndexPageInterface;
@ -124,6 +125,12 @@ final readonly class ManagingShippingMethodsContext implements Context
$this->shippingMethodFormElement->chooseCalculator($calculatorName); $this->shippingMethodFormElement->chooseCalculator($calculatorName);
} }
#[When('I fill in :label with :value')]
public function iFillInWith(string $label, string $value): void
{
$this->shippingMethodFormElement->setField($label, $value);
}
/** /**
* @When I check (also) the :shippingMethodName shipping method * @When I check (also) the :shippingMethodName shipping method
*/ */
@ -625,4 +632,12 @@ final readonly class ManagingShippingMethodsContext implements Context
{ {
Assert::same($this->shippingMethodFormElement->getValidationMessage($element), $expectedMessage); Assert::same($this->shippingMethodFormElement->getValidationMessage($element), $expectedMessage);
} }
/**
* @Then I should be notified that Maximum delivery time must be greater than or equal to the minimum.
*/
public function iShouldBeNotifiedThatMaxDeliveryIsGreaterOrEqualMin(): void
{
$this->assertFieldValidationMessage('max_delivery_time_days', 'Maximum delivery time must be greater than or equal to the minimum.');
}
} }

View file

@ -132,6 +132,14 @@ final class ManagingZonesContext implements Context
$this->formElement->selectScope($scope); $this->formElement->selectScope($scope);
} }
/**
* @When I set its priority to :priority
*/
public function iSetItsPriorityTo(int $priority): void
{
$this->formElement->prioritizeIt($priority);
}
/** /**
* @When I add it * @When I add it
* @When I try to add it * @When I try to add it
@ -338,4 +346,44 @@ final class ManagingZonesContext implements Context
throw new \InvalidArgumentException(sprintf('Member "%s" should not be selectable.', $name)); throw new \InvalidArgumentException(sprintf('Member "%s" should not be selectable.', $name));
} }
/**
* @Then the first zone on the list should have :field :value
*/
public function theFirstZoneOnTheListShouldHave(string $field, string $value): void
{
$fields = $this->indexPage->getColumnFields($field);
$actualValue = reset($fields);
Assert::same(
$actualValue,
$value,
sprintf('Expected first zone\'s %s to be "%s", but it is "%s".', $field, $value, $actualValue),
);
}
/**
* @Then the last zone on the list should have :field :value
*/
public function theLastZoneOnTheListShouldHave(string $field, string $value): void
{
$fields = $this->indexPage->getColumnFields($field);
$actualValue = end($fields);
Assert::same(
$actualValue,
$value,
sprintf('Expected last zone\'s %s to be "%s", but it is "%s".', $field, $value, $actualValue),
);
}
/**
* @Given the :zone zone should have priority :priority
*/
public function theZoneShouldHavePriority(ZoneInterface $zone, int $priority)
{
$this->iWantToModifyAZoneNamed($zone);
Assert::same($this->formElement->getPriority(), $priority);
}
} }

View file

@ -164,6 +164,8 @@ class FormElement extends BaseFormElement implements FormElementInterface
'last_rule' => '[data-test-rules] [data-test-entry-row]:last-child', 'last_rule' => '[data-test-rules] [data-test-entry-row]:last-child',
'last_rule_amount' => '[data-test-rules] [data-test-entry-row]:last-child [id$="_configuration_%channelCode%_amount"]', 'last_rule_amount' => '[data-test-rules] [data-test-entry-row]:last-child [id$="_configuration_%channelCode%_amount"]',
'last_rule_weight' => '[data-test-rules] [data-test-entry-row]:last-child [id$="_configuration_weight"]', 'last_rule_weight' => '[data-test-rules] [data-test-entry-row]:last-child [id$="_configuration_weight"]',
'max_delivery_time_days' => '#sylius_admin_shipping_method_maxDeliveryTimeDays',
'min_delivery_time_days' => '#sylius_admin_shipping_method_minDeliveryTimeDays',
'name' => '#sylius_admin_shipping_method_translations_%localeCode%_name', 'name' => '#sylius_admin_shipping_method_translations_%localeCode%_name',
'position' => '#sylius_admin_shipping_method_position', 'position' => '#sylius_admin_shipping_method_position',
'zone' => '#sylius_admin_shipping_method_zone', 'zone' => '#sylius_admin_shipping_method_zone',

View file

@ -85,7 +85,7 @@ class TreeElement extends SyliusElement implements TreeElementInterface
protected function getDefinedElements(): array protected function getDefinedElements(): array
{ {
return array_merge(parent::getDefinedElements(), [ return array_merge(parent::getDefinedElements(), [
'confirm_delete_button' => '[data-test-tree-taxons] [data-test-tree-taxon="%name%"] [data-test-delete-modal] [data-test-confirm-button]', 'confirm_delete_button' => '.modal.show[data-test-delete-modal] [data-test-confirm-button]',
'first_tree_taxon' => '[data-test-tree-taxons] [data-test-tree-taxon]:first-child', 'first_tree_taxon' => '[data-test-tree-taxons] [data-test-tree-taxon]:first-child',
'last_tree_taxon' => '[data-test-tree-taxons] [data-test-tree-taxon]:last-child', 'last_tree_taxon' => '[data-test-tree-taxons] [data-test-tree-taxon]:last-child',
'tree_taxons' => '[data-test-tree-taxons]', 'tree_taxons' => '[data-test-tree-taxons]',

View file

@ -31,6 +31,11 @@ class FormElement extends BaseFormElement implements FormElementInterface
return $this->getElement('name')->getValue(); return $this->getElement('name')->getValue();
} }
public function getPriority(): int
{
return (int) $this->getElement('priority')->getValue();
}
public function getType(): string public function getType(): string
{ {
return $this->getElement('type')->getValue(); return $this->getElement('type')->getValue();
@ -67,6 +72,11 @@ class FormElement extends BaseFormElement implements FormElementInterface
$this->waitForElement(5, 'zone_member_added'); $this->waitForElement(5, 'zone_member_added');
} }
public function prioritizeIt(int $priority): void
{
$this->getElement('priority')->setValue($priority);
}
public function removeMember(string $member): void public function removeMember(string $member): void
{ {
$this->getElement('zone_member_delete', ['%name%' => $member])->click(); $this->getElement('zone_member_delete', ['%name%' => $member])->click();
@ -100,6 +110,7 @@ class FormElement extends BaseFormElement implements FormElementInterface
'code' => '[data-test-code]', 'code' => '[data-test-code]',
'form_validation_message' => 'form > div.alert.alert-danger.d-block', 'form_validation_message' => 'form > div.alert.alert-danger.d-block',
'name' => '[data-test-name]', 'name' => '[data-test-name]',
'priority' => '#sylius_admin_zone_priority',
'scope' => '[data-test-scope]', 'scope' => '[data-test-scope]',
'type' => '[data-test-type]', 'type' => '[data-test-type]',
'zone_member' => '[data-test-zone-member]:contains("%name%")', 'zone_member' => '[data-test-zone-member]:contains("%name%")',

View file

@ -19,6 +19,8 @@ interface FormElementInterface extends BaseFormElementInterface
{ {
public function getName(): string; public function getName(): string;
public function getPriority(): int;
public function nameIt(string $name): void; public function nameIt(string $name): void;
public function getType(): string; public function getType(): string;
@ -31,6 +33,8 @@ interface FormElementInterface extends BaseFormElementInterface
public function addMember(): void; public function addMember(): void;
public function prioritizeIt(int $priority): void;
public function getScope(): string; public function getScope(): string;
public function selectScope(string $scope): void; public function selectScope(string $scope): void;

View file

@ -39,17 +39,15 @@ class IndexPage extends BaseIndexPage implements IndexPageInterface
public function archiveShippingMethod(string $name): void public function archiveShippingMethod(string $name): void
{ {
$actions = $this->getActionsForResource(['name' => $name]); $actions = $this->getActionsForResource(['name' => $name]);
$archiveRestoreModal = $actions->find('css', '[data-test-modal="archive-restore"]'); $actions->find('css', '[data-test-trigger-button="Archive"]')->press();
$archiveRestoreModal->find('css', '[data-test-trigger-button="Archive"]')->press(); $this->getDocument()->find('css', '.modal.show[data-test-modal="archive-restore"] [data-test-confirm-button]')->press();
$archiveRestoreModal->find('css', '[data-test-confirm-button]')->press();
} }
public function restoreShippingMethod(string $name): void public function restoreShippingMethod(string $name): void
{ {
$actions = $this->getActionsForResource(['name' => $name]); $actions = $this->getActionsForResource(['name' => $name]);
$archiveRestoreModal = $actions->find('css', '[data-test-modal="archive-restore"]'); $actions->find('css', '[data-test-trigger-button="Restore"]')->press();
$archiveRestoreModal->find('css', '[data-test-trigger-button="Restore"]')->press(); $this->getDocument()->find('css', '.modal.show[data-test-modal="archive-restore"] [data-test-confirm-button]')->press();
$archiveRestoreModal->find('css', '[data-test-confirm-button]')->press();
} }
public function isShippingMethodDisabled(ShippingMethodInterface $shippingMethod): bool public function isShippingMethodDisabled(ShippingMethodInterface $shippingMethod): bool

View file

@ -19,6 +19,7 @@ use Sylius\Bundle\ResourceBundle\Form\Type\AbstractResourceType;
use Sylius\Component\Addressing\Model\ZoneInterface; use Sylius\Component\Addressing\Model\ZoneInterface;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType; use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvent;
@ -46,6 +47,10 @@ final class ZoneType extends AbstractResourceType
->add('type', ZoneTypeChoiceType::class, [ ->add('type', ZoneTypeChoiceType::class, [
'disabled' => true, 'disabled' => true,
]) ])
->add('priority', IntegerType::class, [
'label' => 'sylius.form.zone.priority',
'required' => true,
])
; ;
if (!empty($this->scopeChoices)) { if (!empty($this->scopeChoices)) {

View file

@ -33,6 +33,7 @@ class ZoneRepository extends EntityRepository implements ZoneRepositoryInterface
->andWhere($queryBuilder->expr()->eq('o.type', ':type')) ->andWhere($queryBuilder->expr()->eq('o.type', ':type'))
->setParameter('type', $type) ->setParameter('type', $type)
->setMaxResults(1) ->setMaxResults(1)
->addOrderBy('o.priority', 'DESC')
; ;
return $queryBuilder->getQuery()->getOneOrNullResult(); return $queryBuilder->getQuery()->getOneOrNullResult();

View file

@ -23,6 +23,12 @@
<field name="type" column="type" type="string" length="8" /> <field name="type" column="type" type="string" length="8" />
<field name="scope" column="scope" type="string" nullable="true"/> <field name="scope" column="scope" type="string" nullable="true"/>
<field name="priority" type="integer">
<options>
<option name="default">0</option>
</options>
</field>
<one-to-many field="members" target-entity="Sylius\Component\Addressing\Model\ZoneMemberInterface" mapped-by="belongsTo" orphan-removal="true"> <one-to-many field="members" target-entity="Sylius\Component\Addressing\Model\ZoneMemberInterface" mapped-by="belongsTo" orphan-removal="true">
<cascade> <cascade>
<cascade-all/> <cascade-all/>

View file

@ -30,5 +30,6 @@ sylius:
zone: Area zone: Area
scope: Gebied scope: Gebied
select: Kies select: Kies
priority: Prioriteit
zone_member: zone_member:
select: Kies select: Kies

View file

@ -38,5 +38,6 @@ sylius:
all: الكل all: الكل
select: اختر select: اختر
select_scope: اختر النطاق select_scope: اختر النطاق
priority: الأولوية
zone_member: zone_member:
select: اختر select: اختر

View file

@ -38,5 +38,6 @@ sylius:
all: Усе all: Усе
select: Выберыце select: Выберыце
select_scope: Выбярыце сферу ўжывання select_scope: Выбярыце сферу ўжывання
priority: Прыярытэт
zone_member: zone_member:
select: Выберыце select: Выберыце

View file

@ -38,5 +38,6 @@ sylius:
all: Всички all: Всички
select: Изберете select: Изберете
select_scope: Изберете обхват select_scope: Изберете обхват
priority: Приоритет
zone_member: zone_member:
select: Изберете select: Изберете

View file

@ -38,5 +38,6 @@ sylius:
all: Totes all: Totes
select: Tria select: Tria
select_scope: Seleccioneu l'àmbit select_scope: Seleccioneu l'àmbit
priority: Prioritat
zone_member: zone_member:
select: Tria select: Tria

View file

@ -37,5 +37,6 @@ sylius:
scopes: scopes:
all: 'Všechny' all: 'Všechny'
select: 'Vybrat' select: 'Vybrat'
priority: 'Priorita'
zone_member: zone_member:
select: 'Vybrat' select: 'Vybrat'

View file

@ -38,5 +38,6 @@ sylius:
all: Alle all: Alle
select: Vælg select: Vælg
select_scope: Vælg omfang select_scope: Vælg omfang
priority: Prioritet
zone_member: zone_member:
select: Vælg select: Vælg

View file

@ -38,5 +38,6 @@ sylius:
all: 'Alle' all: 'Alle'
select: 'Zone auswählen' select: 'Zone auswählen'
select_scope: 'Geltungsbereich auswählen' select_scope: 'Geltungsbereich auswählen'
priority: 'Priorität'
zone_member: zone_member:
select: 'Mitglied auswählen' select: 'Mitglied auswählen'

View file

@ -37,5 +37,6 @@ sylius:
scopes: scopes:
all: 'Alle' all: 'Alle'
select: 'Auswählen' select: 'Auswählen'
priority: 'Priorität'
zone_member: zone_member:
select: 'Auswählen' select: 'Auswählen'

View file

@ -37,5 +37,6 @@ sylius:
scopes: scopes:
all: 'Alle' all: 'Alle'
select: 'Auswählen' select: 'Auswählen'
priority: 'Priorität'
zone_member: zone_member:
select: 'Auswählen' select: 'Auswählen'

View file

@ -38,5 +38,6 @@ sylius:
all: Όλα all: Όλα
select: Επιλέξτε select: Επιλέξτε
select_scope: Επιλέξτε πεδίο select_scope: Επιλέξτε πεδίο
priority: Προτεραιότητα
zone_member: zone_member:
select: Επιλέξτε select: Επιλέξτε

View file

@ -34,5 +34,6 @@ sylius:
scopes: scopes:
all: All all: All
select: Select select: Select
priority: Priority
zone_member: zone_member:
select: Select select: Select

View file

@ -36,5 +36,6 @@ sylius:
scopes: scopes:
all: All all: All
select: Choose select: Choose
priority: Priority
zone_member: zone_member:
select: Choose select: Choose

View file

@ -38,5 +38,6 @@ sylius:
all: Todos all: Todos
select: Seleccionar select: Seleccionar
select_scope: Seleccione ámbito select_scope: Seleccione ámbito
priority: Prioridad
zone_member: zone_member:
select: Selección select: Selección

View file

@ -38,5 +38,6 @@ sylius:
all: Kõik all: Kõik
select: Vali select: Vali
select_scope: Valige select_scope: Valige
priority: Prioriteet
zone_member: zone_member:
select: Vali select: Vali

View file

@ -38,5 +38,6 @@ sylius:
all: همه all: همه
select: انتخاب select: انتخاب
select_scope: انتخاب محدوده select_scope: انتخاب محدوده
priority: اولویت
zone_member: zone_member:
select: انتخاب select: انتخاب

View file

@ -38,5 +38,6 @@ sylius:
all: Kaikki all: Kaikki
select: Valitse select: Valitse
select_scope: Valitse alue select_scope: Valitse alue
priority: Prioriteetti
zone_member: zone_member:
select: Valitse select: Valitse

View file

@ -37,5 +37,6 @@ sylius:
scopes: scopes:
all: Tous all: Tous
select: Sélectionner select: Sélectionner
priority: Priorité
zone_member: zone_member:
select: Sélectionner select: Sélectionner

View file

@ -38,5 +38,6 @@ sylius:
all: Tout all: Tout
select: Sélectionner select: Sélectionner
select_scope: Sélectionner la portée select_scope: Sélectionner la portée
priority: Priorité
zone_member: zone_member:
select: Sélectionner select: Sélectionner

View file

@ -38,5 +38,6 @@ sylius:
all: Tous all: Tous
select: Choisir select: Choisir
select_scope: Choisissez la portée select_scope: Choisissez la portée
priority: Priorité
zone_member: zone_member:
select: Choisir select: Choisir

View file

@ -38,5 +38,6 @@ sylius:
all: הכל all: הכל
select: בחירה select: בחירה
select_scope: בחרו תחום select_scope: בחרו תחום
priority: עדיפות
zone_member: zone_member:
select: בחירה select: בחירה

View file

@ -37,5 +37,6 @@ sylius:
scopes: scopes:
all: 'सभी' all: 'सभी'
select: 'चयन करें' select: 'चयन करें'
priority: 'प्राथमिकता'
zone_member: zone_member:
select: 'चयन करें' select: 'चयन करें'

View file

@ -38,5 +38,6 @@ sylius:
all: Sve all: Sve
select: Odaberite select: Odaberite
select_scope: Odaberite doseg select_scope: Odaberite doseg
priority: Prioritet
zone_member: zone_member:
select: Odaberite select: Odaberite

View file

@ -38,5 +38,6 @@ sylius:
all: Mind all: Mind
select: Válassza ki select: Válassza ki
select_scope: Hatókör kiválasztása select_scope: Hatókör kiválasztása
priority: Prioritás
zone_member: zone_member:
select: Válassza ki select: Válassza ki

View file

@ -38,5 +38,6 @@ sylius:
all: Semua all: Semua
select: Pilih select: Pilih
select_scope: Pilih lingkup select_scope: Pilih lingkup
priority: Prioritas
zone_member: zone_member:
select: Pilih select: Pilih

View file

@ -38,5 +38,6 @@ sylius:
all: Allt all: Allt
select: Vallisti select: Vallisti
select_scope: Velja umfang select_scope: Velja umfang
priority: Forgangur
zone_member: zone_member:
select: Vallisti select: Vallisti

View file

@ -37,5 +37,6 @@ sylius:
scopes: scopes:
all: 'Tutti' all: 'Tutti'
select: 'Seleziona' select: 'Seleziona'
priority: 'Priorità'
zone_member: zone_member:
select: 'Seleziona' select: 'Seleziona'

View file

@ -37,6 +37,7 @@ sylius:
scopes: scopes:
all: 'すべて' all: 'すべて'
select: '選択' select: '選択'
priority: '優先度'
zone_member: zone_member:
select: '選択' select: '選択'

View file

@ -37,5 +37,6 @@ sylius:
scopes: scopes:
all: '전체' all: '전체'
select: '선택' select: '선택'
priority: '우선순위'
zone_member: zone_member:
select: '선택' select: '선택'

View file

@ -38,5 +38,6 @@ sylius:
all: Visi all: Visi
select: Pasirinkite select: Pasirinkite
select_scope: Pasirinkite sritį select_scope: Pasirinkite sritį
priority: Prioritetas
zone_member: zone_member:
select: Pasirinkite select: Pasirinkite

View file

@ -34,3 +34,4 @@ sylius:
scopes: scopes:
all: Visas all: Visas
select_scope: Izvēlēties tvērumu select_scope: Izvēlēties tvērumu
priority: Prioritāte

View file

@ -38,5 +38,6 @@ sylius:
all: Сите all: Сите
select: Изберете select: Изберете
select_scope: Изберете опсег select_scope: Изберете опсег
priority: Приоритет
zone_member: zone_member:
select: Изберете select: Изберете

View file

@ -38,5 +38,6 @@ sylius:
all: Бүгд all: Бүгд
select: Сонгох select: Сонгох
select_scope: Бүс сонгох select_scope: Бүс сонгох
priority: Эрэмбэ
zone_member: zone_member:
select: Сонгох select: Сонгох

View file

@ -38,5 +38,6 @@ sylius:
all: Semua all: Semua
select: Pilih select: Pilih
select_scope: Pilih skop select_scope: Pilih skop
priority: Keutamaan
zone_member: zone_member:
select: Pilih select: Pilih

View file

@ -37,5 +37,6 @@ sylius:
scopes: scopes:
all: 'Alle' all: 'Alle'
select: 'Selecteer' select: 'Selecteer'
priority: 'Prioriteit'
zone_member: zone_member:
select: 'Selecteer' select: 'Selecteer'

View file

@ -37,6 +37,7 @@ sylius:
scopes: scopes:
all: 'Alle' all: 'Alle'
select: 'Velg' select: 'Velg'
priority: 'Prioritet'
zone_member: zone_member:
select: 'Velg' select: 'Velg'

View file

@ -37,5 +37,6 @@ sylius:
scopes: scopes:
all: Wszystko all: Wszystko
select: Wybierz select: Wybierz
priority: Priorytet
zone_member: zone_member:
select: Wybierz select: Wybierz

View file

@ -37,6 +37,7 @@ sylius:
scopes: scopes:
all: 'Todos' all: 'Todos'
select: 'Selecionar' select: 'Selecionar'
priority: 'Prioridade'
zone_member: zone_member:
select: 'Selecionar' select: 'Selecionar'

View file

@ -38,5 +38,6 @@ sylius:
all: Todos(as) all: Todos(as)
select: Selecione select: Selecione
select_scope: Selecione o escopo select_scope: Selecione o escopo
priority: Prioridade
zone_member: zone_member:
select: Selecione select: Selecione

View file

@ -37,5 +37,6 @@ sylius:
scopes: scopes:
all: 'Toate' all: 'Toate'
select: 'Selectați' select: 'Selectați'
priority: 'Prioritate'
zone_member: zone_member:
select: 'Selectați' select: 'Selectați'

View file

@ -38,5 +38,6 @@ sylius:
all: Все all: Все
select: Выбрать select: Выбрать
select_scope: Выберите область select_scope: Выберите область
priority: Приоритет
zone_member: zone_member:
select: Выбрать select: Выбрать

View file

@ -37,5 +37,6 @@ sylius:
scopes: scopes:
all: 'Všetko' all: 'Všetko'
select: 'Vybrať' select: 'Vybrať'
priority: 'Priorita'
zone_member: zone_member:
select: 'Vybrať' select: 'Vybrať'

View file

@ -36,5 +36,6 @@ sylius:
scope: Področje scope: Področje
select: Izberite select: Izberite
select_scope: Izberite področje select_scope: Izberite področje
priority: Prednost
zone_member: zone_member:
select: Izberite select: Izberite

View file

@ -37,5 +37,6 @@ sylius:
all: Të gjitha all: Të gjitha
select: Zgjidh select: Zgjidh
select_scope: Zgjidh zonën select_scope: Zgjidh zonën
priority: Prioriteti
zone_member: zone_member:
select: Zgjidh select: Zgjidh

View file

@ -35,5 +35,6 @@ sylius:
scope: Обухват scope: Обухват
select: Изаберите select: Изаберите
select_scope: Изабери обухват select_scope: Изабери обухват
priority: Приоритет
zone_member: zone_member:
select: Изаберите select: Изаберите

View file

@ -38,5 +38,6 @@ sylius:
all: Sve all: Sve
select: Izaberite select: Izaberite
select_scope: Izaberite adresu isporuke select_scope: Izaberite adresu isporuke
priority: Prioritet
zone_member: zone_member:
select: Izaberite select: Izaberite

View file

@ -37,5 +37,6 @@ sylius:
scopes: scopes:
all: Alla all: Alla
select: Välj select: Välj
priority: Prioritet
zone_member: zone_member:
select: Välj select: Välj

View file

@ -38,5 +38,6 @@ sylius:
all: ทั้งหมด all: ทั้งหมด
select: เลือก select: เลือก
select_scope: เลือกขอบเขต select_scope: เลือกขอบเขต
priority: ลำดับความสำคัญ
zone_member: zone_member:
select: เลือก select: เลือก

View file

@ -38,5 +38,6 @@ sylius:
all: Tümü all: Tümü
select: Seçiniz select: Seçiniz
select_scope: Ölçek seçiniz select_scope: Ölçek seçiniz
priority: Öncelik
zone_member: zone_member:
select: Seçiniz select: Seçiniz

View file

@ -38,5 +38,6 @@ sylius:
all: Всі(-е) all: Всі(-е)
select: Виберіть select: Виберіть
select_scope: Виберіть покриття select_scope: Виберіть покриття
priority: Пріоритет
zone_member: zone_member:
select: Виберіть select: Виберіть

View file

@ -38,5 +38,6 @@ sylius:
all: Tất cả all: Tất cả
select: Chọn select: Chọn
select_scope: Chọn phạm vi select_scope: Chọn phạm vi
priority: Ưu tiên
zone_member: zone_member:
select: Chọn select: Chọn

View file

@ -37,5 +37,6 @@ sylius:
scopes: scopes:
all: '所有' all: '所有'
select: '选择' select: '选择'
priority: '优先级'
zone_member: zone_member:
select: '选择' select: '选择'

View file

@ -37,5 +37,6 @@ sylius:
all: 全部 all: 全部
select: 請選擇 select: 請選擇
select_scope: 選擇範圍 select_scope: 選擇範圍
priority: 優先順序
zone_member: zone_member:
select: 請選擇 select: 請選擇

View file

@ -30,8 +30,8 @@
"stof/doctrine-extensions-bundle": "^1.12", "stof/doctrine-extensions-bundle": "^1.12",
"sylius/addressing": "^2.0", "sylius/addressing": "^2.0",
"sylius/resource-bundle": "^1.12", "sylius/resource-bundle": "^1.12",
"symfony/framework-bundle": "^6.4.1 || ^7.2", "symfony/framework-bundle": "^6.4.1 || ^7.4",
"symfony/intl": "^6.4 || ^7.2" "symfony/intl": "^6.4 || ^7.4"
}, },
"conflict": { "conflict": {
"phpstan/phpdoc-parser": ">= 2.0" "phpstan/phpdoc-parser": ">= 2.0"
@ -39,14 +39,13 @@
"require-dev": { "require-dev": {
"doctrine/data-fixtures": "^1.7", "doctrine/data-fixtures": "^1.7",
"doctrine/doctrine-bundle": "^2.13", "doctrine/doctrine-bundle": "^2.13",
"doctrine/orm": "^2.18 || ^3.3", "doctrine/orm": "^2.18 || ^3.5",
"matthiasnoback/symfony-dependency-injection-test": "^6.0", "matthiasnoback/symfony-dependency-injection-test": "^6.0",
"phpspec/prophecy-phpunit": "^2.2",
"phpunit/phpunit": "^11.5", "phpunit/phpunit": "^11.5",
"symfony/browser-kit": "^6.4 || ^7.2", "symfony/browser-kit": "^6.4 || ^7.4",
"symfony/dependency-injection": "^6.4.1 || ^7.2", "symfony/dependency-injection": "^6.4.1 || ^7.4",
"symfony/form": "^6.4.1 || ^7.2", "symfony/form": "^6.4.1 || ^7.4",
"symfony/validator": "^6.4 || ^7.2", "symfony/validator": "^6.4 || ^7.4",
"theofidry/alice-data-fixtures": "^1.7" "theofidry/alice-data-fixtures": "^1.7"
}, },
"config": { "config": {
@ -56,10 +55,10 @@
}, },
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-main": "2.0-dev" "dev-main": "2.2-dev"
}, },
"symfony": { "symfony": {
"require": "^7.2" "require": "^7.4"
} }
}, },
"autoload": { "autoload": {

View file

@ -15,9 +15,7 @@ namespace Tests\Sylius\Bundle\AddressingBundle\Form\Type;
use PHPUnit\Framework\Assert; use PHPUnit\Framework\Assert;
use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\Test;
use Prophecy\PhpUnit\ProphecyTrait; use PHPUnit\Framework\MockObject\MockObject;
use Prophecy\Prophecy\ObjectProphecy;
use Prophecy\Prophecy\ProphecyInterface;
use Sylius\Bundle\AddressingBundle\Form\Type\CountryChoiceType; use Sylius\Bundle\AddressingBundle\Form\Type\CountryChoiceType;
use Sylius\Component\Addressing\Model\CountryInterface; use Sylius\Component\Addressing\Model\CountryInterface;
use Sylius\Resource\Doctrine\Persistence\RepositoryInterface; use Sylius\Resource\Doctrine\Persistence\RepositoryInterface;
@ -27,48 +25,37 @@ use Symfony\Component\Form\Test\TypeTestCase;
final class CountryChoiceTypeTest extends TypeTestCase final class CountryChoiceTypeTest extends TypeTestCase
{ {
use ProphecyTrait; private MockObject&RepositoryInterface $countryRepository;
private ObjectProphecy $countryRepository; private CountryInterface&MockObject $france;
/** @var ProphecyInterface|CountryInterface */ private CountryInterface&MockObject $poland;
private $france;
/** @var ProphecyInterface|CountryInterface */ private CountryInterface&MockObject $austria;
private $poland;
/** @var ProphecyInterface|CountryInterface */
private $austria;
protected function setUp(): void protected function setUp(): void
{ {
$this->countryRepository = $this->prophesize(RepositoryInterface::class); $this->countryRepository = $this->createMock(RepositoryInterface::class);
/** @var ProphecyInterface|CountryInterface $france */ $this->france = $this->createMock(CountryInterface::class);
$france = $this->prophesize(CountryInterface::class); $this->france->method('getCode')->willReturn('FR');
$france->getCode()->willReturn('FR'); $this->france->method('getName')->willReturn('France');
$france->getName()->willReturn('France');
$this->france = $france;
/** @var ProphecyInterface|CountryInterface $poland */ $this->poland = $this->createMock(CountryInterface::class);
$poland = $this->prophesize(CountryInterface::class); $this->poland->method('getCode')->willReturn('PL');
$poland->getCode()->willReturn('PL'); $this->poland->method('getName')->willReturn('Poland');
$poland->getName()->willReturn('Poland');
$this->poland = $poland;
/** @var ProphecyInterface|CountryInterface $austria */ $this->austria = $this->createMock(CountryInterface::class);
$austria = $this->prophesize(CountryInterface::class); $this->austria->method('getCode')->willReturn('AT');
$austria->getCode()->willReturn('AT'); $this->austria->method('getName')->willReturn('Austria');
$austria->getName()->willReturn('Austria'); $this->austria->method('isEnabled')->willReturn(false);
$austria->isEnabled()->willReturn(false);
$this->austria = $austria;
parent::setUp(); parent::setUp();
} }
protected function getExtensions(): array protected function getExtensions(): array
{ {
$type = new CountryChoiceType($this->countryRepository->reveal()); $type = new CountryChoiceType($this->countryRepository);
return [ return [
new PreloadedExtension([$type], []), new PreloadedExtension([$type], []),
@ -78,9 +65,13 @@ final class CountryChoiceTypeTest extends TypeTestCase
#[Test] #[Test]
public function it_returns_only_enabled_countries_by_default(): void public function it_returns_only_enabled_countries_by_default(): void
{ {
$this->countryRepository->findBy(['enabled' => true])->willReturn([ $this->countryRepository
$this->france->reveal(), ->expects($this->once())
$this->poland->reveal(), ->method('findBy')
->with(['enabled' => true])
->willReturn([
$this->france,
$this->poland,
]); ]);
$this->assertChoicesLabels(['France', 'Poland']); $this->assertChoicesLabels(['France', 'Poland']);
@ -89,10 +80,13 @@ final class CountryChoiceTypeTest extends TypeTestCase
#[Test] #[Test]
public function it_returns_all_countries_when_option_enabled_is_false(): void public function it_returns_all_countries_when_option_enabled_is_false(): void
{ {
$this->countryRepository->findAll()->willReturn([ $this->countryRepository
$this->france->reveal(), ->expects($this->once())
$this->poland->reveal(), ->method('findAll')
$this->austria->reveal(), ->willReturn([
$this->france,
$this->poland,
$this->austria,
]); ]);
$this->assertChoicesLabels(['Austria', 'France', 'Poland'], ['enabled' => false]); $this->assertChoicesLabels(['Austria', 'France', 'Poland'], ['enabled' => false]);
@ -101,9 +95,13 @@ final class CountryChoiceTypeTest extends TypeTestCase
#[Test] #[Test]
public function it_returns_enabled_countries_in_an_alphabetical_order(): void public function it_returns_enabled_countries_in_an_alphabetical_order(): void
{ {
$this->countryRepository->findBy(['enabled' => true])->willReturn([ $this->countryRepository
$this->poland->reveal(), ->expects($this->once())
$this->france->reveal(), ->method('findBy')
->with(['enabled' => true])
->willReturn([
$this->poland,
$this->france,
]); ]);
$this->assertChoicesLabels(['France', 'Poland']); $this->assertChoicesLabels(['France', 'Poland']);
@ -112,10 +110,13 @@ final class CountryChoiceTypeTest extends TypeTestCase
#[Test] #[Test]
public function it_returns_all_countries_in_an_alphabetical_order(): void public function it_returns_all_countries_in_an_alphabetical_order(): void
{ {
$this->countryRepository->findAll()->willReturn([ $this->countryRepository
$this->poland->reveal(), ->expects($this->once())
$this->france->reveal(), ->method('findAll')
$this->austria->reveal(), ->willReturn([
$this->poland,
$this->france,
$this->austria,
]); ]);
$this->assertChoicesLabels(['Austria', 'France', 'Poland'], ['enabled' => false]); $this->assertChoicesLabels(['Austria', 'France', 'Poland'], ['enabled' => false]);
@ -124,10 +125,13 @@ final class CountryChoiceTypeTest extends TypeTestCase
#[Test] #[Test]
public function it_returns_all_filtered_out_countries(): void public function it_returns_all_filtered_out_countries(): void
{ {
$this->countryRepository->findAll()->willReturn([ $this->countryRepository
$this->france->reveal(), ->expects($this->once())
$this->poland->reveal(), ->method('findAll')
$this->austria->reveal(), ->willReturn([
$this->france,
$this->poland,
$this->austria,
]); ]);
$this->assertChoicesLabels(['Poland'], ['choice_filter' => static fn (?CountryInterface $country): bool => $country !== null && $country->getName() === 'Poland', 'enabled' => false]); $this->assertChoicesLabels(['Poland'], ['choice_filter' => static fn (?CountryInterface $country): bool => $country !== null && $country->getName() === 'Poland', 'enabled' => false]);
@ -136,9 +140,13 @@ final class CountryChoiceTypeTest extends TypeTestCase
#[Test] #[Test]
public function it_returns_enabled_filtered_out_countries(): void public function it_returns_enabled_filtered_out_countries(): void
{ {
$this->countryRepository->findBy(['enabled' => true])->willReturn([ $this->countryRepository
$this->france->reveal(), ->expects($this->once())
$this->poland->reveal(), ->method('findBy')
->with(['enabled' => true])
->willReturn([
$this->france,
$this->poland,
]); ]);
$this->assertChoicesLabels(['Poland'], ['choice_filter' => static fn (?CountryInterface $country): bool => $country !== null && $country->getName() === 'Poland']); $this->assertChoicesLabels(['Poland'], ['choice_filter' => static fn (?CountryInterface $country): bool => $country !== null && $country->getName() === 'Poland']);

View file

@ -15,9 +15,7 @@ namespace Tests\Sylius\Bundle\AddressingBundle\Form\Type;
use PHPUnit\Framework\Assert; use PHPUnit\Framework\Assert;
use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\Test;
use Prophecy\PhpUnit\ProphecyTrait; use PHPUnit\Framework\MockObject\MockObject;
use Prophecy\Prophecy\ObjectProphecy;
use Prophecy\Prophecy\ProphecyInterface;
use Sylius\Bundle\AddressingBundle\Form\Type\ZoneChoiceType; use Sylius\Bundle\AddressingBundle\Form\Type\ZoneChoiceType;
use Sylius\Component\Addressing\Model\Scope as AddressingScope; use Sylius\Component\Addressing\Model\Scope as AddressingScope;
use Sylius\Component\Addressing\Model\ZoneInterface; use Sylius\Component\Addressing\Model\ZoneInterface;
@ -28,39 +26,31 @@ use Symfony\Component\Form\Test\TypeTestCase;
final class ZoneChoiceTypeTest extends TypeTestCase final class ZoneChoiceTypeTest extends TypeTestCase
{ {
use ProphecyTrait; private MockObject&RepositoryInterface $zoneRepository;
private ObjectProphecy $zoneRepository; private MockObject&ZoneInterface $zoneAllScopes;
/** @var ProphecyInterface|ZoneInterface */ private MockObject&ZoneInterface $zoneTaxScope;
private $zoneAllScopes;
/** @var ProphecyInterface|ZoneInterface */ private MockObject&ZoneInterface $zoneShippingScope;
private $zoneTaxScope;
/** @var ProphecyInterface|ZoneInterface */
private $zoneShippingScope;
protected function setUp(): void protected function setUp(): void
{ {
$this->zoneRepository = $this->prophesize(RepositoryInterface::class); $this->zoneRepository = $this->createMock(RepositoryInterface::class);
/** @var ProphecyInterface|ZoneInterface $zoneAllScopes */ $zoneAllScopes = $this->createMock(ZoneInterface::class);
$zoneAllScopes = $this->prophesize(ZoneInterface::class); $zoneAllScopes->method('getCode')->willReturn('all');
$zoneAllScopes->getCode()->willReturn('all'); $zoneAllScopes->method('getName')->willReturn('All');
$zoneAllScopes->getName()->willReturn('All');
$this->zoneAllScopes = $zoneAllScopes; $this->zoneAllScopes = $zoneAllScopes;
/** @var ProphecyInterface|ZoneInterface $zoneTaxScope */ $zoneTaxScope = $this->createMock(ZoneInterface::class);
$zoneTaxScope = $this->prophesize(ZoneInterface::class); $zoneTaxScope->method('getCode')->willReturn('tax');
$zoneTaxScope->getCode()->willReturn('tax'); $zoneTaxScope->method('getName')->willReturn('Tax');
$zoneTaxScope->getName()->willReturn('Tax');
$this->zoneTaxScope = $zoneTaxScope; $this->zoneTaxScope = $zoneTaxScope;
/** @var ProphecyInterface|ZoneInterface $zoneShippingScope */ $zoneShippingScope = $this->createMock(ZoneInterface::class);
$zoneShippingScope = $this->prophesize(ZoneInterface::class); $zoneShippingScope->method('getCode')->willReturn('shipping');
$zoneShippingScope->getCode()->willReturn('shipping'); $zoneShippingScope->method('getName')->willReturn('Shipping');
$zoneShippingScope->getName()->willReturn('Shipping');
$this->zoneShippingScope = $zoneShippingScope; $this->zoneShippingScope = $zoneShippingScope;
parent::setUp(); parent::setUp();
@ -74,7 +64,7 @@ final class ZoneChoiceTypeTest extends TypeTestCase
'shipping' => 'Shipping', 'shipping' => 'Shipping',
]; ];
$type = new ZoneChoiceType($this->zoneRepository->reveal(), $scopeTypes); $type = new ZoneChoiceType($this->zoneRepository, $scopeTypes);
return [ return [
new PreloadedExtension([$type], []), new PreloadedExtension([$type], []),
@ -84,10 +74,10 @@ final class ZoneChoiceTypeTest extends TypeTestCase
#[Test] #[Test]
public function it_returns_all_scopes_by_default(): void public function it_returns_all_scopes_by_default(): void
{ {
$this->zoneRepository->findBy([])->willReturn([ $this->zoneRepository->method('findBy')->with([])->willReturn([
$this->zoneAllScopes->reveal(), $this->zoneAllScopes,
$this->zoneTaxScope->reveal(), $this->zoneTaxScope,
$this->zoneShippingScope->reveal(), $this->zoneShippingScope,
]); ]);
$this->assertChoicesLabels(['All', 'Tax', 'Shipping']); $this->assertChoicesLabels(['All', 'Tax', 'Shipping']);
@ -96,10 +86,10 @@ final class ZoneChoiceTypeTest extends TypeTestCase
#[Test] #[Test]
public function it_returns_all_scopes_when_zone_scope_set_to_all(): void public function it_returns_all_scopes_when_zone_scope_set_to_all(): void
{ {
$this->zoneRepository->findBy([])->willReturn([ $this->zoneRepository->method('findBy')->with([])->willReturn([
$this->zoneAllScopes->reveal(), $this->zoneAllScopes,
$this->zoneTaxScope->reveal(), $this->zoneTaxScope,
$this->zoneShippingScope->reveal(), $this->zoneShippingScope,
]); ]);
$this->assertChoicesLabels(['All', 'Tax', 'Shipping'], ['zone_scope' => AddressingScope::ALL]); $this->assertChoicesLabels(['All', 'Tax', 'Shipping'], ['zone_scope' => AddressingScope::ALL]);
@ -108,9 +98,9 @@ final class ZoneChoiceTypeTest extends TypeTestCase
#[Test] #[Test]
public function it_returns_tax_scopes_when_zone_scope_set_to_tax(): void public function it_returns_tax_scopes_when_zone_scope_set_to_tax(): void
{ {
$this->zoneRepository->findBy(['scope' => ['tax', AddressingScope::ALL]])->willReturn([ $this->zoneRepository->method('findBy')->with(['scope' => ['tax', AddressingScope::ALL]])->willReturn([
$this->zoneAllScopes->reveal(), $this->zoneAllScopes,
$this->zoneTaxScope->reveal(), $this->zoneTaxScope,
]); ]);
$this->assertChoicesLabels(['All', 'Tax'], ['zone_scope' => 'tax']); $this->assertChoicesLabels(['All', 'Tax'], ['zone_scope' => 'tax']);
@ -119,9 +109,9 @@ final class ZoneChoiceTypeTest extends TypeTestCase
#[Test] #[Test]
public function it_returns_shipping_scopes_when_zone_scope_set_to_shipping(): void public function it_returns_shipping_scopes_when_zone_scope_set_to_shipping(): void
{ {
$this->zoneRepository->findBy(['scope' => ['shipping', AddressingScope::ALL]])->willReturn([ $this->zoneRepository->method('findBy')->with(['scope' => ['shipping', AddressingScope::ALL]])->willReturn([
$this->zoneAllScopes->reveal(), $this->zoneAllScopes,
$this->zoneShippingScope->reveal(), $this->zoneShippingScope,
]); ]);
$this->assertChoicesLabels(['All', 'Shipping'], ['zone_scope' => 'shipping']); $this->assertChoicesLabels(['All', 'Shipping'], ['zone_scope' => 'shipping']);

View file

@ -28,7 +28,7 @@ final class RemoveAvatarAction
public function __construct( public function __construct(
private AvatarImageRepositoryInterface $avatarRepository, private AvatarImageRepositoryInterface $avatarRepository,
private RouterInterface $router, private RouterInterface $router,
private CsrfTokenManagerInterface $csrfTokenManager, private ?CsrfTokenManagerInterface $csrfTokenManager,
) { ) {
} }
@ -36,7 +36,7 @@ final class RemoveAvatarAction
{ {
$userId = $request->attributes->get('id', ''); $userId = $request->attributes->get('id', '');
if (!$this->csrfTokenManager->isTokenValid( if ($this->csrfTokenManager && !$this->csrfTokenManager->isTokenValid(
new CsrfToken($userId, (string) $request->query->get('_csrf_token', '')), new CsrfToken($userId, (string) $request->query->get('_csrf_token', '')),
)) { )) {
throw new HttpException(Response::HTTP_FORBIDDEN, 'Invalid csrf token.'); throw new HttpException(Response::HTTP_FORBIDDEN, 'Invalid csrf token.');

View file

@ -32,7 +32,7 @@ final readonly class ResendOrderConfirmationEmailAction
public function __construct( public function __construct(
private OrderRepositoryInterface $orderRepository, private OrderRepositoryInterface $orderRepository,
private ResendOrderConfirmationEmailDispatcherInterface $resendOrderConfirmationEmailDispatcher, private ResendOrderConfirmationEmailDispatcherInterface $resendOrderConfirmationEmailDispatcher,
private CsrfTokenManagerInterface $csrfTokenManager, private ?CsrfTokenManagerInterface $csrfTokenManager,
private RequestStack $requestStack, private RequestStack $requestStack,
private RouterInterface $router, private RouterInterface $router,
) { ) {
@ -42,7 +42,7 @@ final readonly class ResendOrderConfirmationEmailAction
{ {
$orderId = $request->attributes->get('id', ''); $orderId = $request->attributes->get('id', '');
if (!$this->csrfTokenManager->isTokenValid( if ($this->csrfTokenManager && !$this->csrfTokenManager->isTokenValid(
new CsrfToken($orderId, (string) $request->query->get('_csrf_token', '')), new CsrfToken($orderId, (string) $request->query->get('_csrf_token', '')),
)) { )) {
throw new HttpException(Response::HTTP_FORBIDDEN, 'Invalid csrf token.'); throw new HttpException(Response::HTTP_FORBIDDEN, 'Invalid csrf token.');

View file

@ -31,7 +31,7 @@ final readonly class ResendShipmentConfirmationEmailAction
public function __construct( public function __construct(
private ShipmentRepositoryInterface $shipmentRepository, private ShipmentRepositoryInterface $shipmentRepository,
private ResendShipmentConfirmationEmailDispatcherInterface $resendShipmentConfirmationDispatcher, private ResendShipmentConfirmationEmailDispatcherInterface $resendShipmentConfirmationDispatcher,
private CsrfTokenManagerInterface $csrfTokenManager, private ?CsrfTokenManagerInterface $csrfTokenManager,
private RequestStack $requestStack, private RequestStack $requestStack,
) { ) {
} }
@ -40,7 +40,7 @@ final readonly class ResendShipmentConfirmationEmailAction
{ {
$shipmentId = $request->attributes->get('id', ''); $shipmentId = $request->attributes->get('id', '');
if (!$this->csrfTokenManager->isTokenValid( if ($this->csrfTokenManager && !$this->csrfTokenManager->isTokenValid(
new CsrfToken($shipmentId, (string) $request->query->get('_csrf_token', '')), new CsrfToken($shipmentId, (string) $request->query->get('_csrf_token', '')),
)) { )) {
throw new HttpException(Response::HTTP_FORBIDDEN, 'Invalid csrf token.'); throw new HttpException(Response::HTTP_FORBIDDEN, 'Invalid csrf token.');

View file

@ -0,0 +1,77 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\AdminBundle\Console\Command;
use Sylius\Component\Core\Model\AdminUserInterface;
use Sylius\Component\User\Repository\UserRepositoryInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'sylius:admin-user:delete',
description: 'Deletes an admin user account by the given email',
)]
final class DeleteAdminUserCommand extends Command
{
protected SymfonyStyle $io;
/** @param UserRepositoryInterface<AdminUserInterface> $userRepository */
public function __construct(
private readonly UserRepositoryInterface $userRepository,
) {
parent::__construct();
}
protected function configure(): void
{
$this
->setDescription('Deletes an admin user')
->addArgument('email', InputArgument::REQUIRED, 'The email of the admin user to delete');
}
protected function initialize(InputInterface $input, OutputInterface $output): void
{
$this->io = new SymfonyStyle($input, $output);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->io->title('Delete admin user');
$email = $input->getArgument('email');
$adminUser = $this->userRepository->findOneBy(['email' => $email]);
if ($adminUser === null) {
$this->io->error(sprintf('Admin Account with the email "%s" does not exist', $email));
return Command::FAILURE;
}
if ($input->isInteractive() && !$this->io->confirm(
sprintf('Are you sure you want to delete the admin user "%s"?', $email),
false,
)) {
return Command::FAILURE;
}
$this->userRepository->remove($adminUser);
$this->io->success(sprintf('Admin Account with the email "%s" has been deleted successfully', $email));
return Command::SUCCESS;
}
}

View file

@ -0,0 +1,145 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Bundle\AdminBundle\Console\Command;
use Doctrine\ORM\EntityManagerInterface;
use Sylius\Component\Core\Model\AdminUserInterface;
use Sylius\Component\User\Repository\UserRepositoryInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'sylius:admin-user:list',
description: 'Displays a list of all admin users along with their details.',
)]
final class ListAdminUsersCommand extends Command
{
protected SymfonyStyle $io;
/** @param UserRepositoryInterface<AdminUserInterface> $adminUserRepository */
public function __construct(
private readonly UserRepositoryInterface $adminUserRepository,
private readonly EntityManagerInterface $entityManager,
) {
parent::__construct();
}
protected function initialize(InputInterface $input, OutputInterface $output): void
{
$this->io = new SymfonyStyle($input, $output);
}
protected function configure(): void
{
$this->setAliases(['sylius:admin-users:list']);
$this->addOption(
'search',
's',
InputOption::VALUE_OPTIONAL,
'Filter users based on a search term. If not provided, all users will be displayed.',
);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->io->title('List available admin users');
$searchTerm = $input->getOption('search');
if ($searchTerm === null || $searchTerm === '') {
$this->renderUsersTable($output, $this->adminUserRepository->findAll());
return Command::SUCCESS;
}
$users = $this->searchUsers($searchTerm);
if (empty($users)) {
$this->io->warning(sprintf('No users found matching "%s"', $searchTerm));
return Command::SUCCESS;
}
$this->io->success(sprintf('Found %d user(s) matching "%s"', count($users), $searchTerm));
$this->renderUsersTable($output, $users);
return Command::SUCCESS;
}
/**
* @return array<AdminUserInterface>
*/
private function searchUsers(string $searchTerm): array
{
$criteria = '%' . mb_strtolower($searchTerm) . '%';
return $this->entityManager->createQueryBuilder()
->from(AdminUserInterface::class, 'u')
->select('u')
->where('LOWER(u.email) LIKE :criteria
OR LOWER(u.username) LIKE :criteria
OR LOWER(u.firstName) LIKE :criteria
OR LOWER(u.lastName) LIKE :criteria')
->setParameter('criteria', $criteria)
->orderBy('u.createdAt', 'DESC')
->getQuery()
->getResult();
}
/**
* @param array<AdminUserInterface> $adminUsers
*/
private function renderUsersTable(OutputInterface $output, array $adminUsers): void
{
$table = new Table($output);
$table->setHeaders([
'ID', 'E-Mail', 'Username', 'First name', 'Last name', 'Locale', 'Enabled', 'Roles',
]);
foreach ($adminUsers as $adminUser) {
$table->addRow([
$adminUser->getId(),
$adminUser->getEmail(),
$adminUser->getUsername(),
$adminUser->getFirstName() ?? '',
$adminUser->getLastName() ?? '',
$adminUser->getLocaleCode(),
$adminUser->isEnabled() ? '✔' : '✘',
$this->formatRoles($adminUser->getRoles()),
]);
}
$table->render();
}
/**
* @param array<string> $roles
*/
private function formatRoles(array $roles): string
{
if ($roles === []) {
return 'No roles';
}
$formatted = array_map(
static fn (string $role): string => str_replace('ROLE_', '', $role),
$roles,
);
return implode(",\n", $formatted);
}
}

Some files were not shown because too many files have changed in this diff Show more