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"
needs: [static-checks, e2e-mariadb, e2e-mysql, e2e-pgsql, frontend, packages]
runs-on: ubuntu-latest
timeout-minutes: 5
timeout-minutes: 15
steps:
- name: "Process data"

View file

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

View file

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

View file

@ -45,7 +45,7 @@ jobs:
needs: get-matrix
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) || '' }}"
timeout-minutes: 45
timeout-minutes: 15
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.get-matrix.outputs.matrix) }}
@ -158,7 +158,7 @@ jobs:
needs: get-matrix
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) || '' }}"
timeout-minutes: 45
timeout-minutes: 15
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.get-matrix.outputs.matrix) }}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -67,3 +67,13 @@ Feature: Editing a zone
And I save my changes
Then I should be notified that it has been successfully edited
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": {
"php": "^8.2",
"symfony/dependency-injection": "^6.4.1 || ^7.2",
"symfony/http-kernel": "^6.4.1 || ^7.2",
"symfony/workflow": "^6.4 || ^7.2"
"symfony/dependency-injection": "^6.4.1 || ^7.4",
"symfony/http-kernel": "^6.4.1 || ^7.4",
"symfony/workflow": "^6.4 || ^7.4"
},
"suggest": {
"twig/twig": "Access the state machine in your twig templates (^2.12|^3.3)",
@ -37,10 +37,10 @@
},
"extra": {
"branch-alias": {
"dev-main": "2.0-dev"
"dev-main": "2.2-dev"
},
"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 appendError(Response $response): ResponseCheckerInterface;
public function cleanErrors(): void;
/** @return array{

View file

@ -129,6 +129,14 @@ final readonly class ManagingZonesContext implements Context
$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
*/
@ -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
{
$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\Component\Core\Model\CustomerInterface;
use Symfony\Component\HttpFoundation\Request as HttpRequest;
use Symfony\Component\HttpFoundation\Response;
use Webmozart\Assert\Assert;
final class RegistrationContext implements Context
@ -206,7 +207,7 @@ final class RegistrationContext implements Context
$content['hydra:description'],
'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',
'e-mail' => 'test@email.com',
'username' => 'test',
'firstName' => '',
'lastName' => '',
'password' => 'pswd',
'confirmation' => 'pswd',
];
@ -146,6 +148,8 @@ final class InstallerContext implements Context
{
$this->inputChoices['e-mail'] = 'test@admin.com';
$this->inputChoices['username'] = 'test';
$this->inputChoices['firstName'] = 'John';
$this->inputChoices['lastName'] = 'Doe';
$this->inputChoices['password'] = 'pswd1$';
$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 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;
use Behat\Behat\Context\Context;
use Behat\Step\When;
use Sylius\Behat\Element\Admin\ShippingMethod\FormElementInterface;
use Sylius\Behat\Page\Admin\ShippingMethod\CreatePageInterface;
use Sylius\Behat\Page\Admin\ShippingMethod\IndexPageInterface;
@ -124,6 +125,12 @@ final readonly class ManagingShippingMethodsContext implements Context
$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
*/
@ -625,4 +632,12 @@ final readonly class ManagingShippingMethodsContext implements Context
{
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);
}
/**
* @When I set its priority to :priority
*/
public function iSetItsPriorityTo(int $priority): void
{
$this->formElement->prioritizeIt($priority);
}
/**
* @When I 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));
}
/**
* @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_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"]',
'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',
'position' => '#sylius_admin_shipping_method_position',
'zone' => '#sylius_admin_shipping_method_zone',

View file

@ -85,7 +85,7 @@ class TreeElement extends SyliusElement implements TreeElementInterface
protected function getDefinedElements(): array
{
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',
'last_tree_taxon' => '[data-test-tree-taxons] [data-test-tree-taxon]:last-child',
'tree_taxons' => '[data-test-tree-taxons]',

View file

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

View file

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

View file

@ -39,17 +39,15 @@ class IndexPage extends BaseIndexPage implements IndexPageInterface
public function archiveShippingMethod(string $name): void
{
$actions = $this->getActionsForResource(['name' => $name]);
$archiveRestoreModal = $actions->find('css', '[data-test-modal="archive-restore"]');
$archiveRestoreModal->find('css', '[data-test-trigger-button="Archive"]')->press();
$archiveRestoreModal->find('css', '[data-test-confirm-button]')->press();
$actions->find('css', '[data-test-trigger-button="Archive"]')->press();
$this->getDocument()->find('css', '.modal.show[data-test-modal="archive-restore"] [data-test-confirm-button]')->press();
}
public function restoreShippingMethod(string $name): void
{
$actions = $this->getActionsForResource(['name' => $name]);
$archiveRestoreModal = $actions->find('css', '[data-test-modal="archive-restore"]');
$archiveRestoreModal->find('css', '[data-test-trigger-button="Restore"]')->press();
$archiveRestoreModal->find('css', '[data-test-confirm-button]')->press();
$actions->find('css', '[data-test-trigger-button="Restore"]')->press();
$this->getDocument()->find('css', '.modal.show[data-test-modal="archive-restore"] [data-test-confirm-button]')->press();
}
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 Symfony\Component\Form\Extension\Core\Type\ChoiceType;
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\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
@ -46,6 +47,10 @@ final class ZoneType extends AbstractResourceType
->add('type', ZoneTypeChoiceType::class, [
'disabled' => true,
])
->add('priority', IntegerType::class, [
'label' => 'sylius.form.zone.priority',
'required' => true,
])
;
if (!empty($this->scopeChoices)) {

View file

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

View file

@ -23,6 +23,12 @@
<field name="type" column="type" type="string" length="8" />
<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">
<cascade>
<cascade-all/>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -31,7 +31,7 @@ final readonly class ResendShipmentConfirmationEmailAction
public function __construct(
private ShipmentRepositoryInterface $shipmentRepository,
private ResendShipmentConfirmationEmailDispatcherInterface $resendShipmentConfirmationDispatcher,
private CsrfTokenManagerInterface $csrfTokenManager,
private ?CsrfTokenManagerInterface $csrfTokenManager,
private RequestStack $requestStack,
) {
}
@ -40,7 +40,7 @@ final readonly class ResendShipmentConfirmationEmailAction
{
$shipmentId = $request->attributes->get('id', '');
if (!$this->csrfTokenManager->isTokenValid(
if ($this->csrfTokenManager && !$this->csrfTokenManager->isTokenValid(
new CsrfToken($shipmentId, (string) $request->query->get('_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