diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 6fed9975..e0e42858 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -28,6 +28,7 @@
### Testing
- [ ] Unit tests added / updated
+- [ ] New/changed code is covered by tests — SonarCloud Quality Gate (coverage on new code) passes on the `sonarcloud` CI job
### Security & Ops
- [ ] No sensitive data or secrets introduced
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b1616306..d3874d07 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -19,13 +19,82 @@ jobs:
with:
symfony-versions: '["7.3"]'
+ # -----------------------------------------------------------------------
+ # COVERAGE — generates a Clover coverage report (PCOV) for SonarCloud to
+ # ingest. Kept separate from the reusable sylius_phpunit matrix (which
+ # runs with coverage: none) so coverage generation stays this repo's
+ # own concern, same as payplug/unified-plugin-core's ci.yml.
+ # -----------------------------------------------------------------------
+ coverage:
+ name: Coverage
+ if: github.base_ref == 'develop'
+ runs-on: ubuntu-latest
+ env:
+ APP_ENV: test
+ services:
+ mariadb:
+ image: 'mariadb:10.4.11'
+ ports:
+ - '3306:3306'
+ env:
+ MYSQL_ALLOW_EMPTY_PASSWORD: true
+ options: '--health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3'
+ steps:
+ -
+ uses: actions/checkout@v4
+ -
+ name: 'Setup PHP 8.2'
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: '8.2'
+ ini-values: date.timezone=UTC
+ extensions: intl
+ tools: symfony
+ coverage: pcov
+ -
+ name: 'Setup Node 20.x'
+ uses: actions/setup-node@v4
+ with:
+ node-version: '20.x'
+ -
+ name: 'Composer - Get Cache Directory'
+ id: composer-cache
+ run: 'echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT'
+ -
+ name: 'Composer - Set cache'
+ uses: actions/cache@v4
+ with:
+ path: '${{ steps.composer-cache.outputs.dir }}'
+ key: 'php-8.2-sylius-2.1.0-symfony-7.3-coverage-composer-${{ hashFiles(''**/composer.json'') }}'
+ restore-keys: 'php-8.2-sylius-2.1.0-symfony-7.3-coverage-composer-'
+ -
+ name: 'Composer - Github Auth'
+ run: 'composer config -g github-oauth.github.com ${{ github.token }}'
+ -
+ name: 'Install Sylius-Standard and Plugin'
+ run: 'make install -e SYLIUS_VERSION=2.1.0 SYMFONY_VERSION=7.3'
+ id: end-of-setup-sylius
+ -
+ name: 'Run tests with coverage'
+ run: 'composer test-coverage'
+ if: 'always() && steps.end-of-setup-sylius.outcome == ''success'''
+ -
+ name: 'Upload coverage report'
+ uses: actions/upload-artifact@v4
+ with:
+ name: clover-coverage
+ path: build/logs/clover.xml
+ retention-days: 1
+
sonarcloud:
if: always() && !failure() && !cancelled() && github.base_ref == 'develop'
- needs: [sylius-matrix]
- uses: payplug/template-ci/.github/workflows/sonarcloud.yml@main
+ needs: [sylius-matrix, coverage]
+ uses: payplug/template-ci/.github/workflows/sonarcloud-coverage.yml@main
with:
project-name: 'github-payplug-payplug-syliuspayplugplugin'
src-folder: 'src/'
+ coverage-report-artifact: 'clover-coverage'
+ enforce-quality-gate: true
secrets:
sonar-orga: ${{ secrets.SONAR_ORGA }}
sonar-token: ${{ secrets.SONAR_TOKEN }}
diff --git a/.gitignore b/.gitignore
index f47650bd..2900961e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,3 +25,5 @@ CLAUDE.md
.claude
.review
.phpunit.result.cache
+docs/
+build/
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7be32a5b..3cf7bb87 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- PHP 8.2+ compatibility
- Use Payment Request API from Sylius
- New Unified Authentication System (OAuth2)
+- **Multi-shop support**: several gateway configurations of the same type may now coexist, each
+ connected to its own PayPlug account and scoped to its own channels
+- The connected PayPlug account is displayed on each gateway's update screen
+- "Disconnect this account" per gateway, clearing that gateway's credentials without touching others
+- Channels already claimed by another enabled gateway of the same type are rendered unselectable
+- **Hosted Fields deferred capture**: with deferred capture enabled, Hosted Fields payments are
+ created as authorizations and captured (in full, partially, several times) or cancelled (in full
+ or partially) from the admin order screen, with the remaining amount and capture deadline shown
+- The Hosted Fields JS SDK URL is configurable through `PAYPLUG_HOSTED_FIELDS_SDK_URL` and defaults
+ to the production SDK (`v2.2.0`): QA, staging and local setups must set it to their own SDK URL
> [!IMPORTANT]
> Merchants will need to contact support to switch to the new authentication method.
@@ -19,12 +29,59 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Plugin structure has been changed to follow the new Symfony bundle structure
- Front assets have been migrated to use Stimulus
+- Gateway uniqueness is now enforced **per channel** instead of per installation: two gateways of
+ the same type may both be enabled as long as their channel sets are disjoint
+- Credentials are resolved from the payment method rather than from the gateway factory name, so a
+ request for one channel can no longer be signed with another channel's account
### Removed
- Drop Payum support
- Drop Sylius 1.x support
- Drop usage of Secret key - Use OAuth2 instead
+### Fixed
+- Integrated Payment no longer accepts a payment method id that does not belong to the order's
+ channel, or a disabled one — previously a shopper could have the payment created on another
+ merchant's PayPlug account
+- Oney instalment options, the Oney availability check and Apple Pay now resolve the gateway serving
+ the current channel instead of an arbitrary one
+- Hosted Fields payments, with a new or a saved card, are accepted again by the Unified API, which
+ now reads the card token from `paymentMethod.hfToken` and the saved card from
+ `paymentMethod.storedId`
+- Cards the shopper asked to save are stored again, including after a 3DS challenge (the alias is
+ read from `paymentMethod.storedId`)
+- Refunding a Hosted Fields payment that PayPlug created without a payment reference is refused
+ with a clear back-office message, in addition to Sylius's own generic error message, and nothing
+ is sent to PayPlug; the payment itself is processed normally
+- Hosted Fields deferred capture and cancel keep working with `payplug/unified-plugin-core` 1.2.1:
+ the plugin now passes the payment currency to UPC's `AmountHelper`, which requires it since
+ PRE-3724
+
+### Breaking changes for anyone extending the plugin
+
+| Removed / changed | Replacement |
+| --- | --- |
+| `PayPlugApiClientFactoryInterface::create(string $factoryName)` | `createForPaymentMethod(PaymentMethodInterface $pm)` |
+| `UnifiedApiPaymentCreatorInterface::createPayment($dto)` | `createPayment($dto, PaymentMethodInterface $method)` |
+| `OperationStatusFetcherInterface::getOperation($id)` | `getOperation($id, PaymentMethodInterface $method)` |
+| `AbstractGatewayConfigurationType::__construct()` — `$gatewayConfigRepository` and `$requestStack` dropped | translator only |
+| `shouldValidateBaseCurrency()` / `baseCurrencyViolationMessage()` — `protected` → `public`, now take the **mapped** config | same hooks, new visibility/shape |
+| `$gatewayFactoryName` property on the 8 configuration types | no longer read; the factory name comes off the gateway config |
+| Translation key `form.only_one_gateway_allowed` | `form.gateway_channel_conflict` (`%channel%`, `%payment_method%`) |
+| Injecting `PayplugUnifiedCore\Contracts\IConfigurationRepository` (its service alias is gone) | `ScopedConfigurationRepositoryInterface`, scoped per payment method |
+| `PaymentMethodRepositoryInterface::findOneByGatewayName()` — **deprecated**, returns an arbitrary config when several share a factory name | `findOneEnabledByGatewayNameAndChannel($factoryName, $channel)` |
+| `OneyExtension::__construct()` — `$gatewayConfigRepository` dropped, `$paymentMethodRepository` is now the plugin's `PaymentMethodRepositoryInterface` | inject the plugin repository |
+| `OneySupportedPaymentChoiceProvider::__construct()` | now also takes a `ChannelContextInterface` |
+| `PayPlugExtension::__construct()` | now also takes `string $hostedFieldsSdkUrl` (bound to `%payplug.hosted_fields_sdk_url%`) |
+
+Requires `payplug/unified-plugin-core ^1.2.1` (for the nullable `TokenOutput::$idToken` and the
+Unified API payment contract: `paymentMethod.hfToken`, `paymentMethod.storedId`).
+
+> [!NOTE]
+> A gateway connected before this release shows a "re-authenticate" placeholder instead of the
+> account email until the merchant reconnects — the address is only available from the interactive
+> OAuth `id_token`, which is minted at login.
+
Please refer to [github releases](https://github.com/payplug/SyliusPayPlugPlugin/releases) for historical release information.
---
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 00000000..00f23d65
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,38 @@
+FROM composer:2 AS composer
+
+FROM php:8.2-cli
+
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ git \
+ unzip \
+ libicu-dev \
+ libzip-dev \
+ libonig-dev \
+ libxml2-dev \
+ libpng-dev \
+ libjpeg-dev \
+ libfreetype6-dev \
+ libsodium-dev \
+ && docker-php-ext-configure gd --with-freetype --with-jpeg \
+ && docker-php-ext-install -j$(nproc) \
+ intl \
+ gd \
+ sodium \
+ pdo_mysql \
+ mbstring \
+ xml \
+ dom \
+ simplexml \
+ xmlwriter \
+ zip \
+ && pecl install pcov \
+ && docker-php-ext-enable pcov \
+ && rm -rf /var/lib/apt/lists/*
+
+COPY --from=composer /usr/bin/composer /usr/local/bin/composer
+
+RUN useradd --create-home --uid 1000 appuser
+
+WORKDIR /app
+
+USER appuser
diff --git a/Makefile b/Makefile
index b3007cc4..baf9ac83 100644
--- a/Makefile
+++ b/Makefile
@@ -8,6 +8,12 @@ SYLIUS_VERSION=2.1.0
SYMFONY_VERSION=6.4
PLUGIN_NAME=payplug/sylius-payplug-plugin
+# Coverage runs inside Docker (PHP 8.2 + PCOV) instead of the host PHP, since the host's default
+# `php`/`composer` may resolve to an unrelated version with no coverage driver installed. Assumes
+# `vendor/` (and the Sylius test-application) is already installed on the host via `make install`.
+IMAGE_DEV := sylius-payplug-plugin-dev
+DOCKER_RUN := docker run --rm -v $(CURDIR):/app -w /app -u "$$(id -u):$$(id -g)" -e COMPOSER_HOME=/tmp/composer $(IMAGE_DEV)
+
###
### DEVELOPMENT
### ¯¯¯¯¯¯¯¯¯¯¯
@@ -23,6 +29,14 @@ phpunit: ## Run PHPUnit tests
./vendor/bin/phpunit
.PHONY: phpunit
+build-dev: ## Build the Docker image used to run coverage
+ docker build -t $(IMAGE_DEV) .
+.PHONY: build-dev
+
+coverage: build-dev ## Run PHPUnit tests with a Clover coverage report (build/logs/clover.xml), via Docker
+ $(DOCKER_RUN) composer test-coverage
+.PHONY: coverage
+
###
### OTHER
### ¯¯¯¯¯¯
@@ -33,11 +47,12 @@ install-sylius:
@echo "Installing Sylius ${SYLIUS_VERSION} using TestApplication"
${COMPOSER} config extra.symfony.require "^${SYMFONY_VERSION}"
${COMPOSER} install
- ${COMPOSER} require --dev sylius/test-application:"^${SYLIUS_VERSION}@alpha" -n -W # TODO: Remove alpha when stable
+ @# Capped below 2.3: Sylius 2.3 (PHP 8.3+, so only resolved on the PHP 8.4 cells) drops knp-gaufrette-bundle,
+ @# which sylius/refund-plugin's config still loads, so test-application:install fails. Lift once RefundPlugin follows.
+ @# sylius/sylius is capped explicitly: older test-application releases (the 2.0.0 cell) do not constrain it and let it float to 2.3.
+ ${COMPOSER} require --dev sylius/test-application:">=${SYLIUS_VERSION},<2.3@alpha" sylius/sylius:"<2.3" -n -W # TODO: Remove alpha when stable
${COMPOSER} test-application:install
-
-
behat-configure: ## Configure Behat
(cd ${TEST_DIRECTORY} && cp behat.yml.dist behat.yml)
(cd ${TEST_DIRECTORY} && sed -i "s#vendor/sylius/sylius/src/Sylius/Behat/Resources/config/suites.yml#vendor/${PLUGIN_NAME}/tests/Behat/Resources/suites.yml#g" behat.yml)
diff --git a/README.md b/README.md
index 40b8025e..d71a5990 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,7 @@
[](https://sonarcloud.io/summary/new_code?id=github-payplug-payplug-syliuspayplugplugin)
[](https://sonarcloud.io/summary/new_code?id=github-payplug-payplug-syliuspayplugplugin)
[](https://sonarcloud.io/summary/new_code?id=github-payplug-payplug-syliuspayplugplugin)
+[](https://sonarcloud.io/summary/new_code?id=github-payplug-payplug-syliuspayplugplugin)
[](https://packagist.org/packages/payplug/sylius-payplug-plugin)
[](https://packagist.org/packages/payplug/sylius-payplug-plugin)
diff --git a/UPGRADE.md b/UPGRADE.md
index d2518688..311a214a 100644
--- a/UPGRADE.md
+++ b/UPGRADE.md
@@ -1,3 +1,35 @@
+# Deprecations
+
+## The Payum wiring — removal planned for early 2027
+
+The plugin no longer uses Payum. The live payment flow is Sylius 2.x command/response providers,
+declared in `config/services.yaml` and implemented in `src/Command/Handler/`.
+
+The Payum code left in the tree is inert: `PayPlugSyliusPayPlugExtension::load()` does not load
+`config/services/gateway.xml`, which holds the only `payum.gateway_factory_builder` tags, so no
+Payum gateway is ever registered for the `payplug*` factory names. The actions in `src/Action/` are
+still tagged `payum.action` in the compiled container (via `#[AutoconfigureTag]` attributes on the
+classes), but with no gateway to attach to they are never executed.
+
+The following are deprecated and **will be removed permanently in early 2027**:
+
+- `PayPlug\SyliusPayPlugPlugin\Action\CaptureAction`
+- `PayPlug\SyliusPayPlugPlugin\Action\StatusAction`
+- `PayPlug\SyliusPayPlugPlugin\Action\NotifyAction`
+- `PayPlug\SyliusPayPlugPlugin\Action\ConvertPaymentAction`
+- `PayPlug\SyliusPayPlugPlugin\Action\Api\ApiAwareTrait::setApi()` (the trait's `$payPlugApiClient`
+ property is **not** deprecated — it is still used by `Controller\OneClickAction`)
+- `PayPlug\SyliusPayPlugPlugin\Gateway\AbstractGatewayFactory::populateConfig()` (the class itself
+ is **not** deprecated — it remains the home of `FACTORY_NAME`, `FACTORY_TITLE` and
+ `BASE_CURRENCY_CODE`)
+- `config/services/gateway.xml`
+
+**If you do not import `config/services/gateway.xml` in your own application, this affects you in no
+way** — none of the above runs today, and there is nothing to migrate.
+
+If you *do* import it to keep using Payum, that escape hatch stops working at the removal date. Move
+to the command handlers in `src/Command/Handler/` before then.
+
# Upgrading from 1.0.0
1. Skip the faulty migration
diff --git a/assets/controllers.json b/assets/controllers.json
index 5582d953..9d9dfddd 100644
--- a/assets/controllers.json
+++ b/assets/controllers.json
@@ -16,6 +16,13 @@
"@payplug/sylius-payplug-plugin/shop/dist/payment/integrated.css": true
}
},
+ "hosted-fields": {
+ "enabled": true,
+ "fetch": "lazy",
+ "autoimport": {
+ "@payplug/sylius-payplug-plugin/shop/dist/payment/hosted-fields.css": true
+ }
+ },
"oney-payment": {
"enabled": true,
"fetch": "lazy"
diff --git a/assets/package.json b/assets/package.json
index 52494447..c17e1363 100644
--- a/assets/package.json
+++ b/assets/package.json
@@ -34,6 +34,15 @@
"@payplug/sylius-payplug-plugin/shop/dist/payment/integrated.css": true
}
},
+ "hosted-fields": {
+ "main": "shop/controllers/hosted-fields_controller.js",
+ "webpackMode": "lazy",
+ "fetch": "lazy",
+ "enabled": true,
+ "autoimport": {
+ "@payplug/sylius-payplug-plugin/shop/dist/payment/hosted-fields.css": true
+ }
+ },
"oney-payment": {
"main": "shop/controllers/oney-payment_controller.js",
"webpackMode": "lazy",
diff --git a/assets/shop/controllers/hosted-fields_controller.js b/assets/shop/controllers/hosted-fields_controller.js
new file mode 100644
index 00000000..e7a4c512
--- /dev/null
+++ b/assets/shop/controllers/hosted-fields_controller.js
@@ -0,0 +1,233 @@
+import { Controller } from '@hotwired/stimulus';
+
+const ALLOWED_BRANDS = ['CB', 'VISA', 'MASTERCARD'];
+
+// Applied inside each hosted iframe (Dalenys renders these rules into the field's own
+// document). The SDK only accepts a small whitelist of CSS properties here — anything
+// outside it (we tried "height": SDK logged "Css property ... is not supported" and threw,
+// which aborted ALL fields, not just the one it complained about) — so stick to exactly the
+// properties confirmed by PayPlug's own documented example (font-size/color/font-style).
+// Background and sizing/centering of the field's content are NOT controllable this way.
+const FIELD_STYLE = {
+ input: {
+ 'font-size': '14px',
+ color: '#2B343D',
+ 'background-color': 'transparent',
+ },
+ '::placeholder': {
+ 'font-size': '14px',
+ color: '#969a9f',
+ },
+};
+
+/* stimulusFetch: 'lazy' */
+export default class extends Controller {
+ static targets = ['container', 'error', 'submitButton'];
+
+ connect() {
+ if (typeof payplug_hosted_fields_params === 'undefined') {
+ return;
+ }
+
+ this.form = this.element.closest('form');
+ this.hfields = null;
+
+ if (this.hasSubmitButtonTarget) {
+ this.submitButtonTarget.addEventListener('click', (event) => {
+ event.preventDefault();
+ this.tokenizeAndSubmit();
+ });
+ }
+
+ // With saved cards on offer, selecting the payment method is no longer enough to mean "show
+ // the card form": the customer must also have chosen "pay with another card". Nothing else
+ // establishes that initial state — handleShow()/handleHide() are only ever reached from the
+ // change actions wired onto the choice radios in
+ // templates/form/sylius_checkout_select_payment_row.html.twig, and no change event fires on
+ // page load — so without this branch a pre-selected saved card still renders the fields open
+ // (the --loaded class alone drives their visibility). Mirrors integrated-payment_controller.
+ if (payplug_hosted_fields_params.has_saved_cards) {
+ this.watchCardChoice();
+
+ return;
+ }
+
+ // Stimulus connects as soon as the markup is in the DOM, even though the payment method
+ // container starts hidden (see shop/select_payment/choice.html.twig). Mounting the
+ // cross-origin Dalenys iframes into a display:none container breaks their rendering, so
+ // load them only once this payment method is actually selected.
+ const isChecked = this.getPaymentMethodSelectors({
+ methodCode: payplug_hosted_fields_params.payment_method_code,
+ checked: true,
+ });
+ if (isChecked.length) {
+ this.openFields();
+ }
+
+ this.getPaymentMethodSelectors().forEach((element) => {
+ element.addEventListener('change', (e) => {
+ if (payplug_hosted_fields_params.payment_method_code === e.currentTarget.value && e.currentTarget.checked) {
+ this.openFields();
+ }
+ });
+ });
+ }
+
+ // Opens the fields only while BOTH "pay with another card" and this payment method are
+ // selected — on load and on every subsequent change to either set of radios. Closing is left
+ // to handleHide(), which the choice radios already trigger and which also resets
+ // data-payment-inline-submit.
+ watchCardChoice() {
+ if (this.isOtherCardChosen()) {
+ this.openFields();
+ }
+
+ this.element
+ .querySelectorAll('.payment-choice__input, [id*="checkout_select_payment_payments"]')
+ .forEach((element) => {
+ element.addEventListener('change', () => {
+ if (this.isOtherCardChosen()) {
+ this.openFields();
+ }
+ });
+ });
+ }
+
+ isOtherCardChosen() {
+ if (true !== this.element.querySelector('#payplug_choice_card_other')?.checked) {
+ return false;
+ }
+
+ // The payment method radio is absent when the shop offers only one method — Sylius renders
+ // no choice at all in that case, so a checked card choice is on its own enough to mean this
+ // method is the one being paid with.
+ const methodRadio = document.querySelector(
+ `[id*="checkout_select_payment_payments"][value="${payplug_hosted_fields_params.payment_method_code}"]`,
+ );
+
+ return null === methodRadio
+ ? null !== document.querySelector('.payplug-payment-choice__input:checked')
+ : methodRadio.checked;
+ }
+
+ handleShow(event) {
+ if (this.hasContainerTarget) {
+ import('jquery').then(({ default: $ }) => {
+ $(this.containerTarget).slideDown();
+ });
+ this.openFields();
+ this.containerTarget.dataset.paymentInlineSubmit = "true";
+ this.element.dispatchEvent(new CustomEvent('payment-method-state-change', { bubbles: true }));
+ }
+ }
+
+ handleHide(event) {
+ if (this.hasContainerTarget) {
+ import('jquery').then(({ default: $ }) => {
+ $(this.containerTarget).slideUp();
+ });
+ this.closeFields();
+ this.containerTarget.dataset.paymentInlineSubmit = "false";
+ this.element.dispatchEvent(new CustomEvent('payment-method-state-change', { bubbles: true }));
+ }
+ }
+
+ getPaymentMethodSelectors({ methodCode, checked } = {}) {
+ const baseSelector = '[id*=checkout_select_payment_payments]';
+
+ if (methodCode) {
+ if (checked) {
+ return document.querySelectorAll(`${baseSelector}[value=${methodCode}]:checked`);
+ }
+ return document.querySelectorAll(`${baseSelector}[value=${methodCode}]`);
+ }
+ return document.querySelectorAll(baseSelector);
+ }
+
+ openFields() {
+ if (this.hasContainerTarget) {
+ this.containerTarget.classList.add('payplugHostedFields--loaded');
+ }
+ if (null === this.hfields) {
+ this.load();
+ }
+ }
+
+ closeFields() {
+ if (this.hasContainerTarget) {
+ this.containerTarget.classList.remove('payplugHostedFields--loaded');
+ }
+ }
+
+ load() {
+ this.hfields = window.dalenys.hostedFields({
+ companyId: payplug_hosted_fields_params.companyId,
+ fields: {
+ brand: { id: "brand-container", version: 2, style: FIELD_STYLE },
+ card: { id: "card-container", placeholder: "•••• •••• •••• ••••", enableAutospacing: true, style: FIELD_STYLE },
+ expiry: { id: "expiry-container", placeholder: "MM/AA", style: FIELD_STYLE },
+ cryptogram: { id: "cvv-container", placeholder: "CVV", style: FIELD_STYLE },
+ },
+
+ locale: payplug_hosted_fields_params.locale,
+ });
+ this.hfields.load();
+ }
+
+ tokenizeAndSubmit() {
+ if (null === this.hfields) {
+ // Fields were never mounted (payment method not selected yet): nothing to tokenize.
+ return;
+ }
+
+ this.hideError();
+ this.hfields.createToken((result) => {
+ if (result.execCode !== '0000') {
+ this.showError(payplug_hosted_fields_params.error.tokenization_failed);
+ return;
+ }
+
+ const selectedBrand = (result.selectedBrand || '').toUpperCase();
+ if (!ALLOWED_BRANDS.includes(selectedBrand)) {
+ this.showError(payplug_hosted_fields_params.error.unsupported_brand);
+ return;
+ }
+
+ const saveCardElement = this.element.querySelector('#hostedfields_savecard');
+ const saveCard = null !== saveCardElement && saveCardElement.checked;
+
+ this.form.querySelector('#hostedfields_token').value = result.hfToken;
+ this.form.querySelector('#hostedfields_selected_brand').value = selectedBrand;
+ this.form.querySelector('#hostedfields_save_card').value = saveCard ? 'true' : 'false';
+ // last4/expirationMonth/expirationYear/country field names are unverified against a real
+ // createToken() response (no vendored SDK docs/types exist in this repo to confirm them) —
+ // if wrong, these silently fall back to '' rather than error. This data is fully
+ // client-controlled and only ever used as a display-only fallback for a saved card's
+ // metadata when PayPlug's own operation-fetch is unavailable — PayplugCardPersister
+ // validates the format of each field (4-digit last4, 1-12 month, a plausible year, a
+ // 2-letter country) before trusting any of it, and discards anything that doesn't match
+ // rather than persisting it as-is.
+ this.form.querySelector('#hostedfields_last4').value = result.last4 || '';
+ this.form.querySelector('#hostedfields_exp_month').value = result.expirationMonth || '';
+ this.form.querySelector('#hostedfields_exp_year').value = result.expirationYear || '';
+ this.form.querySelector('#hostedfields_country').value = result.country || '';
+ this.form.submit();
+ });
+ }
+
+ showError(message) {
+ if (!this.hasErrorTarget) {
+ return;
+ }
+ this.errorTarget.textContent = message;
+ this.errorTarget.classList.remove('payplugHostedFields__error--hide');
+ }
+
+ hideError() {
+ if (!this.hasErrorTarget) {
+ return;
+ }
+ this.errorTarget.textContent = '';
+ this.errorTarget.classList.add('payplugHostedFields__error--hide');
+ }
+}
diff --git a/assets/shop/dist/payment/hosted-fields.css b/assets/shop/dist/payment/hosted-fields.css
new file mode 100644
index 00000000..d158fdd4
--- /dev/null
+++ b/assets/shop/dist/payment/hosted-fields.css
@@ -0,0 +1,301 @@
+.payplugHostedFields {
+ justify-self: center;
+ display: none
+}
+
+.payplugHostedFields * {
+ font-family: Poppins, Arial, sans-serif !important
+}
+
+.payplugHostedFields--loaded {
+ width: 100%;
+ max-width: 400px;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ margin: 20px auto 0;
+ display: flex;
+ position: relative
+}
+
+.payplugHostedFields__container {
+ width: 100%;
+ margin: 0 0 10px;
+ padding: 0;
+ display: flex;
+ position: relative
+}
+
+.payplugHostedFields__schemes {
+ width: 100%;
+ display: flex;
+ justify-content: flex-end;
+ gap: 6px;
+ margin: 0 0 10px
+}
+
+.payplugHostedFields__scheme-badge {
+ padding: 2px 6px;
+ border: 1px solid #d5d6d8;
+ border-radius: 4px;
+ font-size: 10px;
+ font-weight: 700;
+ color: #2b343d;
+ background: #f9fafb
+}
+
+.payplugHostedFields__cardWrapper {
+ width: 100%;
+ margin: 0 0 10px;
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 4px
+}
+
+.payplugHostedFields__cardWrapper .payplugHostedFields__container--card {
+ flex: 1 1 100%;
+ min-width: 0;
+ margin: 0
+}
+
+.payplugHostedFields__network {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ position: relative
+}
+
+.payplugHostedFields__networkLabel {
+ display: none;
+ align-items: center;
+ gap: 6px;
+ font-size: 12px;
+ color: #767a80
+}
+
+.payplugHostedFields__networkInfo {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 16px;
+ height: 16px;
+ border: 1px solid #767a80;
+ border-radius: 50%;
+ font-size: 10px;
+ font-weight: 700;
+ font-style: italic;
+ cursor: pointer
+}
+
+.payplugHostedFields__networkInfo:before {
+ content: "i"
+}
+
+.payplugHostedFields__networkTooltip {
+ position: absolute;
+ bottom: calc(100% + 6px);
+ left: 0;
+ z-index: 10;
+ width: 280px;
+ max-width: 100%;
+ padding: 10px 12px;
+ border-radius: 6px;
+ font-size: 12px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 1.4;
+ color: #fff;
+ background: #000;
+ visibility: hidden;
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity .15s
+}
+
+.payplugHostedFields__networkInfo:hover .payplugHostedFields__networkTooltip,
+.payplugHostedFields__networkInfo:focus .payplugHostedFields__networkTooltip {
+ visibility: visible;
+ opacity: 1
+}
+
+/* The SDK's own label is an absolutely positioned
in , placed with
+ pixel coordinates measured once. It stays in the DOM and only toggles inline display, so hide it
+ and show our in-flow label exactly while the SDK reveals its own. */
+body:has(.payplugHostedFields) #info-container {
+ display: none !important
+}
+
+body:has(#info-container:not([style*="display: none"])) .payplugHostedFields__networkLabel {
+ display: inline-flex
+}
+
+.payplugHostedFields__container--brand {
+ height: 44px;
+ min-height: 0;
+ width: 170px;
+ margin: 0 0 0 auto;
+ flex-shrink: 0;
+ align-items: center;
+ justify-content: center
+}
+
+.payplugHostedFields__container--card,
+.payplugHostedFields__container--expiry,
+.payplugHostedFields__container--cvv {
+ height: 42px;
+ cursor: text;
+ background: #fafafa;
+ border: 1.5px solid #d5d6d8;
+ border-radius: 8px;
+ line-height: 42px;
+ transition: border-color .2s, box-shadow .2s, background-color .2s
+}
+
+.payplugHostedFields__container--card:focus-within,
+.payplugHostedFields__container--expiry:focus-within,
+.payplugHostedFields__container--cvv:focus-within {
+ background: #fff;
+ border-color: #2b343d;
+ box-shadow: 0 0 0 3px rgba(43, 52, 61, .1)
+}
+
+.payplugHostedFields__container--card.is-invalid,
+.payplugHostedFields__container--expiry.is-invalid,
+.payplugHostedFields__container--cvv.is-invalid {
+ border-color: #e91932;
+ box-shadow: 0 0 0 3px rgba(233, 25, 50, .1)
+}
+
+.payplugHostedFields__container--card,
+.payplugHostedFields__container--expiry,
+.payplugHostedFields__container--cvv {
+ padding: 0 16px 0 50px
+}
+
+.payplugHostedFields__container--card:before,
+.payplugHostedFields__container--expiry:before,
+.payplugHostedFields__container--cvv:before {
+ content: "";
+ width: 24px;
+ height: 24px;
+ background: #95999e 50%/100% no-repeat;
+ position: absolute;
+ top: 20%;
+ left: 16px
+}
+
+.payplugHostedFields__container--card:before {
+ -webkit-mask-image: url(card.0d2bd9bc.svg);
+ mask-image: url(card.0d2bd9bc.svg)
+}
+
+.payplugHostedFields__container--expiry:before {
+ -webkit-mask-image: url(calendar.3c23bb16.svg);
+ mask-image: url(calendar.3c23bb16.svg)
+}
+
+.payplugHostedFields__container--cvv:before {
+ -webkit-mask-image: url(lock.fe8a73cd.svg);
+ mask-image: url(lock.fe8a73cd.svg)
+}
+
+.payplugHostedFields__container--expiry,
+.payplugHostedFields__container--cvv {
+ max-width: calc(50% - 2px);
+ display: inline-block
+}
+
+.payplugHostedFields__container--brand,
+.payplugHostedFields__container--card,
+.payplugHostedFields__container--expiry,
+.payplugHostedFields__container--cvv {
+ overflow: hidden
+}
+
+.payplugHostedFields__container--brand iframe,
+.payplugHostedFields__container--card iframe,
+.payplugHostedFields__container--expiry iframe,
+.payplugHostedFields__container--cvv iframe {
+ width: 100%;
+ height: 100%;
+ border: none;
+ display: block
+}
+
+.payplugHostedFields__container--saveCard {
+ height: auto;
+ align-items: center;
+ padding: 10px 0 0;
+ display: flex
+}
+
+.payplugHostedFields__container--saveCard input {
+ display: none
+}
+
+.payplugHostedFields__container--saveCard input:checked+label span:before {
+ opacity: 1
+}
+
+.payplugHostedFields__container--saveCard label {
+ cursor: pointer;
+ color: #918f8f;
+ margin: 0 !important;
+ font-size: 12px !important
+}
+
+.payplugHostedFields__container--saveCard label span {
+ cursor: pointer;
+ height: 16px;
+ -o-transition: border .4s;
+ width: 16px;
+ border: 1px solid #d5d6d8;
+ border-radius: 2px;
+ margin: 0 10px -3px 0;
+ transition: border .4s;
+ display: inline-block;
+ position: relative
+}
+
+.payplugHostedFields__container--saveCard label span:before {
+ content: "";
+ height: 5px;
+ opacity: 0;
+ width: 10px;
+ border-top: none;
+ border-bottom: 2.5px solid #2b343d;
+ border-left: 2.5px solid #2b343d;
+ border-right: none;
+ border-radius: 1px;
+ transition: opacity .4s;
+ display: block;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -55%)rotate(-48deg)
+}
+
+.payplugHostedFields__container--saveCard label:hover {
+ color: #2b343d;
+ transition: all .1s
+}
+
+.payplugHostedFields__container--saveCard label:hover span {
+ border-color: #2b343d;
+ transition: all .1s
+}
+
+.payplugHostedFields__error {
+ color: #e91932;
+ width: 100%;
+ margin: -10px 0 10px;
+ padding-left: 4px;
+ font-size: 12px;
+ line-height: 18px
+}
+
+.payplugHostedFields__error--hide {
+ display: none
+}
diff --git a/composer.json b/composer.json
index 2d93bb4d..3ea65550 100755
--- a/composer.json
+++ b/composer.json
@@ -14,6 +14,7 @@
"ext-json": "*",
"giggsey/libphonenumber-for-php": "^8.12",
"payplug/payplug-php": "^4.0",
+ "payplug/unified-plugin-core": "^1.2.1",
"php-http/message-factory": "^1.1",
"sylius/refund-plugin": "^2.0",
"sylius/sylius": "^2.0",
@@ -26,6 +27,7 @@
"behat/mink-selenium2-driver": "1.7.0",
"dmore/behat-chrome-extension": "1.4.0",
"dmore/chrome-mink-driver": "2.9.3",
+ "doctrine/orm": ">=3.5 <3.7",
"friends-of-behat/mink": "1.11.0",
"friends-of-behat/mink-browserkit-driver": "1.6.2",
"friends-of-behat/mink-debug-extension": "2.1.0",
@@ -83,8 +85,9 @@
"ecs": "ecs check -c ruleset/ecs.php --ansi --clear-cache",
"fix-ecs": "@ecs --fix --memory-limit=4G",
"phpmd": "phpmd src ansi ruleset/.php_md.xml",
- "phpstan": "phpstan analyse src -c ruleset/phpstan.neon",
+ "phpstan": "phpstan analyse src -c ruleset/phpstan.neon --memory-limit=4G",
"phpunit": "phpunit tests/PHPUnit --colors=always",
+ "test-coverage": "phpunit tests/PHPUnit --colors=always --coverage-clover=build/logs/clover.xml",
"tests": [
"@ecs",
"@phpmd",
diff --git a/composer.lock b/composer.lock
new file mode 100644
index 00000000..15ccf32d
--- /dev/null
+++ b/composer.lock
@@ -0,0 +1,21943 @@
+{
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+ "This file is @generated automatically"
+ ],
+ "content-hash": "39a63c300688d00971a9a43114e99ec7",
+ "packages": [
+ {
+ "name": "alcohol/iso4217",
+ "version": "4.3.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/alcohol/iso4217.git",
+ "reference": "9ea65a1ce6979f2b973948982cba7e3c5f0edea3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/alcohol/iso4217/zipball/9ea65a1ce6979f2b973948982cba7e3c5f0edea3",
+ "reference": "9ea65a1ce6979f2b973948982cba7e3c5f0edea3",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4 || ^8.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.5.28 || ^10.5.58 || ^11.5.43 || ^12.4.2"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "4.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Alcohol\\": "."
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Rob Bast",
+ "email": "rob.bast@gmail.com"
+ }
+ ],
+ "description": "ISO 4217 PHP Library",
+ "homepage": "http://alcohol.github.io/iso4217/",
+ "keywords": [
+ "4217",
+ "ISO 4217",
+ "currencies",
+ "iso",
+ "library"
+ ],
+ "support": {
+ "issues": "https://github.com/alcohol/iso4217/issues",
+ "source": "https://github.com/alcohol/iso4217"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/alcohol",
+ "type": "github"
+ }
+ ],
+ "time": "2026-01-02T09:46:17+00:00"
+ },
+ {
+ "name": "api-platform/doctrine-common",
+ "version": "v4.3.21",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/api-platform/doctrine-common.git",
+ "reference": "202b4e6626c01a593e61b89028f89348e7190dd9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/api-platform/doctrine-common/zipball/202b4e6626c01a593e61b89028f89348e7190dd9",
+ "reference": "202b4e6626c01a593e61b89028f89348e7190dd9",
+ "shasum": ""
+ },
+ "require": {
+ "api-platform/metadata": "^4.2.6",
+ "api-platform/state": "^4.2.4",
+ "doctrine/collections": "^2.1 || ^3.0",
+ "doctrine/common": "^3.2.2",
+ "doctrine/persistence": "^3.2 || ^4.0",
+ "php": ">=8.2"
+ },
+ "conflict": {
+ "doctrine/persistence": "<1.3"
+ },
+ "require-dev": {
+ "doctrine/mongodb-odm": "^2.10",
+ "doctrine/orm": "^2.17 || ^3.0",
+ "phpspec/prophecy-phpunit": "^2.2",
+ "phpunit/phpunit": "^11.5 || ^12.2",
+ "symfony/type-info": "^7.3 || ^8.0"
+ },
+ "suggest": {
+ "api-platform/graphql": "For GraphQl mercure subscriptions.",
+ "api-platform/http-cache": "For HTTP cache invalidation.",
+ "phpstan/phpdoc-parser": "For PHP documentation support.",
+ "symfony/config": "For XML resource configuration.",
+ "symfony/mercure-bundle": "For mercure updates publisher.",
+ "symfony/yaml": "For YAML resource configuration."
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/api-platform/api-platform",
+ "name": "api-platform/api-platform"
+ },
+ "symfony": {
+ "require": "^6.4 || ^7.0 || ^8.0"
+ },
+ "branch-alias": {
+ "dev-3.4": "3.4.x-dev",
+ "dev-4.1": "4.1.x-dev",
+ "dev-4.2": "4.2.x-dev",
+ "dev-main": "4.4.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "ApiPlatform\\Doctrine\\Common\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Kévin Dunglas",
+ "email": "kevin@dunglas.fr",
+ "homepage": "https://dunglas.fr"
+ },
+ {
+ "name": "API Platform Community",
+ "homepage": "https://api-platform.com/community/contributors"
+ }
+ ],
+ "description": "Common files used by api-platform/doctrine-orm and api-platform/doctrine-odm",
+ "homepage": "https://api-platform.com",
+ "keywords": [
+ "doctrine",
+ "graphql",
+ "odm",
+ "orm",
+ "rest"
+ ],
+ "support": {
+ "source": "https://github.com/api-platform/doctrine-common/tree/v4.3.21"
+ },
+ "time": "2026-09-12T13:19:57+00:00"
+ },
+ {
+ "name": "api-platform/doctrine-orm",
+ "version": "v4.3.21",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/api-platform/doctrine-orm.git",
+ "reference": "997fa78bf13ddc5cb3951eea61bd23f425e91486"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/api-platform/doctrine-orm/zipball/997fa78bf13ddc5cb3951eea61bd23f425e91486",
+ "reference": "997fa78bf13ddc5cb3951eea61bd23f425e91486",
+ "shasum": ""
+ },
+ "require": {
+ "api-platform/doctrine-common": "^4.2.23",
+ "api-platform/metadata": "^4.2",
+ "api-platform/serializer": "^4.2.16",
+ "api-platform/state": "^4.2.4",
+ "composer/semver": "^3.4",
+ "doctrine/orm": "^2.17 || ^3.0.1",
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "doctrine/doctrine-bundle": "^2.11 || ^3.1",
+ "phpspec/prophecy-phpunit": "^2.2",
+ "phpunit/phpunit": "^11.5 || ^12.2",
+ "ramsey/uuid": "^4.7",
+ "ramsey/uuid-doctrine": "^2.0",
+ "symfony/cache": "^6.4 || ^7.0 || ^8.0",
+ "symfony/framework-bundle": "^6.4 || ^7.0 || ^8.0",
+ "symfony/property-access": "^6.4 || ^7.0 || ^8.0",
+ "symfony/property-info": "^6.4 || ^7.1 || ^8.0",
+ "symfony/serializer": "^6.4 || ^7.0 || ^8.0",
+ "symfony/type-info": "^7.3 || ^8.0",
+ "symfony/uid": "^6.4 || ^7.0 || ^8.0",
+ "symfony/validator": "^6.4.11 || ^7.0 || ^8.0",
+ "symfony/yaml": "^6.4 || ^7.0 || ^8.0"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/api-platform/api-platform",
+ "name": "api-platform/api-platform"
+ },
+ "symfony": {
+ "require": "^6.4 || ^7.0 || ^8.0"
+ },
+ "branch-alias": {
+ "dev-3.4": "3.4.x-dev",
+ "dev-4.1": "4.1.x-dev",
+ "dev-4.2": "4.2.x-dev",
+ "dev-main": "4.4.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "ApiPlatform\\Doctrine\\Orm\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Kévin Dunglas",
+ "email": "kevin@dunglas.fr",
+ "homepage": "https://dunglas.fr"
+ },
+ {
+ "name": "API Platform Community",
+ "homepage": "https://api-platform.com/community/contributors"
+ }
+ ],
+ "description": "Doctrine ORM bridge",
+ "homepage": "https://api-platform.com",
+ "keywords": [
+ "api",
+ "doctrine",
+ "graphql",
+ "orm",
+ "rest"
+ ],
+ "support": {
+ "source": "https://github.com/api-platform/doctrine-orm/tree/v4.3.21"
+ },
+ "time": "2026-09-09T07:14:18+00:00"
+ },
+ {
+ "name": "api-platform/documentation",
+ "version": "v4.3.21",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/api-platform/documentation.git",
+ "reference": "f07b444aef1f75bb07beb9f8d799213f05070e5f"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/api-platform/documentation/zipball/f07b444aef1f75bb07beb9f8d799213f05070e5f",
+ "reference": "f07b444aef1f75bb07beb9f8d799213f05070e5f",
+ "shasum": ""
+ },
+ "require": {
+ "api-platform/metadata": "^4.3",
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.5 || ^12.2"
+ },
+ "type": "project",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/api-platform/api-platform",
+ "name": "api-platform/api-platform"
+ },
+ "symfony": {
+ "require": "^6.4 || ^7.0 || ^8.0"
+ },
+ "branch-alias": {
+ "dev-3.4": "3.4.x-dev",
+ "dev-4.1": "4.1.x-dev",
+ "dev-4.2": "4.2.x-dev",
+ "dev-main": "4.4.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "ApiPlatform\\Documentation\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Kévin Dunglas",
+ "email": "kevin@dunglas.fr",
+ "homepage": "https://dunglas.fr"
+ },
+ {
+ "name": "API Platform Community",
+ "homepage": "https://api-platform.com/community/contributors"
+ }
+ ],
+ "description": "API Platform documentation controller.",
+ "support": {
+ "source": "https://github.com/api-platform/documentation/tree/v4.4.0-alpha.1"
+ },
+ "time": "2026-04-30T12:21:24+00:00"
+ },
+ {
+ "name": "api-platform/http-cache",
+ "version": "v4.3.21",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/api-platform/http-cache.git",
+ "reference": "8e71916de766f503dd60f1a1e886b4d704f881a8"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/api-platform/http-cache/zipball/8e71916de766f503dd60f1a1e886b4d704f881a8",
+ "reference": "8e71916de766f503dd60f1a1e886b4d704f881a8",
+ "shasum": ""
+ },
+ "require": {
+ "api-platform/metadata": "^4.3",
+ "api-platform/state": "^4.3",
+ "php": ">=8.2",
+ "symfony/http-foundation": "^6.4.14 || ^7.0 || ^8.0"
+ },
+ "require-dev": {
+ "guzzlehttp/guzzle": "^6.0 || ^7.0 || ^8.0",
+ "phpspec/prophecy-phpunit": "^2.2",
+ "phpunit/phpunit": "^11.5 || ^12.2",
+ "symfony/dependency-injection": "^6.4 || ^7.0 || ^8.0",
+ "symfony/http-client": "^6.4 || ^7.0 || ^8.0",
+ "symfony/type-info": "^7.3 || ^8.0"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/api-platform/api-platform",
+ "name": "api-platform/api-platform"
+ },
+ "symfony": {
+ "require": "^6.4 || ^7.0 || ^8.0"
+ },
+ "branch-alias": {
+ "dev-3.4": "3.4.x-dev",
+ "dev-4.1": "4.1.x-dev",
+ "dev-4.2": "4.2.x-dev",
+ "dev-main": "4.4.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "ApiPlatform\\HttpCache\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Kévin Dunglas",
+ "email": "kevin@dunglas.fr",
+ "homepage": "https://dunglas.fr"
+ },
+ {
+ "name": "API Platform Community",
+ "homepage": "https://api-platform.com/comunnity/contributors"
+ }
+ ],
+ "description": "API Platform HttpCache component",
+ "homepage": "https://api-platform.com",
+ "keywords": [
+ "api",
+ "cache",
+ "http",
+ "rest"
+ ],
+ "support": {
+ "source": "https://github.com/api-platform/http-cache/tree/v4.4.0-alpha.1"
+ },
+ "time": "2026-06-09T14:20:49+00:00"
+ },
+ {
+ "name": "api-platform/hydra",
+ "version": "v4.3.21",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/api-platform/hydra.git",
+ "reference": "6498a597510fbd6aa46a25cdfa867d415b28ed9f"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/api-platform/hydra/zipball/6498a597510fbd6aa46a25cdfa867d415b28ed9f",
+ "reference": "6498a597510fbd6aa46a25cdfa867d415b28ed9f",
+ "shasum": ""
+ },
+ "require": {
+ "api-platform/documentation": "^4.3",
+ "api-platform/json-schema": "^4.3",
+ "api-platform/jsonld": "^4.3",
+ "api-platform/metadata": "^4.3",
+ "api-platform/serializer": "^4.3.12",
+ "api-platform/state": "^4.3",
+ "php": ">=8.2",
+ "symfony/type-info": "^7.3 || ^8.0",
+ "symfony/web-link": "^6.4 || ^7.1 || ^8.0"
+ },
+ "require-dev": {
+ "api-platform/doctrine-common": "^4.3",
+ "api-platform/doctrine-odm": "^4.3",
+ "api-platform/doctrine-orm": "^4.3",
+ "phpspec/prophecy": "^1.19",
+ "phpspec/prophecy-phpunit": "^2.2",
+ "phpunit/phpunit": "^11.5 || ^12.2"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/api-platform/api-platform",
+ "name": "api-platform/api-platform"
+ },
+ "symfony": {
+ "require": "^6.4 || ^7.0 || ^8.0"
+ },
+ "branch-alias": {
+ "dev-3.4": "3.4.x-dev",
+ "dev-4.1": "4.1.x-dev",
+ "dev-4.2": "4.2.x-dev",
+ "dev-main": "4.4.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "ApiPlatform\\Hydra\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Kévin Dunglas",
+ "email": "kevin@dunglas.fr",
+ "homepage": "https://dunglas.fr"
+ },
+ {
+ "name": "API Platform Community",
+ "homepage": "https://api-platform.com/community/contributors"
+ }
+ ],
+ "description": "API Hydra support",
+ "homepage": "https://api-platform.com",
+ "keywords": [
+ "Hydra",
+ "JSON-LD",
+ "api",
+ "graphql",
+ "jsonapi",
+ "rest"
+ ],
+ "support": {
+ "source": "https://github.com/api-platform/hydra/tree/v4.3.21"
+ },
+ "time": "2026-08-16T15:12:48+00:00"
+ },
+ {
+ "name": "api-platform/json-schema",
+ "version": "v4.3.21",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/api-platform/json-schema.git",
+ "reference": "b97491371c40f9080bf0a4d24799d7779e1eb0b2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/api-platform/json-schema/zipball/b97491371c40f9080bf0a4d24799d7779e1eb0b2",
+ "reference": "b97491371c40f9080bf0a4d24799d7779e1eb0b2",
+ "shasum": ""
+ },
+ "require": {
+ "api-platform/metadata": "^4.3",
+ "php": ">=8.2",
+ "symfony/console": "^6.4 || ^7.0 || ^8.0",
+ "symfony/property-info": "^6.4 || ^7.1 || ^8.0",
+ "symfony/serializer": "^6.4 || ^7.0 || ^8.0",
+ "symfony/type-info": "^7.3 || ^8.0",
+ "symfony/uid": "^6.4 || ^7.0 || ^8.0"
+ },
+ "require-dev": {
+ "phpspec/prophecy-phpunit": "^2.2",
+ "phpunit/phpunit": "^11.5 || ^12.2"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/api-platform/api-platform",
+ "name": "api-platform/api-platform"
+ },
+ "symfony": {
+ "require": "^6.4 || ^7.0 || ^8.0"
+ },
+ "branch-alias": {
+ "dev-3.4": "3.4.x-dev",
+ "dev-4.1": "4.1.x-dev",
+ "dev-4.2": "4.2.x-dev",
+ "dev-main": "4.4.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "ApiPlatform\\JsonSchema\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Kévin Dunglas",
+ "email": "kevin@dunglas.fr",
+ "homepage": "https://dunglas.fr"
+ },
+ {
+ "name": "API Platform Community",
+ "homepage": "https://api-platform.com/community/contributors"
+ }
+ ],
+ "description": "Generate a JSON Schema from a PHP class",
+ "homepage": "https://api-platform.com",
+ "keywords": [
+ "JSON Schema",
+ "api",
+ "json",
+ "openapi",
+ "rest",
+ "swagger"
+ ],
+ "support": {
+ "source": "https://github.com/api-platform/json-schema/tree/v4.3.21"
+ },
+ "time": "2026-09-02T08:21:34+00:00"
+ },
+ {
+ "name": "api-platform/jsonld",
+ "version": "v4.3.21",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/api-platform/jsonld.git",
+ "reference": "026a380c3c85c4210028da43e0cea1b64211bbf5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/api-platform/jsonld/zipball/026a380c3c85c4210028da43e0cea1b64211bbf5",
+ "reference": "026a380c3c85c4210028da43e0cea1b64211bbf5",
+ "shasum": ""
+ },
+ "require": {
+ "api-platform/metadata": "^4.3",
+ "api-platform/serializer": "^4.3.12",
+ "api-platform/state": "^4.3",
+ "php": ">=8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.5 || ^12.2",
+ "symfony/type-info": "^7.3 || ^8.0"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/api-platform/api-platform",
+ "name": "api-platform/api-platform"
+ },
+ "symfony": {
+ "require": "^6.4 || ^7.0 || ^8.0"
+ },
+ "branch-alias": {
+ "dev-3.4": "3.4.x-dev",
+ "dev-4.1": "4.1.x-dev",
+ "dev-4.2": "4.2.x-dev",
+ "dev-main": "4.4.x-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "./HydraContext.php"
+ ],
+ "psr-4": {
+ "ApiPlatform\\JsonLd\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Kévin Dunglas",
+ "email": "kevin@dunglas.fr",
+ "homepage": "https://dunglas.fr"
+ },
+ {
+ "name": "API Platform Community",
+ "homepage": "https://api-platform.com/community/contributors"
+ }
+ ],
+ "description": "API JSON-LD support",
+ "homepage": "https://api-platform.com",
+ "keywords": [
+ "Hydra",
+ "JSON-LD",
+ "api",
+ "graphql",
+ "rest"
+ ],
+ "support": {
+ "source": "https://github.com/api-platform/jsonld/tree/v4.3.21"
+ },
+ "time": "2026-06-13T05:11:46+00:00"
+ },
+ {
+ "name": "api-platform/metadata",
+ "version": "v4.3.21",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/api-platform/metadata.git",
+ "reference": "6493d73e04649e07cff914db3f2c178abeae3278"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/api-platform/metadata/zipball/6493d73e04649e07cff914db3f2c178abeae3278",
+ "reference": "6493d73e04649e07cff914db3f2c178abeae3278",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/inflector": "^2.0",
+ "php": ">=8.2",
+ "psr/cache": "^1.0 || ^2.0 || ^3.0",
+ "psr/log": "^1.0 || ^2.0 || ^3.0",
+ "symfony/property-info": "^6.4 || ^7.1 || ^8.0",
+ "symfony/string": "^6.4 || ^7.0 || ^8.0",
+ "symfony/type-info": "^7.3 || ^8.0"
+ },
+ "require-dev": {
+ "api-platform/json-schema": "^4.3",
+ "api-platform/openapi": "^4.3",
+ "api-platform/state": "^4.3",
+ "phpspec/prophecy-phpunit": "^2.2",
+ "phpstan/phpdoc-parser": "^1.29 || ^2.0",
+ "phpunit/phpunit": "^11.5 || ^12.2",
+ "symfony/config": "^6.4 || ^7.0 || ^8.0",
+ "symfony/routing": "^6.4 || ^7.0 || ^8.0",
+ "symfony/var-dumper": "^6.4 || ^7.0 || ^8.0",
+ "symfony/web-link": "^6.4 || ^7.1 || ^8.0",
+ "symfony/yaml": "^6.4 || ^7.0 || ^8.0"
+ },
+ "suggest": {
+ "phpstan/phpdoc-parser": "For PHP documentation support.",
+ "symfony/config": "For XML resource configuration.",
+ "symfony/yaml": "For YAML resource configuration."
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/api-platform/api-platform",
+ "name": "api-platform/api-platform"
+ },
+ "symfony": {
+ "require": "^6.4 || ^7.0 || ^8.0"
+ },
+ "branch-alias": {
+ "dev-3.4": "3.4.x-dev",
+ "dev-4.1": "4.1.x-dev",
+ "dev-4.2": "4.2.x-dev",
+ "dev-main": "4.4.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "ApiPlatform\\Metadata\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Kévin Dunglas",
+ "email": "kevin@dunglas.fr",
+ "homepage": "https://dunglas.fr"
+ },
+ {
+ "name": "API Platform Community",
+ "homepage": "https://api-platform.com/community/contributors"
+ }
+ ],
+ "description": "API Resource-oriented metadata attributes and factories",
+ "homepage": "https://api-platform.com",
+ "keywords": [
+ "Hydra",
+ "JSON-LD",
+ "api",
+ "graphql",
+ "hal",
+ "jsonapi",
+ "openapi",
+ "rest",
+ "swagger"
+ ],
+ "support": {
+ "source": "https://github.com/api-platform/metadata/tree/v4.3.21"
+ },
+ "time": "2026-09-29T18:57:50+00:00"
+ },
+ {
+ "name": "api-platform/openapi",
+ "version": "v4.3.21",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/api-platform/openapi.git",
+ "reference": "c72470132f2eb35a4f8f252e60342f0f7c487704"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/api-platform/openapi/zipball/c72470132f2eb35a4f8f252e60342f0f7c487704",
+ "reference": "c72470132f2eb35a4f8f252e60342f0f7c487704",
+ "shasum": ""
+ },
+ "require": {
+ "api-platform/json-schema": "^4.3",
+ "api-platform/metadata": "^4.3",
+ "api-platform/state": "^4.3",
+ "php": ">=8.2",
+ "symfony/console": "^6.4 || ^7.0 || ^8.0",
+ "symfony/filesystem": "^6.4 || ^7.0 || ^8.0",
+ "symfony/property-access": "^6.4 || ^7.0 || ^8.0",
+ "symfony/serializer": "^6.4 || ^7.0 || ^8.0",
+ "symfony/type-info": "^7.3 || ^8.0"
+ },
+ "require-dev": {
+ "api-platform/doctrine-common": "^4.3",
+ "api-platform/doctrine-odm": "^4.3",
+ "api-platform/doctrine-orm": "^4.3",
+ "api-platform/serializer": "^4.3.12",
+ "phpspec/prophecy-phpunit": "^2.2",
+ "phpunit/phpunit": "^11.5 || ^12.2",
+ "symfony/type-info": "^7.3 || ^8.0"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/api-platform/api-platform",
+ "name": "api-platform/api-platform"
+ },
+ "symfony": {
+ "require": "^6.4 || ^7.0 || ^8.0"
+ },
+ "branch-alias": {
+ "dev-3.4": "3.4.x-dev",
+ "dev-4.1": "4.1.x-dev",
+ "dev-4.2": "4.2.x-dev",
+ "dev-main": "4.4.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "ApiPlatform\\OpenApi\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Kévin Dunglas",
+ "email": "kevin@dunglas.fr",
+ "homepage": "https://dunglas.fr"
+ },
+ {
+ "name": "API Platform Community",
+ "homepage": "https://api-platform.com/community/contributors"
+ }
+ ],
+ "description": "Models to build and serialize an OpenAPI specification.",
+ "homepage": "https://api-platform.com",
+ "keywords": [
+ "Hydra",
+ "JSON-LD",
+ "api",
+ "graphql",
+ "hal",
+ "jsonapi",
+ "openapi",
+ "rest",
+ "swagger"
+ ],
+ "support": {
+ "source": "https://github.com/api-platform/openapi/tree/v4.3.21"
+ },
+ "time": "2026-06-16T10:01:53+00:00"
+ },
+ {
+ "name": "api-platform/serializer",
+ "version": "v4.3.21",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/api-platform/serializer.git",
+ "reference": "d6984c417ca6c0ea22b31269c274564577ad71c6"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/api-platform/serializer/zipball/d6984c417ca6c0ea22b31269c274564577ad71c6",
+ "reference": "d6984c417ca6c0ea22b31269c274564577ad71c6",
+ "shasum": ""
+ },
+ "require": {
+ "api-platform/metadata": "^4.3",
+ "api-platform/state": "^4.3",
+ "php": ">=8.2",
+ "symfony/property-access": "^6.4 || ^7.0 || ^8.0",
+ "symfony/property-info": "^6.4 || ^7.1 || ^8.0",
+ "symfony/serializer": "^6.4.37 || ^7.4.9 || ^8.0.9",
+ "symfony/validator": "^6.4.11 || ^7.0 || ^8.0"
+ },
+ "require-dev": {
+ "api-platform/doctrine-common": "^4.3",
+ "api-platform/doctrine-odm": "^4.3",
+ "api-platform/doctrine-orm": "^4.3",
+ "api-platform/json-schema": "^4.3",
+ "api-platform/openapi": "^4.3",
+ "doctrine/collections": "^2.1",
+ "phpspec/prophecy-phpunit": "^2.2",
+ "phpunit/phpunit": "^11.5 || ^12.2",
+ "sebastian/exporter": "^6.3.2 || ^7.0.2",
+ "symfony/mercure-bundle": "^0.4.3|^0.5",
+ "symfony/type-info": "^7.3 || ^8.0",
+ "symfony/var-dumper": "^6.4 || ^7.0 || ^8.0",
+ "symfony/yaml": "^6.4 || ^7.0 || ^8.0"
+ },
+ "suggest": {
+ "api-platform/doctrine-odm": "To support Doctrine MongoDB ODM state options.",
+ "api-platform/doctrine-orm": "To support Doctrine ORM state options."
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/api-platform/api-platform",
+ "name": "api-platform/api-platform"
+ },
+ "symfony": {
+ "require": "^6.4 || ^7.0 || ^8.0"
+ },
+ "branch-alias": {
+ "dev-3.4": "3.4.x-dev",
+ "dev-4.1": "4.1.x-dev",
+ "dev-4.2": "4.2.x-dev",
+ "dev-main": "4.4.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "ApiPlatform\\Serializer\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Kévin Dunglas",
+ "email": "kevin@dunglas.fr",
+ "homepage": "https://dunglas.fr"
+ },
+ {
+ "name": "API Platform Community",
+ "homepage": "https://api-platform.com/community/contributors"
+ }
+ ],
+ "description": "API Platform core Serializer",
+ "homepage": "https://api-platform.com",
+ "keywords": [
+ "api",
+ "graphql",
+ "rest",
+ "serializer"
+ ],
+ "support": {
+ "source": "https://github.com/api-platform/serializer/tree/v4.3.21"
+ },
+ "time": "2026-09-09T07:14:58+00:00"
+ },
+ {
+ "name": "api-platform/state",
+ "version": "v4.3.21",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/api-platform/state.git",
+ "reference": "e5b1b7fa70fa8a8de2065ab4fc960db035723c0b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/api-platform/state/zipball/e5b1b7fa70fa8a8de2065ab4fc960db035723c0b",
+ "reference": "e5b1b7fa70fa8a8de2065ab4fc960db035723c0b",
+ "shasum": ""
+ },
+ "require": {
+ "api-platform/metadata": "^4.3",
+ "php": ">=8.2",
+ "psr/container": "^1.0 || ^2.0",
+ "symfony/deprecation-contracts": "^3.1",
+ "symfony/http-kernel": "^6.4.13 || ^7.0 || ^8.0",
+ "symfony/serializer": "^6.4 || ^7.0 || ^8.0",
+ "symfony/translation-contracts": "^3.0"
+ },
+ "require-dev": {
+ "api-platform/serializer": "^4.3.12",
+ "api-platform/validator": "^4.3.1",
+ "phpunit/phpunit": "^11.5 || ^12.2",
+ "symfony/http-foundation": "^6.4.14 || ^7.0 || ^8.0",
+ "symfony/object-mapper": "^7.4 || ^8.0",
+ "symfony/type-info": "^7.4 || ^8.0",
+ "symfony/web-link": "^6.4 || ^7.1 || ^8.0",
+ "willdurand/negotiation": "^3.1"
+ },
+ "suggest": {
+ "api-platform/serializer": "To use API Platform serializer.",
+ "api-platform/validator": "To use API Platform validation.",
+ "symfony/http-foundation": "To use our HTTP providers and processor.",
+ "symfony/web-link": "To support adding web links to the response headers.",
+ "willdurand/negotiation": "To use the API Platform content negoatiation provider."
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/api-platform/api-platform",
+ "name": "api-platform/api-platform"
+ },
+ "symfony": {
+ "require": "^6.4 || ^7.0 || ^8.0"
+ },
+ "branch-alias": {
+ "dev-3.4": "3.4.x-dev",
+ "dev-4.1": "4.1.x-dev",
+ "dev-4.2": "4.2.x-dev",
+ "dev-main": "4.4.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "ApiPlatform\\State\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Kévin Dunglas",
+ "email": "kevin@dunglas.fr",
+ "homepage": "https://dunglas.fr"
+ },
+ {
+ "name": "API Platform Community",
+ "homepage": "https://api-platform.com/community/contributors"
+ }
+ ],
+ "description": "API Platform State component ",
+ "homepage": "https://api-platform.com",
+ "keywords": [
+ "Hydra",
+ "JSON-LD",
+ "api",
+ "graphql",
+ "hal",
+ "jsonapi",
+ "openapi",
+ "rest",
+ "swagger"
+ ],
+ "support": {
+ "source": "https://github.com/api-platform/state/tree/v4.3.21"
+ },
+ "time": "2026-09-12T13:42:25+00:00"
+ },
+ {
+ "name": "api-platform/symfony",
+ "version": "v4.3.21",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/api-platform/symfony.git",
+ "reference": "04aec51ec642c202cce0c476cf93ff0b0ce76ab9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/api-platform/symfony/zipball/04aec51ec642c202cce0c476cf93ff0b0ce76ab9",
+ "reference": "04aec51ec642c202cce0c476cf93ff0b0ce76ab9",
+ "shasum": ""
+ },
+ "require": {
+ "api-platform/documentation": "^4.3",
+ "api-platform/http-cache": "^4.3",
+ "api-platform/hydra": "^4.3",
+ "api-platform/json-schema": "^4.3",
+ "api-platform/jsonld": "^4.3",
+ "api-platform/metadata": "^4.3",
+ "api-platform/openapi": "^4.3",
+ "api-platform/serializer": "^4.3.12",
+ "api-platform/state": "^4.3",
+ "api-platform/validator": "^4.3.1",
+ "php": ">=8.2",
+ "symfony/asset": "^6.4 || ^7.0 || ^8.0",
+ "symfony/finder": "^6.4 || ^7.0 || ^8.0",
+ "symfony/http-kernel": "^6.4.13 || ^7.0 || ^8.0",
+ "symfony/property-access": "^6.4 || ^7.0 || ^8.0",
+ "symfony/property-info": "^6.4 || ^7.0 || ^8.0",
+ "symfony/security-core": "^6.4 || ^7.0 || ^8.0",
+ "symfony/serializer": "^6.4 || ^7.0 || ^8.0",
+ "willdurand/negotiation": "^3.1"
+ },
+ "require-dev": {
+ "api-platform/doctrine-common": "^4.3",
+ "api-platform/doctrine-odm": "^4.3",
+ "api-platform/doctrine-orm": "^4.3",
+ "api-platform/elasticsearch": "^4.3",
+ "api-platform/graphql": "^4.3",
+ "api-platform/hal": "^4.3",
+ "phpspec/prophecy-phpunit": "^2.2",
+ "phpunit/phpunit": "^11.5 || ^12.2",
+ "symfony/expression-language": "^6.4 || ^7.0 || ^8.0",
+ "symfony/intl": "^6.4 || ^7.0 || ^8.0",
+ "symfony/mercure-bundle": "^0.4.3|^0.5",
+ "symfony/object-mapper": "^7.0 || ^8.0",
+ "symfony/routing": "^6.4 || ^7.0 || ^8.0",
+ "symfony/type-info": "^7.3 || ^8.0",
+ "symfony/validator": "^6.4.11 || ^7.0 || ^8.0",
+ "webonyx/graphql-php": "^15.0"
+ },
+ "suggest": {
+ "api-platform/doctrine-odm": "To support MongoDB. Only versions 4.0 and later are supported.",
+ "api-platform/doctrine-orm": "To support Doctrine ORM.",
+ "api-platform/elasticsearch": "To support Elasticsearch.",
+ "api-platform/graphql": "To support GraphQL.",
+ "api-platform/hal": "to support the HAL format",
+ "api-platform/json-api": "to support the JSON-API format",
+ "api-platform/ramsey-uuid": "To support Ramsey's UUID identifiers.",
+ "phpstan/phpdoc-parser": "To support extracting metadata from PHPDoc.",
+ "psr/cache-implementation": "To use metadata caching.",
+ "symfony/cache": "To have metadata caching when using Symfony integration.",
+ "symfony/config": "To load XML configuration files.",
+ "symfony/expression-language": "To use authorization and mercure advanced features.",
+ "symfony/http-client": "To use the HTTP cache invalidation system.",
+ "symfony/mercure-bundle": "To support mercure integration.",
+ "symfony/messenger": "To support messenger integration and asynchronous Mercure updates.",
+ "symfony/security": "To use authorization features.",
+ "symfony/twig-bundle": "To use the Swagger UI integration.",
+ "symfony/uid": "To support Symfony UUID/ULID identifiers.",
+ "symfony/web-profiler-bundle": "To use the data collector."
+ },
+ "type": "symfony-bundle",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/api-platform/api-platform",
+ "name": "api-platform/api-platform"
+ },
+ "symfony": {
+ "require": "^6.4 || ^7.0 || ^8.0"
+ },
+ "branch-alias": {
+ "dev-main": "4.4.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "ApiPlatform\\Symfony\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Kévin Dunglas",
+ "email": "kevin@dunglas.fr",
+ "homepage": "https://dunglas.fr"
+ },
+ {
+ "name": "API Platform Community",
+ "homepage": "https://api-platform.com/community/contributors"
+ }
+ ],
+ "description": "Symfony API Platform integration",
+ "homepage": "https://api-platform.com",
+ "keywords": [
+ "Hydra",
+ "JSON-LD",
+ "api",
+ "graphql",
+ "hal",
+ "jsonapi",
+ "openapi",
+ "rest",
+ "swagger",
+ "symfony"
+ ],
+ "support": {
+ "source": "https://github.com/api-platform/symfony/tree/v4.3.21"
+ },
+ "time": "2026-09-29T14:56:10+00:00"
+ },
+ {
+ "name": "api-platform/validator",
+ "version": "v4.3.21",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/api-platform/validator.git",
+ "reference": "b7a35b2a15216229ef93271b1fdd4a4d691d2ba7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/api-platform/validator/zipball/b7a35b2a15216229ef93271b1fdd4a4d691d2ba7",
+ "reference": "b7a35b2a15216229ef93271b1fdd4a4d691d2ba7",
+ "shasum": ""
+ },
+ "require": {
+ "api-platform/metadata": "^4.3",
+ "php": ">=8.2",
+ "symfony/http-kernel": "^6.4.13 || ^7.1 || ^8.0",
+ "symfony/serializer": "^6.4 || ^7.1 || ^8.0",
+ "symfony/type-info": "^7.3 || ^8.0",
+ "symfony/validator": "^6.4.11 || ^7.1 || ^8.0",
+ "symfony/web-link": "^6.4 || ^7.1 || ^8.0"
+ },
+ "require-dev": {
+ "phpspec/prophecy-phpunit": "^2.2",
+ "phpunit/phpunit": "^11.5 || ^12.2"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/api-platform/api-platform",
+ "name": "api-platform/api-platform"
+ },
+ "symfony": {
+ "require": "^6.4 || ^7.0 || ^8.0"
+ },
+ "branch-alias": {
+ "dev-3.4": "3.4.x-dev",
+ "dev-4.1": "4.1.x-dev",
+ "dev-4.2": "4.2.x-dev",
+ "dev-main": "4.4.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "ApiPlatform\\Validator\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Kévin Dunglas",
+ "email": "kevin@dunglas.fr",
+ "homepage": "https://dunglas.fr"
+ },
+ {
+ "name": "API Platform Community",
+ "homepage": "https://api-platform.com/community/contributors"
+ }
+ ],
+ "description": "API Platform validator component",
+ "homepage": "https://api-platform.com",
+ "keywords": [
+ "api",
+ "graphql",
+ "rest",
+ "validator"
+ ],
+ "support": {
+ "source": "https://github.com/api-platform/validator/tree/v4.3.21"
+ },
+ "time": "2026-09-05T06:05:04+00:00"
+ },
+ {
+ "name": "babdev/pagerfanta-bundle",
+ "version": "v4.6.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/BabDev/PagerfantaBundle.git",
+ "reference": "ea3eb6a3f9d838de73254683b1a57014877d2ff3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/BabDev/PagerfantaBundle/zipball/ea3eb6a3f9d838de73254683b1a57014877d2ff3",
+ "reference": "ea3eb6a3f9d838de73254683b1a57014877d2ff3",
+ "shasum": ""
+ },
+ "require": {
+ "pagerfanta/core": "^3.7 || ^4.0",
+ "php": "^8.1",
+ "psr/container": "^1.0 || ^2.0",
+ "symfony/config": "^5.4 || ^6.4 || ^7.3 || ^8.0",
+ "symfony/dependency-injection": "^5.4 || ^6.4 || ^7.3 || ^8.0",
+ "symfony/http-foundation": "^5.4 || ^6.4 || ^7.3 || ^8.0",
+ "symfony/http-kernel": "^5.4 || ^6.4 || ^7.3 || ^8.0",
+ "symfony/property-access": "^5.4 || ^6.4 || ^7.3 || ^8.0",
+ "symfony/routing": "^5.4 || ^6.4 || ^7.3 || ^8.0"
+ },
+ "conflict": {
+ "jms/serializer": "<3.18",
+ "jms/serializer-bundle": "<4.2",
+ "pagerfanta/twig": "<3.7",
+ "symfony/serializer": "<5.4 || >=6.0,<6.4 || >=7.0,<7.3",
+ "symfony/translation": "<5.4 || >=6.0,<6.4 || >=7.0,<7.3",
+ "symfony/twig-bridge": "<5.4 || >=6.0,<6.4 || >=7.0,<7.3",
+ "symfony/twig-bundle": "<5.4 || >=6.0,<6.4 || >=7.0,<7.3",
+ "twig/twig": "<2.13",
+ "white-october/pagerfanta-bundle": "*"
+ },
+ "require-dev": {
+ "jms/serializer": "^3.18",
+ "jms/serializer-bundle": "^4.2 || ^5.0",
+ "matthiasnoback/symfony-dependency-injection-test": "^6.2",
+ "pagerfanta/twig": "^3.7 || ^4.0",
+ "phpstan/extension-installer": "^1.3",
+ "phpstan/phpstan": "2.1.32",
+ "phpstan/phpstan-phpunit": "2.0.8",
+ "phpstan/phpstan-symfony": "2.0.9",
+ "phpunit/phpunit": "10.5.58 || 11.5.44 || 12.4.4",
+ "symfony/serializer": "^5.4 || ^6.4 || ^7.3 || ^8.0",
+ "symfony/translation": "^5.4 || ^6.4 || ^7.3 || ^8.0",
+ "symfony/twig-bridge": "^5.4 || ^6.4 || ^7.3 || ^8.0",
+ "symfony/twig-bundle": "^5.4 || ^6.4 || ^7.3 || ^8.0",
+ "twig/twig": "^2.13 || ^3.0"
+ },
+ "suggest": {
+ "jms/serializer-bundle": "To use the Pagerfanta class with the JMS Serializer",
+ "symfony/serializer": "To use the Pagerfanta class with the Symfony Serializer",
+ "symfony/translation": "To use the Twig templates with translation support",
+ "twig/twig": "To integrate Pagerfanta with Twig through extensions"
+ },
+ "type": "symfony-bundle",
+ "autoload": {
+ "psr-4": {
+ "BabDev\\PagerfantaBundle\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Bundle integrating Pagerfanta with Symfony",
+ "keywords": [
+ "pagerfanta",
+ "pagination",
+ "symfony"
+ ],
+ "support": {
+ "issues": "https://github.com/BabDev/PagerfantaBundle/issues",
+ "source": "https://github.com/BabDev/PagerfantaBundle/tree/v4.6.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/mbabker",
+ "type": "github"
+ }
+ ],
+ "time": "2025-11-29T13:01:51+00:00"
+ },
+ {
+ "name": "behat/transliterator",
+ "version": "v1.5.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Behat/Transliterator.git",
+ "reference": "baac5873bac3749887d28ab68e2f74db3a4408af"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Behat/Transliterator/zipball/baac5873bac3749887d28ab68e2f74db3a4408af",
+ "reference": "baac5873bac3749887d28ab68e2f74db3a4408af",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "require-dev": {
+ "chuyskywalker/rolling-curl": "^3.1",
+ "php-yaoi/php-yaoi": "^1.0",
+ "phpunit/phpunit": "^8.5.25 || ^9.5.19"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Behat\\Transliterator\\": "src/Behat/Transliterator"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "Artistic-1.0"
+ ],
+ "description": "String transliterator",
+ "keywords": [
+ "i18n",
+ "slug",
+ "transliterator"
+ ],
+ "support": {
+ "issues": "https://github.com/Behat/Transliterator/issues",
+ "source": "https://github.com/Behat/Transliterator/tree/v1.5.0"
+ },
+ "abandoned": true,
+ "time": "2022-03-30T09:27:43+00:00"
+ },
+ {
+ "name": "brick/math",
+ "version": "1.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/brick/math.git",
+ "reference": "2effe05d2177c451b86c6a073196a4034c02f211"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/brick/math/zipball/2effe05d2177c451b86c6a073196a4034c02f211",
+ "reference": "2effe05d2177c451b86c6a073196a4034c02f211",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.2"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "2.2.13",
+ "phpstan/phpstan-phpunit": "2.0.18",
+ "phpunit/phpunit": "^11.5"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Brick\\Math\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Arbitrary-precision arithmetic library",
+ "keywords": [
+ "Arbitrary-precision",
+ "BigInteger",
+ "BigRational",
+ "arithmetic",
+ "bigdecimal",
+ "bignum",
+ "bignumber",
+ "brick",
+ "decimal",
+ "integer",
+ "math",
+ "mathematics",
+ "rational"
+ ],
+ "support": {
+ "issues": "https://github.com/brick/math/issues",
+ "source": "https://github.com/brick/math/tree/1.0.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/BenMorel",
+ "type": "github"
+ }
+ ],
+ "time": "2026-09-12T10:28:18+00:00"
+ },
+ {
+ "name": "clue/stream-filter",
+ "version": "v1.7.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/clue/stream-filter.git",
+ "reference": "049509fef80032cb3f051595029ab75b49a3c2f7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/clue/stream-filter/zipball/049509fef80032cb3f051595029ab75b49a3c2f7",
+ "reference": "049509fef80032cb3f051595029ab75b49a3c2f7",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/functions_include.php"
+ ],
+ "psr-4": {
+ "Clue\\StreamFilter\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Christian Lück",
+ "email": "christian@clue.engineering"
+ }
+ ],
+ "description": "A simple and modern approach to stream filtering in PHP",
+ "homepage": "https://github.com/clue/stream-filter",
+ "keywords": [
+ "bucket brigade",
+ "callback",
+ "filter",
+ "php_user_filter",
+ "stream",
+ "stream_filter_append",
+ "stream_filter_register"
+ ],
+ "support": {
+ "issues": "https://github.com/clue/stream-filter/issues",
+ "source": "https://github.com/clue/stream-filter/tree/v1.7.0"
+ },
+ "funding": [
+ {
+ "url": "https://clue.engineering/support",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/clue",
+ "type": "github"
+ }
+ ],
+ "time": "2023-12-20T15:40:13+00:00"
+ },
+ {
+ "name": "composer/semver",
+ "version": "3.5.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/semver.git",
+ "reference": "528948210cc46f967a4b98e83b19e76f2b504993"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/semver/zipball/528948210cc46f967a4b98e83b19e76f2b504993",
+ "reference": "528948210cc46f967a4b98e83b19e76f2b504993",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.3.2 || ^7.0 || ^8.0"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^1.11",
+ "symfony/phpunit-bridge": "^3 || ^7"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Composer\\Semver\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nils Adermann",
+ "email": "naderman@naderman.de",
+ "homepage": "http://www.naderman.de"
+ },
+ {
+ "name": "Jordi Boggiano",
+ "email": "j.boggiano@seld.be",
+ "homepage": "http://seld.be"
+ },
+ {
+ "name": "Rob Bast",
+ "email": "rob.bast@gmail.com",
+ "homepage": "http://robbast.nl"
+ }
+ ],
+ "description": "Version comparison library that offers utilities, version constraint parsing and validation.",
+ "keywords": [
+ "semantic",
+ "semver",
+ "validation",
+ "versioning"
+ ],
+ "support": {
+ "irc": "ircs://irc.libera.chat:6697/composer",
+ "issues": "https://github.com/composer/semver/issues",
+ "source": "https://github.com/composer/semver/tree/3.5.1"
+ },
+ "funding": [
+ {
+ "url": "https://packagist.com",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/composer",
+ "type": "github"
+ }
+ ],
+ "time": "2026-10-05T13:12:12+00:00"
+ },
+ {
+ "name": "doctrine/collections",
+ "version": "2.6.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/collections.git",
+ "reference": "7713da39d8e237f28411d6a616a3dce5e20d5de2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/collections/zipball/7713da39d8e237f28411d6a616a3dce5e20d5de2",
+ "reference": "7713da39d8e237f28411d6a616a3dce5e20d5de2",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/deprecations": "^1",
+ "php": "^8.1",
+ "symfony/polyfill-php84": "^1.30"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^14",
+ "ext-json": "*",
+ "phpstan/phpstan": "^2.1.30",
+ "phpstan/phpstan-phpunit": "^2.0.7",
+ "phpunit/phpunit": "^10.5.58 || ^11.5.42 || ^12.4"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Common\\Collections\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Guilherme Blanco",
+ "email": "guilhermeblanco@gmail.com"
+ },
+ {
+ "name": "Roman Borschel",
+ "email": "roman@code-factory.org"
+ },
+ {
+ "name": "Benjamin Eberlei",
+ "email": "kontakt@beberlei.de"
+ },
+ {
+ "name": "Jonathan Wage",
+ "email": "jonwage@gmail.com"
+ },
+ {
+ "name": "Johannes Schmitt",
+ "email": "schmittjoh@gmail.com"
+ }
+ ],
+ "description": "PHP Doctrine Collections library that adds additional functionality on top of PHP arrays.",
+ "homepage": "https://www.doctrine-project.org/projects/collections.html",
+ "keywords": [
+ "array",
+ "collections",
+ "iterators",
+ "php"
+ ],
+ "support": {
+ "issues": "https://github.com/doctrine/collections/issues",
+ "source": "https://github.com/doctrine/collections/tree/2.6.0"
+ },
+ "funding": [
+ {
+ "url": "https://www.doctrine-project.org/sponsorship.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://www.patreon.com/phpdoctrine",
+ "type": "patreon"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcollections",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-15T10:01:58+00:00"
+ },
+ {
+ "name": "doctrine/common",
+ "version": "3.5.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/common.git",
+ "reference": "d9ea4a54ca2586db781f0265d36bea731ac66ec5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/common/zipball/d9ea4a54ca2586db781f0265d36bea731ac66ec5",
+ "reference": "d9ea4a54ca2586db781f0265d36bea731ac66ec5",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/persistence": "^2.0 || ^3.0 || ^4.0",
+ "php": "^7.1 || ^8.0"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^9.0 || ^10.0",
+ "doctrine/collections": "^1",
+ "phpstan/phpstan": "^1.4.1",
+ "phpstan/phpstan-phpunit": "^1",
+ "phpunit/phpunit": "^7.5.20 || ^8.5 || ^9.0",
+ "squizlabs/php_codesniffer": "^3.0",
+ "symfony/phpunit-bridge": "^6.1",
+ "vimeo/psalm": "^4.4"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Common\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Guilherme Blanco",
+ "email": "guilhermeblanco@gmail.com"
+ },
+ {
+ "name": "Roman Borschel",
+ "email": "roman@code-factory.org"
+ },
+ {
+ "name": "Benjamin Eberlei",
+ "email": "kontakt@beberlei.de"
+ },
+ {
+ "name": "Jonathan Wage",
+ "email": "jonwage@gmail.com"
+ },
+ {
+ "name": "Johannes Schmitt",
+ "email": "schmittjoh@gmail.com"
+ },
+ {
+ "name": "Marco Pivetta",
+ "email": "ocramius@gmail.com"
+ }
+ ],
+ "description": "PHP Doctrine Common project is a library that provides additional functionality that other Doctrine projects depend on such as better reflection support, proxies and much more.",
+ "homepage": "https://www.doctrine-project.org/projects/common.html",
+ "keywords": [
+ "common",
+ "doctrine",
+ "php"
+ ],
+ "support": {
+ "issues": "https://github.com/doctrine/common/issues",
+ "source": "https://github.com/doctrine/common/tree/3.5.0"
+ },
+ "funding": [
+ {
+ "url": "https://www.doctrine-project.org/sponsorship.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://www.patreon.com/phpdoctrine",
+ "type": "patreon"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcommon",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-01-01T22:12:03+00:00"
+ },
+ {
+ "name": "doctrine/data-fixtures",
+ "version": "2.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/data-fixtures.git",
+ "reference": "bf7ac3a050b54b261cedfb3d0a44733819062275"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/data-fixtures/zipball/bf7ac3a050b54b261cedfb3d0a44733819062275",
+ "reference": "bf7ac3a050b54b261cedfb3d0a44733819062275",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/persistence": "^3.1 || ^4.0",
+ "php": "^8.1",
+ "psr/log": "^1.1 || ^2 || ^3"
+ },
+ "conflict": {
+ "doctrine/dbal": "<3.5 || >=5",
+ "doctrine/orm": "<2.14 || >=4",
+ "doctrine/phpcr-odm": "<1.3.0"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^14",
+ "doctrine/dbal": "^3.5 || ^4",
+ "doctrine/mongodb-odm": "^1.3.0 || ^2.0.0",
+ "doctrine/orm": "^2.14 || ^3",
+ "doctrine/phpcr-odm": "^1.8 || ^2.0",
+ "ext-sqlite3": "*",
+ "fig/log-test": "^1",
+ "jackalope/jackalope-fs": "*",
+ "phpstan/phpstan": "2.1.46",
+ "phpunit/phpunit": "10.5.63 || 12.5.12",
+ "symfony/cache": "^6.4 || ^7 || ^8",
+ "symfony/var-exporter": "^6.4 || ^7 || ^8"
+ },
+ "suggest": {
+ "alcaeus/mongo-php-adapter": "For using MongoDB ODM 1.3 with PHP 7 (deprecated)",
+ "doctrine/mongodb-odm": "For loading MongoDB ODM fixtures",
+ "doctrine/orm": "For loading ORM fixtures",
+ "doctrine/phpcr-odm": "For loading PHPCR ODM fixtures"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Common\\DataFixtures\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jonathan Wage",
+ "email": "jonwage@gmail.com"
+ }
+ ],
+ "description": "Data Fixtures for all Doctrine Object Managers",
+ "homepage": "https://www.doctrine-project.org",
+ "keywords": [
+ "database"
+ ],
+ "support": {
+ "issues": "https://github.com/doctrine/data-fixtures/issues",
+ "source": "https://github.com/doctrine/data-fixtures/tree/2.2.1"
+ },
+ "funding": [
+ {
+ "url": "https://www.doctrine-project.org/sponsorship.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://www.patreon.com/phpdoctrine",
+ "type": "patreon"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdata-fixtures",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-04-01T13:56:01+00:00"
+ },
+ {
+ "name": "doctrine/dbal",
+ "version": "3.10.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/dbal.git",
+ "reference": "c95589d775a0b2e543467d40f8c3ecccf586f2b4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/dbal/zipball/c95589d775a0b2e543467d40f8c3ecccf586f2b4",
+ "reference": "c95589d775a0b2e543467d40f8c3ecccf586f2b4",
+ "shasum": ""
+ },
+ "require": {
+ "composer-runtime-api": "^2",
+ "doctrine/deprecations": "^0.5.3|^1",
+ "doctrine/event-manager": "^1|^2",
+ "php": "^7.4 || ^8.0",
+ "psr/cache": "^1|^2|^3",
+ "psr/log": "^1|^2|^3"
+ },
+ "conflict": {
+ "doctrine/cache": "< 1.11"
+ },
+ "require-dev": {
+ "doctrine/cache": "^1.11|^2.0",
+ "doctrine/coding-standard": "14.0.0",
+ "fig/log-test": "^1",
+ "jetbrains/phpstorm-stubs": "2023.1",
+ "phpstan/phpstan": "2.1.30",
+ "phpstan/phpstan-strict-rules": "^2",
+ "phpunit/phpunit": "9.6.34",
+ "slevomat/coding-standard": "8.27.1",
+ "squizlabs/php_codesniffer": "4.0.1",
+ "symfony/cache": "^5.4|^6.0|^7.0|^8.0",
+ "symfony/console": "^4.4|^5.4|^6.0|^7.0|^8.0"
+ },
+ "suggest": {
+ "symfony/console": "For helpful console commands such as SQL execution and import of files."
+ },
+ "bin": [
+ "bin/doctrine-dbal"
+ ],
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\DBAL\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Guilherme Blanco",
+ "email": "guilhermeblanco@gmail.com"
+ },
+ {
+ "name": "Roman Borschel",
+ "email": "roman@code-factory.org"
+ },
+ {
+ "name": "Benjamin Eberlei",
+ "email": "kontakt@beberlei.de"
+ },
+ {
+ "name": "Jonathan Wage",
+ "email": "jonwage@gmail.com"
+ }
+ ],
+ "description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.",
+ "homepage": "https://www.doctrine-project.org/projects/dbal.html",
+ "keywords": [
+ "abstraction",
+ "database",
+ "db2",
+ "dbal",
+ "mariadb",
+ "mssql",
+ "mysql",
+ "oci8",
+ "oracle",
+ "pdo",
+ "pgsql",
+ "postgresql",
+ "queryobject",
+ "sasql",
+ "sql",
+ "sqlite",
+ "sqlserver",
+ "sqlsrv"
+ ],
+ "support": {
+ "issues": "https://github.com/doctrine/dbal/issues",
+ "source": "https://github.com/doctrine/dbal/tree/3.10.6"
+ },
+ "funding": [
+ {
+ "url": "https://www.doctrine-project.org/sponsorship.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://www.patreon.com/phpdoctrine",
+ "type": "patreon"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdbal",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-07-21T12:57:49+00:00"
+ },
+ {
+ "name": "doctrine/deprecations",
+ "version": "1.1.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/deprecations.git",
+ "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca",
+ "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1 || ^8.0"
+ },
+ "conflict": {
+ "phpunit/phpunit": "<=7.5 || >=14"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^9 || ^12 || ^14",
+ "phpstan/phpstan": "1.4.10 || 2.1.30",
+ "phpstan/phpstan-phpunit": "^1.0 || ^2",
+ "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0",
+ "psr/log": "^1 || ^2 || ^3"
+ },
+ "suggest": {
+ "psr/log": "Allows logging deprecations via PSR-3 logger implementation"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Deprecations\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.",
+ "homepage": "https://www.doctrine-project.org/",
+ "support": {
+ "issues": "https://github.com/doctrine/deprecations/issues",
+ "source": "https://github.com/doctrine/deprecations/tree/1.1.6"
+ },
+ "time": "2026-02-07T07:09:04+00:00"
+ },
+ {
+ "name": "doctrine/doctrine-bundle",
+ "version": "2.19.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/DoctrineBundle.git",
+ "reference": "72b53688c30451a7c65addd50b2167d31459724a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/72b53688c30451a7c65addd50b2167d31459724a",
+ "reference": "72b53688c30451a7c65addd50b2167d31459724a",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/dbal": "^3.7.0 || ^4.0",
+ "doctrine/deprecations": "^1.0",
+ "doctrine/persistence": "^3.1 || ^4",
+ "doctrine/sql-formatter": "^1.0.1",
+ "php": "^8.1",
+ "symfony/cache": "^6.4 || ^7.0",
+ "symfony/config": "^6.4 || ^7.0",
+ "symfony/console": "^6.4 || ^7.0",
+ "symfony/dependency-injection": "^6.4 || ^7.0",
+ "symfony/doctrine-bridge": "^6.4.3 || ^7.0.3",
+ "symfony/framework-bundle": "^6.4 || ^7.0",
+ "symfony/service-contracts": "^2.5 || ^3"
+ },
+ "conflict": {
+ "doctrine/annotations": ">=3.0",
+ "doctrine/cache": "< 1.11",
+ "doctrine/orm": "<2.17 || >=4.0",
+ "symfony/var-exporter": "< 6.4.1 || 7.0.0",
+ "twig/twig": "<2.13 || >=3.0 <3.0.4 || >=5"
+ },
+ "require-dev": {
+ "doctrine/annotations": "^1 || ^2",
+ "doctrine/cache": "^1.11 || ^2.0",
+ "doctrine/coding-standard": "^14",
+ "doctrine/orm": "^2.17 || ^3.1",
+ "friendsofphp/proxy-manager-lts": "^1.0",
+ "phpstan/phpstan": "2.1.1",
+ "phpstan/phpstan-phpunit": "2.0.3",
+ "phpstan/phpstan-strict-rules": "^2",
+ "phpunit/phpunit": "^10.5.53 || ^12.3.10",
+ "psr/log": "^1.1.4 || ^2.0 || ^3.0",
+ "symfony/doctrine-messenger": "^6.4 || ^7.0",
+ "symfony/event-dispatcher": "^6.4 || ^7.0",
+ "symfony/expression-language": "^6.4 || ^7.0",
+ "symfony/http-kernel": "^6.4 || ^7.0",
+ "symfony/messenger": "^6.4 || ^7.0",
+ "symfony/property-info": "^6.4 || ^7.0",
+ "symfony/runtime": "^6.4 || ^7.0",
+ "symfony/security-bundle": "^6.4 || ^7.0",
+ "symfony/stopwatch": "^6.4 || ^7.0",
+ "symfony/string": "^6.4 || ^7.0",
+ "symfony/twig-bridge": "^6.4 || ^7.0",
+ "symfony/validator": "^6.4 || ^7.0",
+ "symfony/var-exporter": "^6.4.1 || ^7.0.1",
+ "symfony/web-profiler-bundle": "^6.4 || ^7.0",
+ "symfony/yaml": "^6.4 || ^7.0",
+ "twig/twig": "^2.14.7 || ^3.0.4 || ^4"
+ },
+ "suggest": {
+ "doctrine/orm": "The Doctrine ORM integration is optional in the bundle.",
+ "ext-pdo": "*",
+ "symfony/web-profiler-bundle": "To use the data collector."
+ },
+ "type": "symfony-bundle",
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Bundle\\DoctrineBundle\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Benjamin Eberlei",
+ "email": "kontakt@beberlei.de"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ },
+ {
+ "name": "Doctrine Project",
+ "homepage": "https://www.doctrine-project.org/"
+ }
+ ],
+ "description": "Symfony DoctrineBundle",
+ "homepage": "https://www.doctrine-project.org",
+ "keywords": [
+ "database",
+ "dbal",
+ "orm",
+ "persistence"
+ ],
+ "support": {
+ "issues": "https://github.com/doctrine/DoctrineBundle/issues",
+ "source": "https://github.com/doctrine/DoctrineBundle/tree/2.19.1"
+ },
+ "funding": [
+ {
+ "url": "https://www.doctrine-project.org/sponsorship.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://www.patreon.com/phpdoctrine",
+ "type": "patreon"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdoctrine-bundle",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-09-08T11:48:41+00:00"
+ },
+ {
+ "name": "doctrine/doctrine-migrations-bundle",
+ "version": "3.7.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/DoctrineMigrationsBundle.git",
+ "reference": "00056695242a3e88369fe7a6e10669d8f6d7496a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/DoctrineMigrationsBundle/zipball/00056695242a3e88369fe7a6e10669d8f6d7496a",
+ "reference": "00056695242a3e88369fe7a6e10669d8f6d7496a",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/dbal": "^2 || ^3 || ^4",
+ "doctrine/doctrine-bundle": "^2.4 || ^3.0",
+ "doctrine/migrations": "^3.2",
+ "php": "^7.2 || ^8.0",
+ "psr/log": "^1 || ^2 || ^3",
+ "symfony/config": "^5.4 || ^6.0 || ^7.0 || ^8.0",
+ "symfony/console": "^5.4 || ^6.0 || ^7.0 || ^8.0",
+ "symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0 || ^8.0",
+ "symfony/deprecation-contracts": "^2.1 || ^3",
+ "symfony/framework-bundle": "^5.4 || ^6.0 || ^7.0 || ^8.0",
+ "symfony/http-foundation": "^5.4 || ^6.0 || ^7.0 || ^8.0",
+ "symfony/http-kernel": "^5.4 || ^6.0 || ^7.0",
+ "symfony/service-contracts": "^1.0 || ^2.0 || ^3.0"
+ },
+ "require-dev": {
+ "composer/semver": "^3.0",
+ "doctrine/coding-standard": "^12 || ^14",
+ "doctrine/orm": "^2.6 || ^3",
+ "phpstan/phpstan": "^1.4 || ^2",
+ "phpstan/phpstan-deprecation-rules": "^1 || ^2",
+ "phpstan/phpstan-phpunit": "^1 || ^2",
+ "phpstan/phpstan-strict-rules": "^1.1 || ^2",
+ "phpstan/phpstan-symfony": "^1.3 || ^2",
+ "phpunit/phpunit": "^8.5 || ^9.5",
+ "symfony/phpunit-bridge": "^6.3 || ^7 || ^8",
+ "symfony/var-exporter": "^5.4 || ^6 || ^7 || ^8"
+ },
+ "type": "symfony-bundle",
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Bundle\\MigrationsBundle\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Doctrine Project",
+ "homepage": "https://www.doctrine-project.org"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony DoctrineMigrationsBundle",
+ "homepage": "https://www.doctrine-project.org",
+ "keywords": [
+ "dbal",
+ "migrations",
+ "schema"
+ ],
+ "support": {
+ "issues": "https://github.com/doctrine/DoctrineMigrationsBundle/issues",
+ "source": "https://github.com/doctrine/DoctrineMigrationsBundle/tree/3.7.1"
+ },
+ "funding": [
+ {
+ "url": "https://www.doctrine-project.org/sponsorship.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://www.patreon.com/phpdoctrine",
+ "type": "patreon"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdoctrine-migrations-bundle",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-08-26T05:40:19+00:00"
+ },
+ {
+ "name": "doctrine/event-manager",
+ "version": "2.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/event-manager.git",
+ "reference": "dda33921b198841ca8dbad2eaa5d4d34769d18cf"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/event-manager/zipball/dda33921b198841ca8dbad2eaa5d4d34769d18cf",
+ "reference": "dda33921b198841ca8dbad2eaa5d4d34769d18cf",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.1"
+ },
+ "conflict": {
+ "doctrine/common": "<2.9"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^14",
+ "phpdocumentor/guides-cli": "^1.4",
+ "phpstan/phpstan": "^2.1.32",
+ "phpunit/phpunit": "^10.5.58"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Common\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Guilherme Blanco",
+ "email": "guilhermeblanco@gmail.com"
+ },
+ {
+ "name": "Roman Borschel",
+ "email": "roman@code-factory.org"
+ },
+ {
+ "name": "Benjamin Eberlei",
+ "email": "kontakt@beberlei.de"
+ },
+ {
+ "name": "Jonathan Wage",
+ "email": "jonwage@gmail.com"
+ },
+ {
+ "name": "Johannes Schmitt",
+ "email": "schmittjoh@gmail.com"
+ },
+ {
+ "name": "Marco Pivetta",
+ "email": "ocramius@gmail.com"
+ }
+ ],
+ "description": "The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.",
+ "homepage": "https://www.doctrine-project.org/projects/event-manager.html",
+ "keywords": [
+ "event",
+ "event dispatcher",
+ "event manager",
+ "event system",
+ "events"
+ ],
+ "support": {
+ "issues": "https://github.com/doctrine/event-manager/issues",
+ "source": "https://github.com/doctrine/event-manager/tree/2.1.1"
+ },
+ "funding": [
+ {
+ "url": "https://www.doctrine-project.org/sponsorship.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://www.patreon.com/phpdoctrine",
+ "type": "patreon"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fevent-manager",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-29T07:11:08+00:00"
+ },
+ {
+ "name": "doctrine/inflector",
+ "version": "2.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/inflector.git",
+ "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b",
+ "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^12.0 || ^13.0",
+ "phpstan/phpstan": "^1.12 || ^2.0",
+ "phpstan/phpstan-phpunit": "^1.4 || ^2.0",
+ "phpstan/phpstan-strict-rules": "^1.6 || ^2.0",
+ "phpunit/phpunit": "^8.5 || ^12.2"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Inflector\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Guilherme Blanco",
+ "email": "guilhermeblanco@gmail.com"
+ },
+ {
+ "name": "Roman Borschel",
+ "email": "roman@code-factory.org"
+ },
+ {
+ "name": "Benjamin Eberlei",
+ "email": "kontakt@beberlei.de"
+ },
+ {
+ "name": "Jonathan Wage",
+ "email": "jonwage@gmail.com"
+ },
+ {
+ "name": "Johannes Schmitt",
+ "email": "schmittjoh@gmail.com"
+ }
+ ],
+ "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.",
+ "homepage": "https://www.doctrine-project.org/projects/inflector.html",
+ "keywords": [
+ "inflection",
+ "inflector",
+ "lowercase",
+ "manipulation",
+ "php",
+ "plural",
+ "singular",
+ "strings",
+ "uppercase",
+ "words"
+ ],
+ "support": {
+ "issues": "https://github.com/doctrine/inflector/issues",
+ "source": "https://github.com/doctrine/inflector/tree/2.1.0"
+ },
+ "funding": [
+ {
+ "url": "https://www.doctrine-project.org/sponsorship.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://www.patreon.com/phpdoctrine",
+ "type": "patreon"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-08-10T19:31:58+00:00"
+ },
+ {
+ "name": "doctrine/instantiator",
+ "version": "2.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/instantiator.git",
+ "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0",
+ "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.1"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^11",
+ "ext-pdo": "*",
+ "ext-phar": "*",
+ "phpbench/phpbench": "^1.2",
+ "phpstan/phpstan": "^1.9.4",
+ "phpstan/phpstan-phpunit": "^1.3",
+ "phpunit/phpunit": "^9.5.27",
+ "vimeo/psalm": "^5.4"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Marco Pivetta",
+ "email": "ocramius@gmail.com",
+ "homepage": "https://ocramius.github.io/"
+ }
+ ],
+ "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors",
+ "homepage": "https://www.doctrine-project.org/projects/instantiator.html",
+ "keywords": [
+ "constructor",
+ "instantiate"
+ ],
+ "support": {
+ "issues": "https://github.com/doctrine/instantiator/issues",
+ "source": "https://github.com/doctrine/instantiator/tree/2.0.0"
+ },
+ "funding": [
+ {
+ "url": "https://www.doctrine-project.org/sponsorship.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://www.patreon.com/phpdoctrine",
+ "type": "patreon"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2022-12-30T00:23:10+00:00"
+ },
+ {
+ "name": "doctrine/lexer",
+ "version": "3.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/lexer.git",
+ "reference": "71d305eaa6480d3d3460c7d053d1da7749fa9cca"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/lexer/zipball/71d305eaa6480d3d3460c7d053d1da7749fa9cca",
+ "reference": "71d305eaa6480d3d3460c7d053d1da7749fa9cca",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.1"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^14",
+ "phpstan/phpstan": "^2",
+ "phpunit/phpunit": "^10.5.58 || ^12.5.4"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Common\\Lexer\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Guilherme Blanco",
+ "email": "guilhermeblanco@gmail.com"
+ },
+ {
+ "name": "Roman Borschel",
+ "email": "roman@code-factory.org"
+ },
+ {
+ "name": "Johannes Schmitt",
+ "email": "schmittjoh@gmail.com"
+ }
+ ],
+ "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.",
+ "homepage": "https://www.doctrine-project.org/projects/lexer.html",
+ "keywords": [
+ "annotations",
+ "docblock",
+ "lexer",
+ "parser",
+ "php"
+ ],
+ "support": {
+ "issues": "https://github.com/doctrine/lexer/issues",
+ "source": "https://github.com/doctrine/lexer/tree/3.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://www.doctrine-project.org/sponsorship.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://www.patreon.com/phpdoctrine",
+ "type": "patreon"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-10-01T06:59:29+00:00"
+ },
+ {
+ "name": "doctrine/migrations",
+ "version": "3.9.8",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/migrations.git",
+ "reference": "eb0b9ce3f999d720fa73037bbd06977ea34b76a0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/migrations/zipball/eb0b9ce3f999d720fa73037bbd06977ea34b76a0",
+ "reference": "eb0b9ce3f999d720fa73037bbd06977ea34b76a0",
+ "shasum": ""
+ },
+ "require": {
+ "composer-runtime-api": "^2",
+ "doctrine/dbal": "^3.6 || ^4",
+ "doctrine/deprecations": "^0.5.3 || ^1",
+ "doctrine/event-manager": "^1.2 || ^2.0",
+ "php": "^8.1",
+ "psr/log": "^1.1.3 || ^2 || ^3",
+ "symfony/console": "^5.4 || ^6.0 || ^7.0 || ^8.0",
+ "symfony/stopwatch": "^5.4 || ^6.0 || ^7.0 || ^8.0",
+ "symfony/var-exporter": "^6.2 || ^7.0 || ^8.0"
+ },
+ "conflict": {
+ "doctrine/orm": "<2.12 || >=4"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^14",
+ "doctrine/orm": "^2.13 || ^3",
+ "doctrine/persistence": "^2 || ^3 || ^4",
+ "doctrine/sql-formatter": "^1.0",
+ "ext-pdo_sqlite": "*",
+ "fig/log-test": "^1",
+ "phpstan/phpstan": "^2",
+ "phpstan/phpstan-deprecation-rules": "^2",
+ "phpstan/phpstan-phpunit": "^2",
+ "phpstan/phpstan-strict-rules": "^2",
+ "phpstan/phpstan-symfony": "^2",
+ "phpunit/phpunit": "^10.3 || ^11.0 || ^12.0",
+ "symfony/cache": "^5.4 || ^6.0 || ^7.0 || ^8.0",
+ "symfony/process": "^5.4 || ^6.0 || ^7.0 || ^8.0",
+ "symfony/yaml": "^5.4 || ^6.0 || ^7.0 || ^8.0"
+ },
+ "suggest": {
+ "doctrine/sql-formatter": "Allows to generate formatted SQL with the diff command.",
+ "symfony/yaml": "Allows the use of yaml for migration configuration files."
+ },
+ "bin": [
+ "bin/doctrine-migrations"
+ ],
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Migrations\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Benjamin Eberlei",
+ "email": "kontakt@beberlei.de"
+ },
+ {
+ "name": "Jonathan Wage",
+ "email": "jonwage@gmail.com"
+ },
+ {
+ "name": "Michael Simonson",
+ "email": "contact@mikesimonson.com"
+ }
+ ],
+ "description": "PHP Doctrine Migrations project offer additional functionality on top of the database abstraction layer (DBAL) for versioning your database schema and easily deploying changes to it. It is a very easy to use and a powerful tool.",
+ "homepage": "https://www.doctrine-project.org/projects/migrations.html",
+ "keywords": [
+ "database",
+ "dbal",
+ "migrations"
+ ],
+ "support": {
+ "issues": "https://github.com/doctrine/migrations/issues",
+ "source": "https://github.com/doctrine/migrations/tree/3.9.8"
+ },
+ "funding": [
+ {
+ "url": "https://www.doctrine-project.org/sponsorship.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://www.patreon.com/phpdoctrine",
+ "type": "patreon"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fmigrations",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-10-04T12:35:58+00:00"
+ },
+ {
+ "name": "doctrine/orm",
+ "version": "3.6.9",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/orm.git",
+ "reference": "1ee072c5734f1d4e41d67eff6587f29c96db51c8"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/orm/zipball/1ee072c5734f1d4e41d67eff6587f29c96db51c8",
+ "reference": "1ee072c5734f1d4e41d67eff6587f29c96db51c8",
+ "shasum": ""
+ },
+ "require": {
+ "composer-runtime-api": "^2",
+ "doctrine/collections": "^2.2",
+ "doctrine/dbal": "^3.8.2 || ^4",
+ "doctrine/deprecations": "^0.5.3 || ^1",
+ "doctrine/event-manager": "^1.2 || ^2",
+ "doctrine/inflector": "^1.4 || ^2.0",
+ "doctrine/instantiator": "^1.3 || ^2",
+ "doctrine/lexer": "^3",
+ "doctrine/persistence": "^3.3.1 || ^4",
+ "ext-ctype": "*",
+ "php": "^8.1",
+ "psr/cache": "^1 || ^2 || ^3",
+ "symfony/console": "^5.4 || ^6.0 || ^7.0 || ^8.0",
+ "symfony/var-exporter": "^6.3.9 || ^7.0 || ^8.0"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^14.0",
+ "phpbench/phpbench": "^1.0",
+ "phpstan/extension-installer": "^1.4",
+ "phpstan/phpstan": "2.1.23",
+ "phpstan/phpstan-deprecation-rules": "^2",
+ "phpunit/phpunit": "^10.5.0 || ^11.5",
+ "psr/log": "^1 || ^2 || ^3",
+ "symfony/cache": "^5.4 || ^6.2 || ^7.0 || ^8.0"
+ },
+ "suggest": {
+ "ext-deepclone": "Improves performance when not using native lazy objects (Symfony 8.1+)",
+ "ext-dom": "Provides support for XSD validation for XML mapping files",
+ "symfony/cache": "Provides cache support for Setup Tool with doctrine/cache 2.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\ORM\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Guilherme Blanco",
+ "email": "guilhermeblanco@gmail.com"
+ },
+ {
+ "name": "Roman Borschel",
+ "email": "roman@code-factory.org"
+ },
+ {
+ "name": "Benjamin Eberlei",
+ "email": "kontakt@beberlei.de"
+ },
+ {
+ "name": "Jonathan Wage",
+ "email": "jonwage@gmail.com"
+ },
+ {
+ "name": "Marco Pivetta",
+ "email": "ocramius@gmail.com"
+ }
+ ],
+ "description": "Object-Relational-Mapper for PHP",
+ "homepage": "https://www.doctrine-project.org/projects/orm.html",
+ "keywords": [
+ "database",
+ "orm"
+ ],
+ "support": {
+ "issues": "https://github.com/doctrine/orm/issues",
+ "source": "https://github.com/doctrine/orm/tree/3.6.9"
+ },
+ "time": "2026-09-07T18:59:09+00:00"
+ },
+ {
+ "name": "doctrine/persistence",
+ "version": "3.4.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/persistence.git",
+ "reference": "703a29d8597336fb75f42eeabcdf3f2863156ca8"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/persistence/zipball/703a29d8597336fb75f42eeabcdf3f2863156ca8",
+ "reference": "703a29d8597336fb75f42eeabcdf3f2863156ca8",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/event-manager": "^1 || ^2",
+ "php": "^7.2 || ^8.0",
+ "psr/cache": "^1.0 || ^2.0 || ^3.0"
+ },
+ "conflict": {
+ "doctrine/common": "<2.10"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^12 || ^14",
+ "doctrine/common": "^3.0",
+ "phpstan/phpstan": "^1 || 2.1.30",
+ "phpstan/phpstan-phpunit": "^1 || ^2",
+ "phpstan/phpstan-strict-rules": "^1 || ^2",
+ "phpunit/phpunit": "^8.5.38 || ^9.5",
+ "symfony/cache": "^4.4 || ^5.4 || ^6.0 || ^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Persistence\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Guilherme Blanco",
+ "email": "guilhermeblanco@gmail.com"
+ },
+ {
+ "name": "Roman Borschel",
+ "email": "roman@code-factory.org"
+ },
+ {
+ "name": "Benjamin Eberlei",
+ "email": "kontakt@beberlei.de"
+ },
+ {
+ "name": "Jonathan Wage",
+ "email": "jonwage@gmail.com"
+ },
+ {
+ "name": "Johannes Schmitt",
+ "email": "schmittjoh@gmail.com"
+ },
+ {
+ "name": "Marco Pivetta",
+ "email": "ocramius@gmail.com"
+ }
+ ],
+ "description": "The Doctrine Persistence project is a set of shared interfaces and functionality that the different Doctrine object mappers share.",
+ "homepage": "https://www.doctrine-project.org/projects/persistence.html",
+ "keywords": [
+ "mapper",
+ "object",
+ "odm",
+ "orm",
+ "persistence"
+ ],
+ "support": {
+ "issues": "https://github.com/doctrine/persistence/issues",
+ "source": "https://github.com/doctrine/persistence/tree/3.4.5"
+ },
+ "funding": [
+ {
+ "url": "https://www.doctrine-project.org/sponsorship.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://www.patreon.com/phpdoctrine",
+ "type": "patreon"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fpersistence",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-06-13T19:29:35+00:00"
+ },
+ {
+ "name": "doctrine/sql-formatter",
+ "version": "1.5.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/sql-formatter.git",
+ "reference": "9563949f5cd3bd12a17d12fb980528bc141c5806"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/sql-formatter/zipball/9563949f5cd3bd12a17d12fb980528bc141c5806",
+ "reference": "9563949f5cd3bd12a17d12fb980528bc141c5806",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.1"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^14",
+ "ergebnis/phpunit-slow-test-detector": "^2.20",
+ "phpstan/phpstan": "^2.1.31",
+ "phpunit/phpunit": "^10.5.58"
+ },
+ "bin": [
+ "bin/sql-formatter"
+ ],
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\SqlFormatter\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jeremy Dorn",
+ "email": "jeremy@jeremydorn.com",
+ "homepage": "https://jeremydorn.com/"
+ }
+ ],
+ "description": "a PHP SQL highlighting library",
+ "homepage": "https://github.com/doctrine/sql-formatter/",
+ "keywords": [
+ "highlight",
+ "sql"
+ ],
+ "support": {
+ "issues": "https://github.com/doctrine/sql-formatter/issues",
+ "source": "https://github.com/doctrine/sql-formatter/tree/1.5.4"
+ },
+ "time": "2026-02-08T16:21:46+00:00"
+ },
+ {
+ "name": "egulias/email-validator",
+ "version": "4.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/egulias/EmailValidator.git",
+ "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa",
+ "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/lexer": "^2.0 || ^3.0",
+ "php": ">=8.1",
+ "symfony/polyfill-intl-idn": "^1.26"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^10.2",
+ "vimeo/psalm": "^5.12"
+ },
+ "suggest": {
+ "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Egulias\\EmailValidator\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Eduardo Gulias Davis"
+ }
+ ],
+ "description": "A library for validating emails against several RFCs",
+ "homepage": "https://github.com/egulias/EmailValidator",
+ "keywords": [
+ "email",
+ "emailvalidation",
+ "emailvalidator",
+ "validation",
+ "validator"
+ ],
+ "support": {
+ "issues": "https://github.com/egulias/EmailValidator/issues",
+ "source": "https://github.com/egulias/EmailValidator/tree/4.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/egulias",
+ "type": "github"
+ }
+ ],
+ "time": "2025-03-06T22:45:56+00:00"
+ },
+ {
+ "name": "fakerphp/faker",
+ "version": "v1.24.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/FakerPHP/Faker.git",
+ "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5",
+ "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4 || ^8.0",
+ "psr/container": "^1.0 || ^2.0",
+ "symfony/deprecation-contracts": "^2.2 || ^3.0"
+ },
+ "conflict": {
+ "fzaninotto/faker": "*"
+ },
+ "require-dev": {
+ "bamarni/composer-bin-plugin": "^1.4.1",
+ "doctrine/persistence": "^1.3 || ^2.0",
+ "ext-intl": "*",
+ "phpunit/phpunit": "^9.5.26",
+ "symfony/phpunit-bridge": "^5.4.16"
+ },
+ "suggest": {
+ "doctrine/orm": "Required to use Faker\\ORM\\Doctrine",
+ "ext-curl": "Required by Faker\\Provider\\Image to download images.",
+ "ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.",
+ "ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.",
+ "ext-mbstring": "Required for multibyte Unicode string functionality."
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Faker\\": "src/Faker/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "François Zaninotto"
+ }
+ ],
+ "description": "Faker is a PHP library that generates fake data for you.",
+ "keywords": [
+ "data",
+ "faker",
+ "fixtures"
+ ],
+ "support": {
+ "issues": "https://github.com/FakerPHP/Faker/issues",
+ "source": "https://github.com/FakerPHP/Faker/tree/v1.24.1"
+ },
+ "time": "2024-11-21T13:46:39+00:00"
+ },
+ {
+ "name": "friendsofphp/proxy-manager-lts",
+ "version": "v1.0.19",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/FriendsOfPHP/proxy-manager-lts.git",
+ "reference": "c20299aa9f48a622052964a75c5a4cef017398b2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/FriendsOfPHP/proxy-manager-lts/zipball/c20299aa9f48a622052964a75c5a4cef017398b2",
+ "reference": "c20299aa9f48a622052964a75c5a4cef017398b2",
+ "shasum": ""
+ },
+ "require": {
+ "laminas/laminas-code": "~3.4.1|^4.0",
+ "php": ">=7.1",
+ "symfony/filesystem": "^4.4.17|^5.0|^6.0|^7.0|^8.0"
+ },
+ "conflict": {
+ "laminas/laminas-stdlib": "<3.2.1",
+ "zendframework/zend-stdlib": "<3.2.1"
+ },
+ "replace": {
+ "ocramius/proxy-manager": "^2.1"
+ },
+ "require-dev": {
+ "ext-phar": "*",
+ "symfony/phpunit-bridge": "^5.4|^6.0|^7.0"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/Ocramius/ProxyManager",
+ "name": "ocramius/proxy-manager"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "ProxyManager\\": "src/ProxyManager"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Marco Pivetta",
+ "email": "ocramius@gmail.com",
+ "homepage": "https://ocramius.github.io/"
+ },
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ }
+ ],
+ "description": "Adding support for a wider range of PHP versions to ocramius/proxy-manager",
+ "homepage": "https://github.com/FriendsOfPHP/proxy-manager-lts",
+ "keywords": [
+ "aop",
+ "lazy loading",
+ "proxy",
+ "proxy pattern",
+ "service proxies"
+ ],
+ "support": {
+ "issues": "https://github.com/FriendsOfPHP/proxy-manager-lts/issues",
+ "source": "https://github.com/FriendsOfPHP/proxy-manager-lts/tree/v1.0.19"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/Ocramius",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/ocramius/proxy-manager",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-10-28T10:28:17+00:00"
+ },
+ {
+ "name": "gedmo/doctrine-extensions",
+ "version": "v3.22.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine-extensions/DoctrineExtensions.git",
+ "reference": "486719169b3042dd4cdb801f70651f30723aab9e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine-extensions/DoctrineExtensions/zipball/486719169b3042dd4cdb801f70651f30723aab9e",
+ "reference": "486719169b3042dd4cdb801f70651f30723aab9e",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/collections": "^1.2 || ^2.0 || ^3.1",
+ "doctrine/deprecations": "^1.0",
+ "doctrine/event-manager": "^1.2 || ^2.0",
+ "doctrine/persistence": "^2.2 || ^3.0 || ^4.0",
+ "php": "^7.4 || ^8.0",
+ "psr/cache": "^1 || ^2 || ^3",
+ "psr/clock": "^1",
+ "symfony/cache": "^5.4 || ^6.4 || ^7.3 || ^8.0",
+ "symfony/string": "^5.4 || ^6.4 || ^7.3 || ^8.0"
+ },
+ "conflict": {
+ "behat/transliterator": "<1.2 || >=2.0",
+ "doctrine/annotations": "<1.13 || >=3.0",
+ "doctrine/common": "<2.13 || >=4.0",
+ "doctrine/dbal": "<3.7 || >=5.0",
+ "doctrine/mongodb-odm": "<2.3 || >=3.0",
+ "doctrine/orm": "<2.20 || >=3.0 <3.3 || >=4.0"
+ },
+ "require-dev": {
+ "behat/transliterator": "^1.2",
+ "doctrine/annotations": "^1.13 || ^2.0",
+ "doctrine/cache": "^1.11 || ^2.0",
+ "doctrine/common": "^2.13 || ^3.0",
+ "doctrine/dbal": "^3.7 || ^4.0",
+ "doctrine/doctrine-bundle": "^2.3 || ^3.0",
+ "doctrine/mongodb-odm": "^2.3",
+ "doctrine/orm": "^2.20 || ^3.3",
+ "friendsofphp/php-cs-fixer": "^3.89",
+ "nesbot/carbon": "^2.71 || ^3.0",
+ "phpstan/phpstan": "^2.1.31",
+ "phpstan/phpstan-doctrine": "^2.0.1",
+ "phpstan/phpstan-phpunit": "^2.0.3",
+ "phpunit/phpunit": "^9.6",
+ "rector/rector": "^2.2.6",
+ "symfony/console": "^5.4 || ^6.4 || ^7.3 || ^8.0",
+ "symfony/doctrine-bridge": "^5.4 || ^6.4 || ^7.3 || ^8.0",
+ "symfony/phpunit-bridge": "^6.4 || ^7.3 || ^8.0",
+ "symfony/uid": "^5.4 || ^6.4 || ^7.3 || ^8.0",
+ "symfony/yaml": "^5.4 || ^6.4 || ^7.3 || ^8.0"
+ },
+ "suggest": {
+ "doctrine/mongodb-odm": "to use the extensions with the MongoDB ODM",
+ "doctrine/orm": "to use the extensions with the ORM"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Gedmo\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Gediminas Morkevicius",
+ "email": "gediminas.morkevicius@gmail.com"
+ },
+ {
+ "name": "Gustavo Falco",
+ "email": "comfortablynumb84@gmail.com"
+ },
+ {
+ "name": "David Buchmann",
+ "email": "david@liip.ch"
+ }
+ ],
+ "description": "Doctrine behavioral extensions",
+ "homepage": "http://gediminasm.org/",
+ "keywords": [
+ "Blameable",
+ "behaviors",
+ "doctrine",
+ "extensions",
+ "gedmo",
+ "loggable",
+ "nestedset",
+ "odm",
+ "orm",
+ "sluggable",
+ "sortable",
+ "timestampable",
+ "translatable",
+ "tree",
+ "uploadable"
+ ],
+ "support": {
+ "docs": "https://github.com/doctrine-extensions/DoctrineExtensions/tree/main/doc",
+ "issues": "https://github.com/doctrine-extensions/DoctrineExtensions/issues",
+ "source": "https://github.com/doctrine-extensions/DoctrineExtensions/tree/v3.22.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/l3pp4rd",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/mbabker",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/phansys",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/stof",
+ "type": "github"
+ }
+ ],
+ "time": "2026-09-29T18:56:23+00:00"
+ },
+ {
+ "name": "giggsey/libphonenumber-for-php",
+ "version": "8.13.45",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/giggsey/libphonenumber-for-php.git",
+ "reference": "142ccdd603e4eeef7de9a7ddbd9ae18c2651dbd6"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/giggsey/libphonenumber-for-php/zipball/142ccdd603e4eeef7de9a7ddbd9ae18c2651dbd6",
+ "reference": "142ccdd603e4eeef7de9a7ddbd9ae18c2651dbd6",
+ "shasum": ""
+ },
+ "require": {
+ "giggsey/locale": "^1.7|^2.0",
+ "php": ">=5.3.2",
+ "symfony/polyfill-mbstring": "^1.17"
+ },
+ "replace": {
+ "giggsey/libphonenumber-for-php-lite": "self.version"
+ },
+ "require-dev": {
+ "pear/pear-core-minimal": "^1.9",
+ "pear/pear_exception": "^1.0",
+ "pear/versioncontrol_git": "^0.5",
+ "phing/phing": "^2.7",
+ "php-coveralls/php-coveralls": "^1.0|^2.0",
+ "symfony/console": "^2.8|^3.0|^v4.4|^v5.2",
+ "symfony/phpunit-bridge": "^4.2 || ^5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "8.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "libphonenumber\\": "src/"
+ },
+ "exclude-from-classmap": [
+ "/src/data/",
+ "/src/carrier/data/",
+ "/src/geocoding/data/",
+ "/src/timezone/data/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "Apache-2.0"
+ ],
+ "authors": [
+ {
+ "name": "Joshua Gigg",
+ "email": "giggsey@gmail.com",
+ "homepage": "https://giggsey.com/"
+ }
+ ],
+ "description": "PHP Port of Google's libphonenumber",
+ "homepage": "https://github.com/giggsey/libphonenumber-for-php",
+ "keywords": [
+ "geocoding",
+ "geolocation",
+ "libphonenumber",
+ "mobile",
+ "phonenumber",
+ "validation"
+ ],
+ "support": {
+ "issues": "https://github.com/giggsey/libphonenumber-for-php/issues",
+ "source": "https://github.com/giggsey/libphonenumber-for-php"
+ },
+ "time": "2024-09-06T11:22:54+00:00"
+ },
+ {
+ "name": "giggsey/locale",
+ "version": "1.9",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/giggsey/Locale.git",
+ "reference": "b07f1eace8072ccc61445ad8fbd493ff9d783043"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/giggsey/Locale/zipball/b07f1eace8072ccc61445ad8fbd493ff9d783043",
+ "reference": "b07f1eace8072ccc61445ad8fbd493ff9d783043",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.2"
+ },
+ "require-dev": {
+ "pear/pear-core-minimal": "^1.9",
+ "pear/pear_exception": "^1.0",
+ "pear/versioncontrol_git": "^0.5",
+ "phing/phing": "~2.7",
+ "php-coveralls/php-coveralls": "^1.0|^2.0",
+ "phpunit/phpunit": "^4.8|^5.0",
+ "symfony/console": "^2.8|^3.0|^4.0",
+ "symfony/filesystem": "^2.8|^3.0|^4.0",
+ "symfony/finder": "^2.8|^3.0|^4.0",
+ "symfony/process": "^2.8|^3.0|^4.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Giggsey\\Locale\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Joshua Gigg",
+ "email": "giggsey@gmail.com",
+ "homepage": "http://giggsey.com/"
+ }
+ ],
+ "description": "Locale functions required by libphonenumber-for-php",
+ "support": {
+ "issues": "https://github.com/giggsey/Locale/issues",
+ "source": "https://github.com/giggsey/Locale/tree/master"
+ },
+ "time": "2020-07-07T11:16:24+00:00"
+ },
+ {
+ "name": "guzzlehttp/guzzle",
+ "version": "7.15.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/guzzle/guzzle.git",
+ "reference": "ee80339fd9177ba44c49cdb653ff02a4d1106b9a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/guzzle/guzzle/zipball/ee80339fd9177ba44c49cdb653ff02a4d1106b9a",
+ "reference": "ee80339fd9177ba44c49cdb653ff02a4d1106b9a",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "guzzlehttp/promises": "^2.5.3",
+ "guzzlehttp/psr7": "^2.13.1",
+ "php": "^7.2.5 || ^8.0",
+ "psr/http-client": "^1.0",
+ "symfony/deprecation-contracts": "^2.5 || ^3.0",
+ "symfony/polyfill-php80": "^1.25"
+ },
+ "provide": {
+ "psr/http-client-implementation": "1.0"
+ },
+ "require-dev": {
+ "bamarni/composer-bin-plugin": "^1.8.2",
+ "ext-curl": "*",
+ "guzzle/client-integration-tests": "3.0.3",
+ "guzzlehttp/test-server": "^0.7",
+ "php-http/message-factory": "^1.1",
+ "phpunit/phpunit": "^8.5.52 || ^9.6.34",
+ "psr/log": "^1.1 || ^2.0 || ^3.0"
+ },
+ "suggest": {
+ "ext-curl": "Required for CURL handler support",
+ "ext-intl": "Required for Internationalized Domain Name (IDN) support",
+ "psr/log": "Required for using the Log middleware"
+ },
+ "type": "library",
+ "extra": {
+ "bamarni-bin": {
+ "bin-links": true,
+ "forward-command": false
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/functions_include.php"
+ ],
+ "psr-4": {
+ "GuzzleHttp\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Graham Campbell",
+ "email": "hello@gjcampbell.co.uk",
+ "homepage": "https://github.com/GrahamCampbell"
+ },
+ {
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ },
+ {
+ "name": "Jeremy Lindblom",
+ "email": "jeremeamia@gmail.com",
+ "homepage": "https://github.com/jeremeamia"
+ },
+ {
+ "name": "George Mponos",
+ "email": "gmponos@gmail.com",
+ "homepage": "https://github.com/gmponos"
+ },
+ {
+ "name": "Tobias Nyholm",
+ "email": "tobias.nyholm@gmail.com",
+ "homepage": "https://github.com/Nyholm"
+ },
+ {
+ "name": "Márk Sági-Kazár",
+ "email": "mark.sagikazar@gmail.com",
+ "homepage": "https://github.com/sagikazarmark"
+ },
+ {
+ "name": "Tobias Schultze",
+ "email": "webmaster@tubo-world.de",
+ "homepage": "https://github.com/Tobion"
+ }
+ ],
+ "description": "Guzzle is a PHP HTTP client library",
+ "keywords": [
+ "client",
+ "curl",
+ "framework",
+ "http",
+ "http client",
+ "psr-18",
+ "psr-7",
+ "rest",
+ "web service"
+ ],
+ "support": {
+ "issues": "https://github.com/guzzle/guzzle/issues",
+ "source": "https://github.com/guzzle/guzzle/tree/7.15.5"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/GrahamCampbell",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/Nyholm",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-08-24T09:21:06+00:00"
+ },
+ {
+ "name": "guzzlehttp/promises",
+ "version": "2.5.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/guzzle/promises.git",
+ "reference": "cde49999552d185d64715fe9c1f77a2aadd2f9f1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/guzzle/promises/zipball/cde49999552d185d64715fe9c1f77a2aadd2f9f1",
+ "reference": "cde49999552d185d64715fe9c1f77a2aadd2f9f1",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2.5 || ^8.0",
+ "symfony/deprecation-contracts": "^2.5 || ^3.0"
+ },
+ "require-dev": {
+ "bamarni/composer-bin-plugin": "^1.8.2",
+ "phpunit/phpunit": "^8.5.52 || ^9.6.34"
+ },
+ "type": "library",
+ "extra": {
+ "bamarni-bin": {
+ "bin-links": true,
+ "forward-command": false
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "GuzzleHttp\\Promise\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Graham Campbell",
+ "email": "hello@gjcampbell.co.uk",
+ "homepage": "https://github.com/GrahamCampbell"
+ },
+ {
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ },
+ {
+ "name": "Tobias Nyholm",
+ "email": "tobias.nyholm@gmail.com",
+ "homepage": "https://github.com/Nyholm"
+ },
+ {
+ "name": "Tobias Schultze",
+ "email": "webmaster@tubo-world.de",
+ "homepage": "https://github.com/Tobion"
+ }
+ ],
+ "description": "Guzzle promises library",
+ "keywords": [
+ "promise"
+ ],
+ "support": {
+ "issues": "https://github.com/guzzle/promises/issues",
+ "source": "https://github.com/guzzle/promises/tree/2.5.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/GrahamCampbell",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/Nyholm",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-08-24T09:11:28+00:00"
+ },
+ {
+ "name": "guzzlehttp/psr7",
+ "version": "2.13.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/guzzle/psr7.git",
+ "reference": "95e7828100de18b4e269fb1703be530082d5166d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/guzzle/psr7/zipball/95e7828100de18b4e269fb1703be530082d5166d",
+ "reference": "95e7828100de18b4e269fb1703be530082d5166d",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2.5 || ^8.0",
+ "psr/http-factory": "^1.0",
+ "psr/http-message": "^1.1 || ^2.0",
+ "ralouphie/getallheaders": "^3.0",
+ "symfony/deprecation-contracts": "^2.5 || ^3.0",
+ "symfony/polyfill-php80": "^1.25"
+ },
+ "provide": {
+ "psr/http-factory-implementation": "1.0",
+ "psr/http-message-implementation": "1.0"
+ },
+ "require-dev": {
+ "bamarni/composer-bin-plugin": "^1.8.2",
+ "http-interop/http-factory-tests": "1.1.0",
+ "jshttp/mime-db": "1.54.0.1",
+ "phpunit/phpunit": "^8.5.52 || ^9.6.34"
+ },
+ "suggest": {
+ "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
+ },
+ "type": "library",
+ "extra": {
+ "bamarni-bin": {
+ "bin-links": true,
+ "forward-command": false
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "GuzzleHttp\\Psr7\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Graham Campbell",
+ "email": "hello@gjcampbell.co.uk",
+ "homepage": "https://github.com/GrahamCampbell"
+ },
+ {
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ },
+ {
+ "name": "George Mponos",
+ "email": "gmponos@gmail.com",
+ "homepage": "https://github.com/gmponos"
+ },
+ {
+ "name": "Tobias Nyholm",
+ "email": "tobias.nyholm@gmail.com",
+ "homepage": "https://github.com/Nyholm"
+ },
+ {
+ "name": "Márk Sági-Kazár",
+ "email": "mark.sagikazar@gmail.com",
+ "homepage": "https://github.com/sagikazarmark"
+ },
+ {
+ "name": "Tobias Schultze",
+ "email": "webmaster@tubo-world.de",
+ "homepage": "https://github.com/Tobion"
+ },
+ {
+ "name": "Márk Sági-Kazár",
+ "email": "mark.sagikazar@gmail.com",
+ "homepage": "https://sagikazarmark.hu"
+ }
+ ],
+ "description": "PSR-7 message implementation that also provides common utility methods",
+ "keywords": [
+ "http",
+ "message",
+ "psr-7",
+ "request",
+ "response",
+ "stream",
+ "uri",
+ "url"
+ ],
+ "support": {
+ "issues": "https://github.com/guzzle/psr7/issues",
+ "source": "https://github.com/guzzle/psr7/tree/2.13.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/GrahamCampbell",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/Nyholm",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-08-24T09:13:11+00:00"
+ },
+ {
+ "name": "imagine/imagine",
+ "version": "1.5.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-imagine/Imagine.git",
+ "reference": "dd57a4c290ff4d223d17bcd219dac9ac8bf1cc16"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-imagine/Imagine/zipball/dd57a4c290ff4d223d17bcd219dac9ac8bf1cc16",
+ "reference": "dd57a4c290ff4d223d17bcd219dac9ac8bf1cc16",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.8 || ^5.7 || ^6.5 || ^7.5 || ^8.4 || ^9.3"
+ },
+ "suggest": {
+ "ext-exif": "to read EXIF metadata",
+ "ext-gd": "to use the GD implementation",
+ "ext-gmagick": "to use the Gmagick implementation",
+ "ext-imagick": "to use the Imagick implementation"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-develop": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Imagine\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Bulat Shakirzyanov",
+ "email": "mallluhuct@gmail.com",
+ "homepage": "http://avalanche123.com"
+ }
+ ],
+ "description": "Image processing for PHP",
+ "homepage": "http://imagine.readthedocs.org/",
+ "keywords": [
+ "drawing",
+ "graphics",
+ "image manipulation",
+ "image processing"
+ ],
+ "support": {
+ "issues": "https://github.com/php-imagine/Imagine/issues",
+ "source": "https://github.com/php-imagine/Imagine/tree/1.5.4"
+ },
+ "time": "2026-06-04T10:05:48+00:00"
+ },
+ {
+ "name": "knplabs/gaufrette",
+ "version": "v0.11.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/KnpLabs/Gaufrette.git",
+ "reference": "3cc396cae0f7c7d3f965e6b69d8fc546ffa0f94c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/KnpLabs/Gaufrette/zipball/3cc396cae0f7c7d3f965e6b69d8fc546ffa0f94c",
+ "reference": "3cc396cae0f7c7d3f965e6b69d8fc546ffa0f94c",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4 || ^8.0"
+ },
+ "conflict": {
+ "microsoft/windowsazure": "<0.4.3"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "^3.9",
+ "mikey179/vfsstream": "v1.x-dev as 1.7.0",
+ "pedrotroller/php-cs-custom-fixer": "^2.28",
+ "phpspec/phpspec": "^7.0",
+ "phpunit/phpunit": "~8.0"
+ },
+ "suggest": {
+ "ext-fileinfo": "This extension is used to automatically detect the content-type of a file in the AwsS3, OpenCloud, AzureBlogStorage and GoogleCloudStorage adapters",
+ "knplabs/knp-gaufrette-bundle": "to use with Symfony"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "0.10.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-0": {
+ "Gaufrette": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "KnpLabs Team",
+ "homepage": "http://knplabs.com"
+ },
+ {
+ "name": "The contributors",
+ "homepage": "http://github.com/knplabs/Gaufrette/contributors"
+ }
+ ],
+ "description": "PHP library that provides a filesystem abstraction layer",
+ "homepage": "http://knplabs.com",
+ "keywords": [
+ "abstraction",
+ "file",
+ "filesystem",
+ "media"
+ ],
+ "support": {
+ "issues": "https://github.com/KnpLabs/Gaufrette/issues",
+ "source": "https://github.com/KnpLabs/Gaufrette/tree/v0.11.1"
+ },
+ "time": "2022-11-03T17:26:17+00:00"
+ },
+ {
+ "name": "knplabs/knp-gaufrette-bundle",
+ "version": "v0.9.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/KnpLabs/KnpGaufretteBundle.git",
+ "reference": "910a16cd7a7af5d72a98975f3e59a0c6feadec32"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/KnpLabs/KnpGaufretteBundle/zipball/910a16cd7a7af5d72a98975f3e59a0c6feadec32",
+ "reference": "910a16cd7a7af5d72a98975f3e59a0c6feadec32",
+ "shasum": ""
+ },
+ "require": {
+ "knplabs/gaufrette": "^0.11",
+ "php": "^7.4 || ^8.0",
+ "symfony/config": "^5.0|^6.0|^7.0",
+ "symfony/dependency-injection": "^5.0|^6.0|^7.0",
+ "symfony/http-kernel": "^5.0|^6.0|^7.0"
+ },
+ "require-dev": {
+ "symfony/console": "^5.0|^6.0|^7.0",
+ "symfony/filesystem": "^5.0|^6.0|^7.0",
+ "symfony/phpunit-bridge": "^7.0",
+ "symfony/yaml": "^5.0|^6.0|^7.0"
+ },
+ "type": "symfony-bundle",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "0.8.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Knp\\Bundle\\GaufretteBundle\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Antoine Hérault",
+ "email": "antoine.herault@gmail.com"
+ },
+ {
+ "name": "The contributors",
+ "homepage": "https://github.com/knplabs/KnpGaufretteBundle/contributors"
+ }
+ ],
+ "description": "Allows to easily use the Gaufrette library in a Symfony project",
+ "homepage": "http://knplabs.com",
+ "keywords": [
+ "abstraction",
+ "file",
+ "filesystem",
+ "media"
+ ],
+ "support": {
+ "issues": "https://github.com/KnpLabs/KnpGaufretteBundle/issues",
+ "source": "https://github.com/KnpLabs/KnpGaufretteBundle/tree/v0.9.0"
+ },
+ "time": "2023-12-18T09:53:43+00:00"
+ },
+ {
+ "name": "knplabs/knp-menu",
+ "version": "v3.8.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/KnpLabs/KnpMenu.git",
+ "reference": "79d325909a1d428a93f1a0f55e90177830e283bb"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/KnpLabs/KnpMenu/zipball/79d325909a1d428a93f1a0f55e90177830e283bb",
+ "reference": "79d325909a1d428a93f1a0f55e90177830e283bb",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.1"
+ },
+ "conflict": {
+ "symfony/http-foundation": "<5.4",
+ "twig/twig": "<2.16"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^2.1",
+ "phpunit/phpunit": "^9.6",
+ "psr/container": "^1.0 || ^2.0",
+ "symfony/http-foundation": "^5.4 || ^6.0 || ^7.0",
+ "symfony/phpunit-bridge": "^7.0",
+ "symfony/routing": "^5.4 || ^6.0 || ^7.0",
+ "twig/twig": "^2.16 || ^3.0"
+ },
+ "suggest": {
+ "twig/twig": "for the TwigRenderer and the integration with your templates"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Knp\\Menu\\": "src/Knp/Menu"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "KnpLabs",
+ "homepage": "https://knplabs.com"
+ },
+ {
+ "name": "Christophe Coevoet",
+ "email": "stof@notk.org"
+ },
+ {
+ "name": "The Community",
+ "homepage": "https://github.com/KnpLabs/KnpMenu/contributors"
+ }
+ ],
+ "description": "An object oriented menu library",
+ "homepage": "https://knplabs.com",
+ "keywords": [
+ "menu",
+ "tree"
+ ],
+ "support": {
+ "issues": "https://github.com/KnpLabs/KnpMenu/issues",
+ "source": "https://github.com/KnpLabs/KnpMenu/tree/v3.8.0"
+ },
+ "time": "2025-06-13T15:03:33+00:00"
+ },
+ {
+ "name": "knplabs/knp-menu-bundle",
+ "version": "v3.7.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/KnpLabs/KnpMenuBundle.git",
+ "reference": "aa22e57f8f41c34ad5e382aae4d0c12998c0eb5a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/KnpLabs/KnpMenuBundle/zipball/aa22e57f8f41c34ad5e382aae4d0c12998c0eb5a",
+ "reference": "aa22e57f8f41c34ad5e382aae4d0c12998c0eb5a",
+ "shasum": ""
+ },
+ "require": {
+ "knplabs/knp-menu": "^3.8",
+ "php": "^8.1",
+ "symfony/config": "^6.4 | ^7.0 | ^8.0",
+ "symfony/dependency-injection": "^6.4 | ^7.0 | ^8.0",
+ "symfony/deprecation-contracts": "^2.5 | ^3.3",
+ "symfony/http-kernel": "^6.4 | ^7.0 | ^8.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^10.5 | ^11.5 | ^12.4",
+ "symfony/expression-language": "^6.4 | ^7.0 | ^8.0",
+ "symfony/phpunit-bridge": "^7.0 | ^8.0",
+ "symfony/templating": "^6.4 | ^7.0 | ^8.0"
+ },
+ "type": "symfony-bundle",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Knp\\Bundle\\MenuBundle\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Knplabs",
+ "homepage": "http://knplabs.com"
+ },
+ {
+ "name": "Christophe Coevoet",
+ "email": "stof@notk.org"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://github.com/KnpLabs/KnpMenuBundle/contributors"
+ }
+ ],
+ "description": "This bundle provides an integration of the KnpMenu library",
+ "keywords": [
+ "menu"
+ ],
+ "support": {
+ "issues": "https://github.com/KnpLabs/KnpMenuBundle/issues",
+ "source": "https://github.com/KnpLabs/KnpMenuBundle/tree/v3.7.0"
+ },
+ "time": "2025-11-30T08:30:04+00:00"
+ },
+ {
+ "name": "knplabs/knp-snappy",
+ "version": "v1.7.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/KnpLabs/snappy.git",
+ "reference": "f749a7d2e0f1260f4a0f96925323e34ede898f1f"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/KnpLabs/snappy/zipball/f749a7d2e0f1260f4a0f96925323e34ede898f1f",
+ "reference": "f749a7d2e0f1260f4a0f96925323e34ede898f1f",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "psr/log": "^2.0||^3.0",
+ "symfony/process": "^5.0||^6.0||^7.0||^8.0"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "^3.0",
+ "pedrotroller/php-cs-custom-fixer": "^2.19",
+ "phpstan/phpstan": "^2.1.39",
+ "phpstan/phpstan-phpunit": "^2.0.15",
+ "phpunit/phpunit": "^9.6.29"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Knp\\Snappy\\": "src/Knp/Snappy"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "KNP Labs Team",
+ "homepage": "http://knplabs.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "http://github.com/KnpLabs/snappy/contributors"
+ }
+ ],
+ "description": "PHP library allowing thumbnail, snapshot or PDF generation from a url or a html page. Wrapper for wkhtmltopdf/wkhtmltoimage.",
+ "homepage": "http://github.com/KnpLabs/snappy",
+ "keywords": [
+ "knp",
+ "knplabs",
+ "pdf",
+ "snapshot",
+ "thumbnail",
+ "wkhtmltopdf"
+ ],
+ "support": {
+ "issues": "https://github.com/KnpLabs/snappy/issues",
+ "source": "https://github.com/KnpLabs/snappy/tree/v1.7.3"
+ },
+ "time": "2026-07-29T11:03:07+00:00"
+ },
+ {
+ "name": "knplabs/knp-snappy-bundle",
+ "version": "v1.10.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/KnpLabs/KnpSnappyBundle.git",
+ "reference": "9501e76b63158bc6c390065ede40963d5a94b065"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/KnpLabs/KnpSnappyBundle/zipball/9501e76b63158bc6c390065ede40963d5a94b065",
+ "reference": "9501e76b63158bc6c390065ede40963d5a94b065",
+ "shasum": ""
+ },
+ "require": {
+ "knplabs/knp-snappy": "^1.4.3",
+ "php": ">=8.1",
+ "symfony/framework-bundle": "^5.1|^6.0|^7.0|^8.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^8.5",
+ "symfony/yaml": "^5.1|^6.0|^7.0|^8.0"
+ },
+ "type": "symfony-bundle",
+ "autoload": {
+ "psr-4": {
+ "Knp\\Bundle\\SnappyBundle\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "KnpLabs Team",
+ "homepage": "http://knplabs.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "http://github.com/KnpLabs/KnpSnappyBundle/contributors"
+ }
+ ],
+ "description": "Easily create PDF and images in Symfony by converting Twig/HTML templates.",
+ "homepage": "http://github.com/KnpLabs/KnpSnappyBundle",
+ "keywords": [
+ "bundle",
+ "knp",
+ "knplabs",
+ "pdf",
+ "snappy"
+ ],
+ "support": {
+ "issues": "https://github.com/KnpLabs/KnpSnappyBundle/issues",
+ "source": "https://github.com/KnpLabs/KnpSnappyBundle/tree/v1.10.6"
+ },
+ "time": "2026-01-07T08:05:32+00:00"
+ },
+ {
+ "name": "laminas/laminas-code",
+ "version": "4.17.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/laminas/laminas-code.git",
+ "reference": "40d61e2899ec17c5d08bbc0a2d586b3ca17ab9bd"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/laminas/laminas-code/zipball/40d61e2899ec17c5d08bbc0a2d586b3ca17ab9bd",
+ "reference": "40d61e2899ec17c5d08bbc0a2d586b3ca17ab9bd",
+ "shasum": ""
+ },
+ "require": {
+ "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0"
+ },
+ "require-dev": {
+ "doctrine/annotations": "^2.0.1",
+ "ext-phar": "*",
+ "laminas/laminas-coding-standard": "^3.0.0",
+ "laminas/laminas-stdlib": "^3.18.0",
+ "phpunit/phpunit": "^10.5.58",
+ "psalm/plugin-phpunit": "^0.19.0",
+ "vimeo/psalm": "^5.15.0"
+ },
+ "suggest": {
+ "doctrine/annotations": "Doctrine\\Common\\Annotations >=1.0 for annotation features",
+ "laminas/laminas-stdlib": "Laminas\\Stdlib component"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Laminas\\Code\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "description": "Extensions to the PHP Reflection API, static code scanning, and code generation",
+ "homepage": "https://laminas.dev",
+ "keywords": [
+ "code",
+ "laminas",
+ "laminasframework"
+ ],
+ "support": {
+ "chat": "https://laminas.dev/chat",
+ "docs": "https://docs.laminas.dev/laminas-code/",
+ "forum": "https://discourse.laminas.dev",
+ "issues": "https://github.com/laminas/laminas-code/issues",
+ "rss": "https://github.com/laminas/laminas-code/releases.atom",
+ "source": "https://github.com/laminas/laminas-code"
+ },
+ "funding": [
+ {
+ "url": "https://funding.communitybridge.org/projects/laminas-project",
+ "type": "community_bridge"
+ }
+ ],
+ "time": "2025-11-01T09:38:14+00:00"
+ },
+ {
+ "name": "laminas/laminas-stdlib",
+ "version": "3.21.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/laminas/laminas-stdlib.git",
+ "reference": "b1c81514cfe158aadf724c42b34d3d0a8164c096"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/laminas/laminas-stdlib/zipball/b1c81514cfe158aadf724c42b34d3d0a8164c096",
+ "reference": "b1c81514cfe158aadf724c42b34d3d0a8164c096",
+ "shasum": ""
+ },
+ "require": {
+ "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0"
+ },
+ "conflict": {
+ "zendframework/zend-stdlib": "*"
+ },
+ "require-dev": {
+ "laminas/laminas-coding-standard": "^3.1.0",
+ "phpbench/phpbench": "^1.4.1",
+ "phpunit/phpunit": "^11.5.42",
+ "psalm/plugin-phpunit": "^0.19.5",
+ "vimeo/psalm": "^6.13.1"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Laminas\\Stdlib\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "description": "SPL extensions, array utilities, error handlers, and more",
+ "homepage": "https://laminas.dev",
+ "keywords": [
+ "laminas",
+ "stdlib"
+ ],
+ "support": {
+ "chat": "https://laminas.dev/chat",
+ "docs": "https://docs.laminas.dev/laminas-stdlib/",
+ "forum": "https://discourse.laminas.dev",
+ "issues": "https://github.com/laminas/laminas-stdlib/issues",
+ "rss": "https://github.com/laminas/laminas-stdlib/releases.atom",
+ "source": "https://github.com/laminas/laminas-stdlib"
+ },
+ "funding": [
+ {
+ "url": "https://funding.communitybridge.org/projects/laminas-project",
+ "type": "community_bridge"
+ }
+ ],
+ "time": "2025-10-11T18:13:12+00:00"
+ },
+ {
+ "name": "lcobucci/jwt",
+ "version": "5.6.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/lcobucci/jwt.git",
+ "reference": "bb3e9f21e4196e8afc41def81ef649c164bca25e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/lcobucci/jwt/zipball/bb3e9f21e4196e8afc41def81ef649c164bca25e",
+ "reference": "bb3e9f21e4196e8afc41def81ef649c164bca25e",
+ "shasum": ""
+ },
+ "require": {
+ "ext-openssl": "*",
+ "ext-sodium": "*",
+ "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
+ "psr/clock": "^1.0"
+ },
+ "require-dev": {
+ "infection/infection": "^0.29",
+ "lcobucci/clock": "^3.2",
+ "lcobucci/coding-standard": "^11.0",
+ "phpbench/phpbench": "^1.2",
+ "phpstan/extension-installer": "^1.2",
+ "phpstan/phpstan": "^1.10.7",
+ "phpstan/phpstan-deprecation-rules": "^1.1.3",
+ "phpstan/phpstan-phpunit": "^1.3.10",
+ "phpstan/phpstan-strict-rules": "^1.5.0",
+ "phpunit/phpunit": "^11.1"
+ },
+ "suggest": {
+ "lcobucci/clock": ">= 3.2"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Lcobucci\\JWT\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Luís Cobucci",
+ "email": "lcobucci@gmail.com",
+ "role": "Developer"
+ }
+ ],
+ "description": "A simple library to work with JSON Web Token and JSON Web Signature",
+ "keywords": [
+ "JWS",
+ "jwt"
+ ],
+ "support": {
+ "issues": "https://github.com/lcobucci/jwt/issues",
+ "source": "https://github.com/lcobucci/jwt/tree/5.6.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/lcobucci",
+ "type": "github"
+ },
+ {
+ "url": "https://www.patreon.com/lcobucci",
+ "type": "patreon"
+ }
+ ],
+ "time": "2025-10-17T11:30:53+00:00"
+ },
+ {
+ "name": "league/flysystem",
+ "version": "3.36.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/thephpleague/flysystem.git",
+ "reference": "f7fb152932f30072d573510cbd4dd657d6475b25"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/f7fb152932f30072d573510cbd4dd657d6475b25",
+ "reference": "f7fb152932f30072d573510cbd4dd657d6475b25",
+ "shasum": ""
+ },
+ "require": {
+ "league/flysystem-local": "^3.0.0",
+ "league/mime-type-detection": "^1.0.0",
+ "php": "^8.0.2"
+ },
+ "conflict": {
+ "async-aws/core": "<1.19.0",
+ "async-aws/s3": "<1.14.0",
+ "aws/aws-sdk-php": "3.209.31 || 3.210.0",
+ "guzzlehttp/guzzle": "<7.0",
+ "guzzlehttp/ringphp": "<1.1.1",
+ "phpseclib/phpseclib": "3.0.15",
+ "symfony/http-client": "<5.2"
+ },
+ "require-dev": {
+ "async-aws/s3": "^1.5 || ^2.0",
+ "async-aws/simple-s3": "^1.1 || ^2.0",
+ "aws/aws-sdk-php": "^3.295.10",
+ "composer/semver": "^3.0",
+ "ext-fileinfo": "*",
+ "ext-ftp": "*",
+ "ext-mongodb": "^1.3|^2",
+ "ext-zip": "*",
+ "friendsofphp/php-cs-fixer": "^3.5",
+ "google/cloud-storage": "^1.23",
+ "guzzlehttp/psr7": "^2.6",
+ "microsoft/azure-storage-blob": "^1.1",
+ "mongodb/mongodb": "^1.2|^2",
+ "phpseclib/phpseclib": "^3.0.36",
+ "phpstan/phpstan": "^1.10",
+ "phpunit/phpunit": "^9.5.11|^10.0",
+ "sabre/dav": "^4.6.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "League\\Flysystem\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Frank de Jonge",
+ "email": "info@frankdejonge.nl"
+ }
+ ],
+ "description": "File storage abstraction for PHP",
+ "keywords": [
+ "WebDAV",
+ "aws",
+ "cloud",
+ "file",
+ "files",
+ "filesystem",
+ "filesystems",
+ "ftp",
+ "s3",
+ "sftp",
+ "storage"
+ ],
+ "support": {
+ "issues": "https://github.com/thephpleague/flysystem/issues",
+ "source": "https://github.com/thephpleague/flysystem/tree/3.36.0"
+ },
+ "time": "2026-09-02T08:00:27+00:00"
+ },
+ {
+ "name": "league/flysystem-bundle",
+ "version": "3.7.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/thephpleague/flysystem-bundle.git",
+ "reference": "9cccb6862c9fcd16e6d8a0fbd62254abb8017c99"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/thephpleague/flysystem-bundle/zipball/9cccb6862c9fcd16e6d8a0fbd62254abb8017c99",
+ "reference": "9cccb6862c9fcd16e6d8a0fbd62254abb8017c99",
+ "shasum": ""
+ },
+ "require": {
+ "league/flysystem": "^3.0",
+ "php": ">=8.2",
+ "symfony/config": "^6.0 || ^7.0 || ^8.0",
+ "symfony/dependency-injection": "^6.0 || ^7.0 || ^8.0",
+ "symfony/deprecation-contracts": "^2.1 || ^3",
+ "symfony/http-kernel": "^6.0 || ^7.0 || ^8.0",
+ "symfony/options-resolver": "^6.0 || ^7.0 || ^8.0"
+ },
+ "require-dev": {
+ "doctrine/mongodb-odm": "^2.0",
+ "league/flysystem-async-aws-s3": "^3.1",
+ "league/flysystem-aws-s3-v3": "^3.1",
+ "league/flysystem-azure-blob-storage": "^3.1",
+ "league/flysystem-ftp": "^3.1",
+ "league/flysystem-google-cloud-storage": "^3.1",
+ "league/flysystem-gridfs": "^3.28",
+ "league/flysystem-memory": "^3.1",
+ "league/flysystem-read-only": "^3.15",
+ "league/flysystem-sftp-v3": "^3.1",
+ "league/flysystem-webdav": "^3.29",
+ "platformcommunity/flysystem-bunnycdn": "^3.3",
+ "symfony/dotenv": "^6.0 || ^7.0 || ^8.0",
+ "symfony/framework-bundle": "^6.0 || ^7.0 || ^8.0",
+ "symfony/phpunit-bridge": "^6.0 || ^7.0 || ^8.0",
+ "symfony/var-dumper": "^6.0 || ^7.0 || ^8.0"
+ },
+ "type": "symfony-bundle",
+ "autoload": {
+ "psr-4": {
+ "League\\FlysystemBundle\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Titouan Galopin",
+ "email": "galopintitouan@gmail.com"
+ }
+ ],
+ "description": "Symfony bundle integrating Flysystem into Symfony applications",
+ "keywords": [
+ "Flysystem",
+ "bundle",
+ "filesystem",
+ "symfony"
+ ],
+ "support": {
+ "issues": "https://github.com/thephpleague/flysystem-bundle/issues",
+ "source": "https://github.com/thephpleague/flysystem-bundle/tree/3.7.1"
+ },
+ "time": "2026-08-10T15:55:07+00:00"
+ },
+ {
+ "name": "league/flysystem-local",
+ "version": "3.35.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/thephpleague/flysystem-local.git",
+ "reference": "a099b24dce160f3b2239043d13d47c4a1a214ea4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/a099b24dce160f3b2239043d13d47c4a1a214ea4",
+ "reference": "a099b24dce160f3b2239043d13d47c4a1a214ea4",
+ "shasum": ""
+ },
+ "require": {
+ "ext-fileinfo": "*",
+ "league/flysystem": "^3.0.0",
+ "league/mime-type-detection": "^1.0.0",
+ "php": "^8.0.2"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "League\\Flysystem\\Local\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Frank de Jonge",
+ "email": "info@frankdejonge.nl"
+ }
+ ],
+ "description": "Local filesystem adapter for Flysystem.",
+ "keywords": [
+ "Flysystem",
+ "file",
+ "files",
+ "filesystem",
+ "local"
+ ],
+ "support": {
+ "source": "https://github.com/thephpleague/flysystem-local/tree/3.35.3"
+ },
+ "time": "2026-08-12T13:29:21+00:00"
+ },
+ {
+ "name": "league/mime-type-detection",
+ "version": "1.17.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/thephpleague/mime-type-detection.git",
+ "reference": "f5f47eff7c48ed1003069a2ca67f316fb4021c76"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/f5f47eff7c48ed1003069a2ca67f316fb4021c76",
+ "reference": "f5f47eff7c48ed1003069a2ca67f316fb4021c76",
+ "shasum": ""
+ },
+ "require": {
+ "ext-fileinfo": "*",
+ "php": "^7.4 || ^8.0"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "^3.2",
+ "phpstan/phpstan": "^0.12.68",
+ "phpunit/phpunit": "^8.5.8 || ^9.3 || ^10.0 || ^11.0 || ^12.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "League\\MimeTypeDetection\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Frank de Jonge",
+ "email": "info@frankdejonge.nl"
+ }
+ ],
+ "description": "Mime-type detection for Flysystem",
+ "support": {
+ "issues": "https://github.com/thephpleague/mime-type-detection/issues",
+ "source": "https://github.com/thephpleague/mime-type-detection/tree/1.17.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/frankdejonge",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/league/flysystem",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-07-09T11:49:27+00:00"
+ },
+ {
+ "name": "league/uri",
+ "version": "7.8.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/thephpleague/uri.git",
+ "reference": "08cf38e3924d4f56238125547b5720496fac8fd4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/thephpleague/uri/zipball/08cf38e3924d4f56238125547b5720496fac8fd4",
+ "reference": "08cf38e3924d4f56238125547b5720496fac8fd4",
+ "shasum": ""
+ },
+ "require": {
+ "league/uri-interfaces": "^7.8.1",
+ "php": "^8.1",
+ "psr/http-factory": "^1"
+ },
+ "conflict": {
+ "league/uri-schemes": "^1.0"
+ },
+ "suggest": {
+ "ext-bcmath": "to improve IPV4 host parsing",
+ "ext-dom": "to convert the URI into an HTML anchor tag",
+ "ext-fileinfo": "to create Data URI from file contennts",
+ "ext-gmp": "to improve IPV4 host parsing",
+ "ext-intl": "to handle IDN host with the best performance",
+ "ext-uri": "to use the PHP native URI class",
+ "jeremykendall/php-domain-parser": "to further parse the URI host and resolve its Public Suffix and Top Level Domain",
+ "league/uri-components": "to provide additional tools to manipulate URI objects components",
+ "league/uri-polyfill": "to backport the PHP URI extension for older versions of PHP",
+ "php-64bit": "to improve IPV4 host parsing",
+ "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification",
+ "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "7.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "League\\Uri\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Ignace Nyamagana Butera",
+ "email": "nyamsprod@gmail.com",
+ "homepage": "https://nyamsprod.com"
+ }
+ ],
+ "description": "URI manipulation library",
+ "homepage": "https://uri.thephpleague.com",
+ "keywords": [
+ "URN",
+ "data-uri",
+ "file-uri",
+ "ftp",
+ "hostname",
+ "http",
+ "https",
+ "middleware",
+ "parse_str",
+ "parse_url",
+ "psr-7",
+ "query-string",
+ "querystring",
+ "rfc2141",
+ "rfc3986",
+ "rfc3987",
+ "rfc6570",
+ "rfc8141",
+ "uri",
+ "uri-template",
+ "url",
+ "ws"
+ ],
+ "support": {
+ "docs": "https://uri.thephpleague.com",
+ "forum": "https://thephpleague.slack.com",
+ "issues": "https://github.com/thephpleague/uri-src/issues",
+ "source": "https://github.com/thephpleague/uri/tree/7.8.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sponsors/nyamsprod",
+ "type": "github"
+ }
+ ],
+ "time": "2026-03-15T20:22:25+00:00"
+ },
+ {
+ "name": "league/uri-components",
+ "version": "7.8.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/thephpleague/uri-components.git",
+ "reference": "848ff9db2f0be06229d6034b7c2e33d41b4fd675"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/thephpleague/uri-components/zipball/848ff9db2f0be06229d6034b7c2e33d41b4fd675",
+ "reference": "848ff9db2f0be06229d6034b7c2e33d41b4fd675",
+ "shasum": ""
+ },
+ "require": {
+ "league/uri": "^7.8.1",
+ "php": "^8.1"
+ },
+ "suggest": {
+ "ext-bcmath": "to improve IPV4 host parsing",
+ "ext-fileinfo": "to create Data URI from file contennts",
+ "ext-gmp": "to improve IPV4 host parsing",
+ "ext-intl": "to handle IDN host with the best performance",
+ "ext-mbstring": "to use the sorting algorithm of URLSearchParams",
+ "jeremykendall/php-domain-parser": "to further parse the URI host and resolve its Public Suffix and Top Level Domain",
+ "league/uri-polyfill": "to backport the PHP URI extension for older versions of PHP",
+ "php-64bit": "to improve IPV4 host parsing",
+ "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification",
+ "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "7.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "League\\Uri\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Ignace Nyamagana Butera",
+ "email": "nyamsprod@gmail.com",
+ "homepage": "https://nyamsprod.com"
+ }
+ ],
+ "description": "URI components manipulation library",
+ "homepage": "http://uri.thephpleague.com",
+ "keywords": [
+ "authority",
+ "components",
+ "fragment",
+ "host",
+ "middleware",
+ "modifier",
+ "path",
+ "port",
+ "query",
+ "rfc3986",
+ "scheme",
+ "uri",
+ "url",
+ "userinfo"
+ ],
+ "support": {
+ "docs": "https://uri.thephpleague.com",
+ "forum": "https://thephpleague.slack.com",
+ "issues": "https://github.com/thephpleague/uri-src/issues",
+ "source": "https://github.com/thephpleague/uri-components/tree/7.8.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/nyamsprod",
+ "type": "github"
+ }
+ ],
+ "time": "2026-03-15T20:22:25+00:00"
+ },
+ {
+ "name": "league/uri-interfaces",
+ "version": "7.8.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/thephpleague/uri-interfaces.git",
+ "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/85d5c77c5d6d3af6c54db4a78246364908f3c928",
+ "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928",
+ "shasum": ""
+ },
+ "require": {
+ "ext-filter": "*",
+ "php": "^8.1",
+ "psr/http-message": "^1.1 || ^2.0"
+ },
+ "suggest": {
+ "ext-bcmath": "to improve IPV4 host parsing",
+ "ext-gmp": "to improve IPV4 host parsing",
+ "ext-intl": "to handle IDN host with the best performance",
+ "php-64bit": "to improve IPV4 host parsing",
+ "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification",
+ "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "7.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "League\\Uri\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Ignace Nyamagana Butera",
+ "email": "nyamsprod@gmail.com",
+ "homepage": "https://nyamsprod.com"
+ }
+ ],
+ "description": "Common tools for parsing and resolving RFC3987/RFC3986 URI",
+ "homepage": "https://uri.thephpleague.com",
+ "keywords": [
+ "data-uri",
+ "file-uri",
+ "ftp",
+ "hostname",
+ "http",
+ "https",
+ "parse_str",
+ "parse_url",
+ "psr-7",
+ "query-string",
+ "querystring",
+ "rfc3986",
+ "rfc3987",
+ "rfc6570",
+ "uri",
+ "url",
+ "ws"
+ ],
+ "support": {
+ "docs": "https://uri.thephpleague.com",
+ "forum": "https://thephpleague.slack.com",
+ "issues": "https://github.com/thephpleague/uri-src/issues",
+ "source": "https://github.com/thephpleague/uri-interfaces/tree/7.8.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sponsors/nyamsprod",
+ "type": "github"
+ }
+ ],
+ "time": "2026-03-08T20:05:35+00:00"
+ },
+ {
+ "name": "lexik/jwt-authentication-bundle",
+ "version": "v3.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/lexik/LexikJWTAuthenticationBundle.git",
+ "reference": "60df75dc70ee6f597929cb2f0812adda591dfa4b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/lexik/LexikJWTAuthenticationBundle/zipball/60df75dc70ee6f597929cb2f0812adda591dfa4b",
+ "reference": "60df75dc70ee6f597929cb2f0812adda591dfa4b",
+ "shasum": ""
+ },
+ "require": {
+ "ext-openssl": "*",
+ "lcobucci/jwt": "^5.0",
+ "php": ">=8.2",
+ "symfony/clock": "^6.4|^7.0|^8.0",
+ "symfony/config": "^6.4|^7.0|^8.0",
+ "symfony/dependency-injection": "^6.4|^7.0|^8.0",
+ "symfony/deprecation-contracts": "^2.4|^3.0",
+ "symfony/event-dispatcher": "^6.4|^7.0|^8.0",
+ "symfony/http-foundation": "^6.4|^7.0|^8.0",
+ "symfony/http-kernel": "^6.4|^7.0|^8.0",
+ "symfony/property-access": "^6.4|^7.0|^8.0",
+ "symfony/security-bundle": "^6.4|^7.0|^8.0",
+ "symfony/translation-contracts": "^1.0|^2.0|^3.0"
+ },
+ "require-dev": {
+ "api-platform/core": "^3.0|^4.0",
+ "rector/rector": "^1.2",
+ "symfony/browser-kit": "^6.4|^7.0|^8.0",
+ "symfony/console": "^6.4|^7.0|^8.0",
+ "symfony/dom-crawler": "^6.4|^7.0|^8.0",
+ "symfony/filesystem": "^6.4|^7.0|^8.0",
+ "symfony/framework-bundle": "^6.4|^7.0|^8.0",
+ "symfony/phpunit-bridge": "^6.4|^7.0|^8.0",
+ "symfony/var-dumper": "^6.4|^7.0|^8.0",
+ "symfony/yaml": "^6.4|^7.0|^8.0"
+ },
+ "suggest": {
+ "gesdinet/jwt-refresh-token-bundle": "Implements a refresh token system over Json Web Tokens in Symfony",
+ "spomky-labs/lexik-jose-bridge": "Provides a JWT Token encoder with encryption support"
+ },
+ "type": "symfony-bundle",
+ "autoload": {
+ "psr-4": {
+ "Lexik\\Bundle\\JWTAuthenticationBundle\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jeremy Barthe",
+ "email": "j.barthe@lexik.fr",
+ "homepage": "https://github.com/jeremyb"
+ },
+ {
+ "name": "Nicolas Cabot",
+ "email": "n.cabot@lexik.fr",
+ "homepage": "https://github.com/slashfan"
+ },
+ {
+ "name": "Cedric Girard",
+ "email": "c.girard@lexik.fr",
+ "homepage": "https://github.com/cedric-g"
+ },
+ {
+ "name": "Dev Lexik",
+ "email": "dev@lexik.fr",
+ "homepage": "https://github.com/lexik"
+ },
+ {
+ "name": "Robin Chalas",
+ "email": "robin.chalas@gmail.com",
+ "homepage": "https://github.com/chalasr"
+ },
+ {
+ "name": "Lexik Community",
+ "homepage": "https://github.com/lexik/LexikJWTAuthenticationBundle/graphs/contributors"
+ }
+ ],
+ "description": "This bundle provides JWT authentication for your Symfony REST API",
+ "homepage": "https://github.com/lexik/LexikJWTAuthenticationBundle",
+ "keywords": [
+ "Authentication",
+ "JWS",
+ "api",
+ "bundle",
+ "jwt",
+ "rest",
+ "symfony"
+ ],
+ "support": {
+ "issues": "https://github.com/lexik/LexikJWTAuthenticationBundle/issues",
+ "source": "https://github.com/lexik/LexikJWTAuthenticationBundle/tree/v3.2.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/chalasr",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/lexik/jwt-authentication-bundle",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-12-20T17:47:00+00:00"
+ },
+ {
+ "name": "liip/imagine-bundle",
+ "version": "2.18.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/liip/LiipImagineBundle.git",
+ "reference": "3c799cf7c56b87166f82c6c48c082cd95977cf6c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/liip/LiipImagineBundle/zipball/3c799cf7c56b87166f82c6c48c082cd95977cf6c",
+ "reference": "3c799cf7c56b87166f82c6c48c082cd95977cf6c",
+ "shasum": ""
+ },
+ "require": {
+ "ext-mbstring": "*",
+ "imagine/imagine": "^1.3.2",
+ "php": "^8.1",
+ "symfony/dependency-injection": "^5.4|^6.4|^7.4|^8.0",
+ "symfony/deprecation-contracts": "^2.5 || ^3",
+ "symfony/filesystem": "^5.4|^6.4|^7.3|^8.0",
+ "symfony/finder": "^5.4|^6.4|^7.3|^8.0",
+ "symfony/framework-bundle": "^5.4|^6.4|^7.3|^8.0",
+ "symfony/mime": "^5.4|^6.4|^7.3|^8.0",
+ "symfony/options-resolver": "^5.4|^6.4|^7.3|^8.0",
+ "symfony/process": "^5.4|^6.4|^7.3|^8.0",
+ "twig/twig": "^1.44|^2.9|^3.0"
+ },
+ "require-dev": {
+ "amazonwebservices/aws-sdk-for-php": "^1.0",
+ "aws/aws-sdk-php": "^2.4|^3.0",
+ "doctrine/cache": "^1.11|^2.0",
+ "doctrine/persistence": "^1.3|^2.0",
+ "enqueue/enqueue-bundle": "^0.9|^0.10",
+ "ext-gd": "*",
+ "league/flysystem": "^1.0|^2.0|^3.0",
+ "phpstan/phpstan": "^1.10.0",
+ "psr/cache": "^1.0|^2.0|^3.0",
+ "psr/log": "^1.0",
+ "symfony/asset": "^5.4|^6.4|^7.3|^8.0",
+ "symfony/browser-kit": "^5.4|^6.4|^7.3|^8.0",
+ "symfony/cache": "^5.4|^6.4|^7.3|^8.0",
+ "symfony/console": "^5.4|^6.4|^7.3|^8.0",
+ "symfony/form": "^5.4|^6.4|^7.3|^8.0",
+ "symfony/messenger": "^5.4|^6.4|^7.3|^8.0",
+ "symfony/phpunit-bridge": "^7.3",
+ "symfony/runtime": "^5.4|^6.4|^7.3|^8.0",
+ "symfony/templating": "^5.4|^6.4|^7.3|^8.0",
+ "symfony/validator": "^5.4|^6.4|^7.3|^8.0",
+ "symfony/yaml": "^5.4|^6.4|^7.3|^8.0"
+ },
+ "suggest": {
+ "alcaeus/mongo-php-adapter": "required for mongodb components",
+ "amazonwebservices/aws-sdk-for-php": "required to use AWS version 1 cache resolver",
+ "aws/aws-sdk-php": "required to use AWS version 2/3 cache resolver",
+ "doctrine/mongodb-odm": "required to use mongodb-backed doctrine components",
+ "enqueue/enqueue-bundle": "^0.9 add if you like to process images in background",
+ "ext-exif": "required to read EXIF metadata from images",
+ "ext-gd": "required to use gd driver",
+ "ext-gmagick": "required to use gmagick driver",
+ "ext-imagick": "required to use imagick driver",
+ "ext-json": "required to read JSON manifest versioning",
+ "ext-mongodb": "required for mongodb components",
+ "league/flysystem": "required to use FlySystem data loader or cache resolver",
+ "monolog/monolog": "A psr/log compatible logger is required to enable logging",
+ "rokka/imagine-vips": "required to use 'vips' driver",
+ "symfony/asset": "If you want to use asset versioning",
+ "symfony/messenger": "If you like to process images in background",
+ "symfony/templating": "required to use deprecated Templating component instead of Twig"
+ },
+ "type": "symfony-bundle",
+ "autoload": {
+ "psr-4": {
+ "Liip\\ImagineBundle\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Liip and other contributors",
+ "homepage": "https://github.com/liip/LiipImagineBundle/contributors"
+ }
+ ],
+ "description": "This bundle provides an image manipulation abstraction toolkit for Symfony-based projects.",
+ "homepage": "https://www.liip.ch",
+ "keywords": [
+ "bundle",
+ "image",
+ "imagine",
+ "liip",
+ "manipulation",
+ "photos",
+ "pictures",
+ "symfony",
+ "transformation"
+ ],
+ "support": {
+ "issues": "https://github.com/liip/LiipImagineBundle/issues",
+ "source": "https://github.com/liip/LiipImagineBundle/tree/2.18.1"
+ },
+ "time": "2026-10-07T20:50:10+00:00"
+ },
+ {
+ "name": "marcj/topsort",
+ "version": "1.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/marcj/topsort.php.git",
+ "reference": "387086c2db60ee0a27ac5df588c0f0b30c6bdc4b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/marcj/topsort.php/zipball/387086c2db60ee0a27ac5df588c0f0b30c6bdc4b",
+ "reference": "387086c2db60ee0a27ac5df588c0f0b30c6bdc4b",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.4"
+ },
+ "require-dev": {
+ "codeclimate/php-test-reporter": "dev-master",
+ "phpunit/phpunit": "~4.0",
+ "symfony/console": "~2.5"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "MJS\\TopSort\\": "src/",
+ "MJS\\TopSort\\Tests\\": "tests/Tests/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Marc J. Schmidt",
+ "email": "marc@marcjschmidt.de"
+ }
+ ],
+ "description": "High-Performance TopSort/Dependency resolving algorithm",
+ "keywords": [
+ "dependency resolving",
+ "topological sort",
+ "topsort"
+ ],
+ "support": {
+ "issues": "https://github.com/marcj/topsort.php/issues",
+ "source": "https://github.com/marcj/topsort.php/tree/1.1.0"
+ },
+ "time": "2016-11-19T14:58:11+00:00"
+ },
+ {
+ "name": "monolog/monolog",
+ "version": "3.12.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Seldaek/monolog.git",
+ "reference": "d7059e4c691d05ea1222b5124bed0f961be46e15"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Seldaek/monolog/zipball/d7059e4c691d05ea1222b5124bed0f961be46e15",
+ "reference": "d7059e4c691d05ea1222b5124bed0f961be46e15",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "psr/log": "^2.0 || ^3.0"
+ },
+ "provide": {
+ "psr/log-implementation": "3.0.0"
+ },
+ "require-dev": {
+ "aws/aws-sdk-php": "^3.0",
+ "doctrine/couchdb": "~1.0@dev",
+ "elasticsearch/elasticsearch": "^7 || ^8",
+ "ext-json": "*",
+ "graylog2/gelf-php": "^1.4.2 || ^2.0",
+ "guzzlehttp/guzzle": "^7.4.5",
+ "guzzlehttp/psr7": "^2.2",
+ "mongodb/mongodb": "^1.8 || ^2.0",
+ "php-amqplib/php-amqplib": "~2.4 || ^3",
+ "php-console/php-console": "^3.1.8",
+ "phpstan/phpstan": "^2",
+ "phpstan/phpstan-deprecation-rules": "^2",
+ "phpstan/phpstan-strict-rules": "^2",
+ "phpunit/phpunit": "^10.5.17 || ^11.0.7",
+ "predis/predis": "^1.1 || ^2",
+ "psr/clock": "^1.0",
+ "rollbar/rollbar": "^4.0",
+ "ruflin/elastica": "^7 || ^8",
+ "symfony/mailer": "^5.4 || ^6",
+ "symfony/mime": "^5.4 || ^6"
+ },
+ "suggest": {
+ "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB",
+ "doctrine/couchdb": "Allow sending log messages to a CouchDB server",
+ "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client",
+ "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)",
+ "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler",
+ "ext-mbstring": "Allow to work properly with unicode symbols",
+ "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)",
+ "ext-openssl": "Required to send log messages using SSL",
+ "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)",
+ "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server",
+ "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)",
+ "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib",
+ "psr/clock": "Required to pass a clock to the Logger and control the timestamp of log records",
+ "rollbar/rollbar": "Allow sending log messages to Rollbar",
+ "ruflin/elastica": "Allow sending log messages to an Elastic Search server"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Monolog\\": "src/Monolog"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jordi Boggiano",
+ "email": "j.boggiano@seld.be",
+ "homepage": "https://seld.be"
+ }
+ ],
+ "description": "Sends your logs to files, sockets, inboxes, databases and various web services",
+ "homepage": "https://github.com/Seldaek/monolog",
+ "keywords": [
+ "log",
+ "logging",
+ "psr-3"
+ ],
+ "support": {
+ "issues": "https://github.com/Seldaek/monolog/issues",
+ "source": "https://github.com/Seldaek/monolog/tree/3.12.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/Seldaek",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/monolog/monolog",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-09-29T19:21:12+00:00"
+ },
+ {
+ "name": "myclabs/php-enum",
+ "version": "1.8.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/myclabs/php-enum.git",
+ "reference": "e7be26966b7398204a234f8673fdad5ac6277802"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/myclabs/php-enum/zipball/e7be26966b7398204a234f8673fdad5ac6277802",
+ "reference": "e7be26966b7398204a234f8673fdad5ac6277802",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "php": "^7.3 || ^8.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.5",
+ "squizlabs/php_codesniffer": "1.*",
+ "vimeo/psalm": "^4.6.2 || ^5.2"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "MyCLabs\\Enum\\": "src/"
+ },
+ "classmap": [
+ "stubs/Stringable.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP Enum contributors",
+ "homepage": "https://github.com/myclabs/php-enum/graphs/contributors"
+ }
+ ],
+ "description": "PHP Enum implementation",
+ "homepage": "https://github.com/myclabs/php-enum",
+ "keywords": [
+ "enum"
+ ],
+ "support": {
+ "issues": "https://github.com/myclabs/php-enum/issues",
+ "source": "https://github.com/myclabs/php-enum/tree/1.8.5"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/mnapoli",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/myclabs/php-enum",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-01-14T11:49:03+00:00"
+ },
+ {
+ "name": "pagerfanta/pagerfanta",
+ "version": "v4.9.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/BabDev/Pagerfanta.git",
+ "reference": "aa8aeff8d2f0e115901aa08368453d049605027e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/BabDev/Pagerfanta/zipball/aa8aeff8d2f0e115901aa08368453d049605027e",
+ "reference": "aa8aeff8d2f0e115901aa08368453d049605027e",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "php": "^8.1",
+ "symfony/deprecation-contracts": "^2.1 || ^3.0"
+ },
+ "conflict": {
+ "doctrine/collections": "<1.8",
+ "doctrine/dbal": "<3.5",
+ "doctrine/mongodb-odm": "<2.4",
+ "doctrine/orm": "<2.14",
+ "doctrine/phpcr-odm": "<1.7",
+ "ruflin/elastica": "<7.3",
+ "solarium/solarium": "<6.2",
+ "twig/twig": "<2.13"
+ },
+ "replace": {
+ "pagerfanta/core": "self.version",
+ "pagerfanta/doctrine-collections-adapter": "self.version",
+ "pagerfanta/doctrine-dbal-adapter": "self.version",
+ "pagerfanta/doctrine-mongodb-odm-adapter": "self.version",
+ "pagerfanta/doctrine-orm-adapter": "self.version",
+ "pagerfanta/doctrine-phpcr-odm-adapter": "self.version",
+ "pagerfanta/elastica-adapter": "self.version",
+ "pagerfanta/solarium-adapter": "self.version",
+ "pagerfanta/twig": "self.version"
+ },
+ "require-dev": {
+ "dg/bypass-finals": "^1.9",
+ "doctrine/collections": "^1.8 || ^2.0 || ^3.0",
+ "doctrine/dbal": "^3.5 || ^4.0",
+ "doctrine/mongodb-odm": "^2.4",
+ "doctrine/orm": "^2.14 || ^3.0",
+ "doctrine/phpcr-odm": "^1.7 || ^2.0",
+ "jackalope/jackalope-doctrine-dbal": "^1.9 || ^2.0",
+ "phpstan/extension-installer": "^1.4",
+ "phpstan/phpstan": "2.2.13",
+ "phpstan/phpstan-phpunit": "2.0.18",
+ "phpunit/phpunit": "10.5.64",
+ "rector/rector": "2.6.6",
+ "ruflin/elastica": "^7.3 || ^8.0",
+ "solarium/solarium": "^6.2",
+ "symfony/cache": "^5.4 || ^6.4 || ^7.4 || ^8.1",
+ "twig/twig": "^2.13 || ^3.0"
+ },
+ "suggest": {
+ "twig/twig": "To integrate Pagerfanta with Twig"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Pagerfanta\\": "lib/Core/",
+ "Pagerfanta\\Twig\\": "lib/Twig/",
+ "Pagerfanta\\Elastica\\": "lib/Adapter/Elastica/",
+ "Pagerfanta\\Solarium\\": "lib/Adapter/Solarium/",
+ "Pagerfanta\\Doctrine\\ORM\\": "lib/Adapter/Doctrine/ORM/",
+ "Pagerfanta\\Doctrine\\DBAL\\": "lib/Adapter/Doctrine/DBAL/",
+ "Pagerfanta\\Doctrine\\PHPCRODM\\": "lib/Adapter/Doctrine/PHPCRODM/",
+ "Pagerfanta\\Doctrine\\MongoDBODM\\": "lib/Adapter/Doctrine/MongoDBODM/",
+ "Pagerfanta\\Doctrine\\Collections\\": "lib/Adapter/Doctrine/Collections/"
+ },
+ "exclude-from-classmap": [
+ "lib/**/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Pagination for PHP",
+ "keywords": [
+ "page",
+ "pagination",
+ "paginator",
+ "paging"
+ ],
+ "support": {
+ "issues": "https://github.com/BabDev/Pagerfanta/issues",
+ "source": "https://github.com/BabDev/Pagerfanta/tree/v4.9.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/mbabker",
+ "type": "github"
+ }
+ ],
+ "time": "2026-09-08T18:12:20+00:00"
+ },
+ {
+ "name": "paragonie/constant_time_encoding",
+ "version": "v3.1.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/paragonie/constant_time_encoding.git",
+ "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77",
+ "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8"
+ },
+ "require-dev": {
+ "infection/infection": "^0",
+ "nikic/php-fuzzer": "^0",
+ "phpunit/phpunit": "^9|^10|^11",
+ "vimeo/psalm": "^4|^5|^6"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "ParagonIE\\ConstantTime\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Paragon Initiative Enterprises",
+ "email": "security@paragonie.com",
+ "homepage": "https://paragonie.com",
+ "role": "Maintainer"
+ },
+ {
+ "name": "Steve 'Sc00bz' Thomas",
+ "email": "steve@tobtu.com",
+ "homepage": "https://www.tobtu.com",
+ "role": "Original Developer"
+ }
+ ],
+ "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)",
+ "keywords": [
+ "base16",
+ "base32",
+ "base32_decode",
+ "base32_encode",
+ "base64",
+ "base64_decode",
+ "base64_encode",
+ "bin2hex",
+ "encoding",
+ "hex",
+ "hex2bin",
+ "rfc4648"
+ ],
+ "support": {
+ "email": "info@paragonie.com",
+ "issues": "https://github.com/paragonie/constant_time_encoding/issues",
+ "source": "https://github.com/paragonie/constant_time_encoding"
+ },
+ "time": "2025-09-24T15:06:41+00:00"
+ },
+ {
+ "name": "paragonie/halite",
+ "version": "v5.1.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/paragonie/halite.git",
+ "reference": "12e7d1ab50ef3cacc27b59140e75d2cf59e85f71"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/paragonie/halite/zipball/12e7d1ab50ef3cacc27b59140e75d2cf59e85f71",
+ "reference": "12e7d1ab50ef3cacc27b59140e75d2cf59e85f71",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "paragonie/constant_time_encoding": "^2|^3",
+ "paragonie/hidden-string": "^1|^2",
+ "paragonie/sodium_compat": "^1|^2",
+ "php": "^8.1"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^2.1",
+ "phpunit/phpunit": "^9",
+ "vimeo/psalm": "^6.8"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "ParagonIE\\Halite\\": "./src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MPL-2.0"
+ ],
+ "authors": [
+ {
+ "name": "Paragon Initiative Enterprises",
+ "email": "info@paragonie.com",
+ "homepage": "https://paragonie.com"
+ }
+ ],
+ "description": "High-level cryptography interface powered by libsodium",
+ "homepage": "https://github.com/paragonie/halite",
+ "keywords": [
+ "Argon2i",
+ "BLAKE",
+ "BLAKE2",
+ "BLAKE2b",
+ "Curve25519",
+ "Ed25519",
+ "X25519",
+ "Xsalsa20",
+ "argon2",
+ "cryptography",
+ "encryption",
+ "ext-sodium",
+ "hashing",
+ "libsodium",
+ "password",
+ "public-key",
+ "signatures",
+ "sodium"
+ ],
+ "support": {
+ "docs": "https://github.com/paragonie/halite/tree/master/doc",
+ "issues": "https://github.com/paragonie/halite/issues",
+ "source": "https://github.com/paragonie/halite/tree/v5.1.4"
+ },
+ "time": "2025-09-19T03:39:32+00:00"
+ },
+ {
+ "name": "paragonie/hidden-string",
+ "version": "v2.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/paragonie/hidden-string.git",
+ "reference": "87886ab8ed7abb61c8bcf8d67cd3d3527feedbf7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/paragonie/hidden-string/zipball/87886ab8ed7abb61c8bcf8d67cd3d3527feedbf7",
+ "reference": "87886ab8ed7abb61c8bcf8d67cd3d3527feedbf7",
+ "shasum": ""
+ },
+ "require": {
+ "paragonie/constant_time_encoding": "^2|^3",
+ "php": "^7.4|^8"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.5",
+ "vimeo/psalm": "^4"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "ParagonIE\\HiddenString\\": "./src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MPL-2.0"
+ ],
+ "authors": [
+ {
+ "name": "Paragon Initiative Enterprises",
+ "email": "info@paragonie.com",
+ "homepage": "https://paragonie.com"
+ }
+ ],
+ "description": "Encapsulate strings in an object to hide them from stack traces",
+ "homepage": "https://github.com/paragonie/hidden-string",
+ "keywords": [
+ "hidden",
+ "stack trace",
+ "string"
+ ],
+ "support": {
+ "issues": "https://github.com/paragonie/hidden-string/issues",
+ "source": "https://github.com/paragonie/hidden-string/tree/v2.2.0"
+ },
+ "time": "2024-05-08T12:45:06+00:00"
+ },
+ {
+ "name": "paragonie/sodium_compat",
+ "version": "v2.5.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/paragonie/sodium_compat.git",
+ "reference": "3246b36803c177847e677862fedeb7f937869adb"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/3246b36803c177847e677862fedeb7f937869adb",
+ "reference": "3246b36803c177847e677862fedeb7f937869adb",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.1",
+ "php-64bit": "*"
+ },
+ "require-dev": {
+ "infection/infection": "^0",
+ "nikic/php-fuzzer": "^0",
+ "phpunit/phpunit": "^7|^8|^9|^10|^11",
+ "vimeo/psalm": "^4|^5|^6"
+ },
+ "suggest": {
+ "ext-sodium": "Better performance, password hashing (Argon2i), secure memory management (memzero), and better security."
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "autoload.php"
+ ],
+ "psr-4": {
+ "ParagonIE\\Sodium\\": "namespaced/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "ISC"
+ ],
+ "authors": [
+ {
+ "name": "Paragon Initiative Enterprises",
+ "email": "security@paragonie.com"
+ },
+ {
+ "name": "Frank Denis",
+ "email": "jedisct1@pureftpd.org"
+ }
+ ],
+ "description": "Pure PHP implementation of libsodium; uses the PHP extension if it exists",
+ "keywords": [
+ "Authentication",
+ "BLAKE2b",
+ "ChaCha20",
+ "ChaCha20-Poly1305",
+ "Chapoly",
+ "Curve25519",
+ "Ed25519",
+ "EdDSA",
+ "Edwards-curve Digital Signature Algorithm",
+ "Elliptic Curve Diffie-Hellman",
+ "Poly1305",
+ "Pure-PHP cryptography",
+ "RFC 7748",
+ "RFC 8032",
+ "Salpoly",
+ "Salsa20",
+ "X25519",
+ "XChaCha20-Poly1305",
+ "XSalsa20-Poly1305",
+ "Xchacha20",
+ "Xsalsa20",
+ "aead",
+ "cryptography",
+ "ecdh",
+ "elliptic curve",
+ "elliptic curve cryptography",
+ "encryption",
+ "libsodium",
+ "php",
+ "public-key cryptography",
+ "secret-key cryptography",
+ "side-channel resistant"
+ ],
+ "support": {
+ "issues": "https://github.com/paragonie/sodium_compat/issues",
+ "source": "https://github.com/paragonie/sodium_compat/tree/v2.5.2"
+ },
+ "time": "2026-08-18T23:46:48+00:00"
+ },
+ {
+ "name": "payplug/payplug-php",
+ "version": "4.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/payplug/payplug-php.git",
+ "reference": "ca089936ea5e442785465db6046ae88406a8db03"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/payplug/payplug-php/zipball/ca089936ea5e442785465db6046ae88406a8db03",
+ "reference": "ca089936ea5e442785465db6046ae88406a8db03",
+ "shasum": ""
+ },
+ "require": {
+ "ext-curl": "*",
+ "ext-openssl": "*",
+ "php": ">=5.3.0"
+ },
+ "require-dev": {
+ "phpdocumentor/phpdocumentor": "2.*",
+ "phpunit/phpunit": "5.7.*",
+ "rector/rector": "^1.2"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-0": {
+ "Payplug\\": "lib/"
+ },
+ "psr-4": {
+ "Payplug\\": "lib/Payplug"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PayPlug",
+ "email": "support@payplug.com"
+ },
+ {
+ "name": "Daniel FLOREZ MURILLO",
+ "email": "dmurillo@payplug.com"
+ }
+ ],
+ "description": "A simple PHP library for PayPlug public API.",
+ "homepage": "https://www.payplug.com",
+ "support": {
+ "issues": "https://github.com/payplug/payplug-php/issues",
+ "source": "https://github.com/payplug/payplug-php/tree/4.2.0"
+ },
+ "time": "2015-05-06T00:00:00+00:00"
+ },
+ {
+ "name": "payplug/unified-plugin-core",
+ "version": "1.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/payplug/unified-plugin-core.git",
+ "reference": "05423ee4bae1bef5e1374ba1aeadf127c9bb0dca"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/payplug/unified-plugin-core/zipball/05423ee4bae1bef5e1374ba1aeadf127c9bb0dca",
+ "reference": "05423ee4bae1bef5e1374ba1aeadf127c9bb0dca",
+ "shasum": ""
+ },
+ "require": {
+ "giggsey/libphonenumber-for-php": "8.13.45",
+ "giggsey/locale": "1.9",
+ "php": ">=7.4",
+ "symfony/polyfill-mbstring": "1.30.0 || ^1.31"
+ },
+ "require-dev": {
+ "captainhook/captainhook": "^5.0",
+ "friendsofphp/php-cs-fixer": "3.60.0",
+ "mockery/mockery": "^1.6",
+ "phpstan/phpstan": "^2.2",
+ "phpstan/phpstan-mockery": "^2.0",
+ "phpstan/phpstan-phpunit": "^2.0",
+ "phpunit/phpunit": "^9.5"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "PayplugUnifiedCore\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Payplug",
+ "email": "support@payplug.com"
+ }
+ ],
+ "description": "Core foundations shared library for Payplug e-commerce plugins.",
+ "support": {
+ "issues": "https://github.com/payplug/unified-plugin-core/issues",
+ "source": "https://github.com/payplug/unified-plugin-core/tree/1.2.1"
+ },
+ "time": "2026-10-08T07:20:41+00:00"
+ },
+ {
+ "name": "payum/core",
+ "version": "1.7.7",
+ "target-dir": "Payum/Core",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Payum/Core.git",
+ "reference": "1319e4396ce91ff92c0fe3f4d5dfcb48ffdeaaf3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Payum/Core/zipball/1319e4396ce91ff92c0fe3f4d5dfcb48ffdeaaf3",
+ "reference": "1319e4396ce91ff92c0fe3f4d5dfcb48ffdeaaf3",
+ "shasum": ""
+ },
+ "require": {
+ "alcohol/iso4217": "^3.1 || ^4.0",
+ "league/uri": "^6.4 || ^7.0",
+ "league/uri-components": "^2.2 || ^7.0",
+ "payum/iso4217": "^1.0",
+ "php": "^7.2 || ^8.0",
+ "php-http/client-implementation": "^1.0",
+ "php-http/message": "^1.0",
+ "psr/log": "^1 || ^2 || ^3",
+ "twig/twig": "^1.34|^2.4|^3.0"
+ },
+ "require-dev": {
+ "defuse/php-encryption": "^2",
+ "doctrine/dbal": "^2",
+ "doctrine/orm": "2.*",
+ "doctrine/persistence": "^1.3.3|^2.0",
+ "ext-curl": "*",
+ "ext-pdo_sqlite": "*",
+ "laminas/laminas-db": "^2.0",
+ "omnipay/common": "^3.0",
+ "omnipay/dummy": "^3.0",
+ "payum/omnipay-v3-bridge": "^1.0",
+ "php-http/guzzle6-adapter": "^1.0",
+ "phpunit/phpunit": "^5.7",
+ "propel/propel1": "~1.7",
+ "symfony/cache": "^4.4|^5.0",
+ "symfony/dependency-injection": "^4.4|^5.0",
+ "symfony/form": "^4.4|^5.0",
+ "symfony/http-foundation": "^4.4|^5.0",
+ "symfony/http-kernel": "^4.4|^5.0",
+ "symfony/phpunit-bridge": "^4.4|^5.0",
+ "symfony/routing": "^4.4|^5.0",
+ "symfony/validator": "^4.4|^5.0"
+ },
+ "suggest": {
+ "defuse/php-encryption": "^2 If you want to encrypt gateways credentials in database",
+ "doctrine/mongodb-odm": "~2.0 If you want to store models to mongo doctrin2 ODM",
+ "doctrine/orm": "~2.3 If you want to store models to database using doctrin2 ORM",
+ "laminas/laminas-db": "~2.0 If you want to store models to Laminas Db ORM",
+ "monolog/monolog": "~1.0 If you want to use PSR-3 logger",
+ "payum/authorize-net-aim": "self.version If you want to use Authorize.Net AIM payment gateway",
+ "payum/be2bill": "self.version If you want to use be2bill payment gateway",
+ "payum/omnipay-v3-bridge": "^1 If you want to use omnipay's gateways",
+ "payum/payex": "self.version If you want to use payex payment gateway",
+ "payum/paypal-express-checkout-nvp": "self.version If you want to use paypal express checkout, digital goods or recurring payments",
+ "payum/paypal-ipn": "self.version If you want to use paypal instant payment notifications(Paypal IPN)",
+ "payum/paypal-pro-checkout-nvp": "self.version If you want to use paypal pro checkout",
+ "payum/paypal-rest": "self.version If you want to use paypal rest gateway",
+ "propel/propel": "If you want to store models to Propel2 ORM",
+ "propel/propel1": "~1.7 If you want to store models to Propel1 ORM",
+ "symfony/dependency-injection": "~2.8|~3.0 If you want to use container aware stuff",
+ "symfony/form": "~2.8|~3.0 If you want to use forms",
+ "symfony/http-foundation": "~2.8|~3.0 If you want to use HttpRequestVerifier or HttpResponse reply from symfony's bridge",
+ "symfony/http-kernel": "~2.8|~3.0 If you want to use HttpRequestVerifier from symfony's bridge",
+ "symfony/routing": "~2.8|~3.0 If you want to use TokenFactory from symfony's bridge"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.6-dev"
+ }
+ },
+ "autoload": {
+ "psr-0": {
+ "Payum\\Core\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Kotlyar Maksim",
+ "email": "kotlyar.maksim@gmail.com"
+ },
+ {
+ "name": "Payum project",
+ "homepage": "https://payum.forma-pro.com/"
+ },
+ {
+ "name": "Community contributions",
+ "homepage": "https://github.com/Payum/Payum/contributors"
+ }
+ ],
+ "description": "One million downloads of Payum already! Payum offers everything you need to work with payments. Friendly for all PHP frameworks (Symfony, Laravel, Laminas, Yii, Silex). Check more visiting site.",
+ "homepage": "https://payum.forma-pro.com/",
+ "keywords": [
+ "authorize",
+ "capture",
+ "notify",
+ "payment",
+ "payout",
+ "recurring payment",
+ "refund",
+ "subscription",
+ "withdrawal"
+ ],
+ "support": {
+ "source": "https://github.com/Payum/Core/tree/1.7.7"
+ },
+ "time": "2025-10-27T17:23:03+00:00"
+ },
+ {
+ "name": "payum/iso4217",
+ "version": "1.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Payum/iso4217.git",
+ "reference": "faafdd2c5e799c673d7aa576aaf26fa2fb631014"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Payum/iso4217/zipball/faafdd2c5e799c673d7aa576aaf26fa2fb631014",
+ "reference": "faafdd2c5e799c673d7aa576aaf26fa2fb631014",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~4.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Payum\\ISO4217\\": "."
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Rob Bast",
+ "email": "rob.bast@gmail.com"
+ },
+ {
+ "name": "Kotlyar Maksim",
+ "email": "kotlyar.maksim@gmail.com"
+ },
+ {
+ "name": "Payum project",
+ "homepage": "http://payum.org/"
+ },
+ {
+ "name": "Community contributions",
+ "homepage": "https://github.com/Payum/Payum/contributors"
+ }
+ ],
+ "description": "ISO 4217 PHP Library",
+ "homepage": "https://payum.forma-pro.com/",
+ "keywords": [
+ "4217",
+ "ISO 4217",
+ "currencies",
+ "iso",
+ "library"
+ ],
+ "support": {
+ "issues": "https://github.com/payum/iso4217/issues",
+ "source": "https://github.com/payum/iso4217"
+ },
+ "time": "2022-02-27T10:14:23+00:00"
+ },
+ {
+ "name": "payum/offline",
+ "version": "1.7.7",
+ "target-dir": "Payum/Offline",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Payum/Offline.git",
+ "reference": "9b10c121473962258a18ac85363de2047f8abbb0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Payum/Offline/zipball/9b10c121473962258a18ac85363de2047f8abbb0",
+ "reference": "9b10c121473962258a18ac85363de2047f8abbb0",
+ "shasum": ""
+ },
+ "require": {
+ "payum/core": "^1.5"
+ },
+ "require-dev": {
+ "payum/core": "^1.5",
+ "phpunit/phpunit": "^5.7",
+ "symfony/phpunit-bridge": "^3.1|^4"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.6-dev"
+ }
+ },
+ "autoload": {
+ "psr-0": {
+ "Payum\\Offline": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Kotlyar Maksim",
+ "email": "kotlyar.maksim@gmail.com"
+ },
+ {
+ "name": "Payum project",
+ "homepage": "https://payum.forma-pro.com/"
+ },
+ {
+ "name": "Community contributions",
+ "homepage": "https://github.com/Payum/Offline/contributors"
+ }
+ ],
+ "description": "The Payum extension. It provides Offline payment integration.",
+ "homepage": "https://payum.forma-pro.com",
+ "keywords": [
+ "invoice",
+ "offlile",
+ "payment"
+ ],
+ "support": {
+ "source": "https://github.com/Payum/Offline/tree/1.7.7"
+ },
+ "time": "2025-10-27T17:23:03+00:00"
+ },
+ {
+ "name": "payum/payum-bundle",
+ "version": "2.7.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Payum/PayumBundle.git",
+ "reference": "854ed36ab02c00c350b70ae3f7656d6a980f8a66"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Payum/PayumBundle/zipball/854ed36ab02c00c350b70ae3f7656d6a980f8a66",
+ "reference": "854ed36ab02c00c350b70ae3f7656d6a980f8a66",
+ "shasum": ""
+ },
+ "require": {
+ "payum/core": "^1.7.2",
+ "php": "^8.0",
+ "symfony/dependency-injection": "^5.4 || ^6.4 || ^7.0 || ^8.0",
+ "symfony/form": "^5.4 || ^6.0 || ^7.0 || ^8.0",
+ "symfony/framework-bundle": "^5.4 || ^6.0 || ^7.0 || ^8.0",
+ "symfony/security-csrf": "^5.4 || ^6.0 || ^7.0 || ^8.0",
+ "symfony/validator": "^5.4 || ^6.0 || ^7.0 || ^8.0"
+ },
+ "require-dev": {
+ "defuse/php-encryption": "^2",
+ "doctrine/orm": "^2.8 || ^3.0",
+ "nyholm/psr7": "^1.5",
+ "omnipay/common": "^3@dev",
+ "omnipay/dummy": "^3@alpha",
+ "omnipay/paypal": "^3@dev",
+ "payum/offline": "^1.7",
+ "payum/omnipay-v3-bridge": "dev-master",
+ "payum/paypal-express-checkout-nvp": "^1.7",
+ "payum/stripe": "^1.7",
+ "php-http/guzzle7-adapter": "^1.0",
+ "php-http/message-factory": "^1.1",
+ "phpunit/phpunit": "^9.5 || ^10.0",
+ "psr/log": "^1 || ^2 || ^3",
+ "stripe/stripe-php": "~7.0",
+ "symfony/browser-kit": "^5.4 || ^6.0 || ^7.0 || ^8.0",
+ "symfony/expression-language": "^5.4 || ^6.0 || ^7.0 || ^8.0",
+ "symfony/http-client": "^5.4 || ^6.0 || ^7.0 || ^8.0",
+ "symfony/phpunit-bridge": "^5.4 || ^6.0 || ^7.0 || ^8.0",
+ "symfony/templating": "^5.4 || ^6.0 || ^7.0 || ^8.0",
+ "symfony/twig-bundle": "^5.4 || ^6.0 || ^7.0 || ^8.0",
+ "symfony/web-profiler-bundle": "^5.4 || ^6.0 || ^7.0 || ^8.0",
+ "symfony/yaml": "^5.4 || ^6.0 || ^7.0 || ^8.0",
+ "twig/twig": "^2.0 || ^3.0"
+ },
+ "suggest": {
+ "sonata-project/admin-bundle": "^3 If you want to configure payments in the backend."
+ },
+ "type": "symfony-bundle",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.5-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Payum\\Bundle\\PayumBundle\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Kotlyar Maksim",
+ "email": "kotlyar.maksim@gmail.com"
+ },
+ {
+ "name": "Payum project",
+ "homepage": "https://payum.forma-pro.com/"
+ },
+ {
+ "name": "Community contributions",
+ "homepage": "https://github.com/Payum/PayumBundle/contributors"
+ }
+ ],
+ "description": "One million downloads of Payum already! Payum offers everything you need to work with payments. Check more visiting site.",
+ "homepage": "https://payum.forma-pro.com/",
+ "keywords": [
+ "authorize.net",
+ "be2bill",
+ "instant notifications",
+ "klarna",
+ "offline",
+ "omnipay",
+ "payex",
+ "payment",
+ "paypal",
+ "paypal express checkout",
+ "paypal pro checkout",
+ "recurring payment",
+ "stripe",
+ "stripe checkout",
+ "stripe.js",
+ "symfony"
+ ],
+ "support": {
+ "issues": "https://github.com/Payum/PayumBundle/issues",
+ "source": "https://github.com/Payum/PayumBundle/tree/2.7.2"
+ },
+ "time": "2026-07-07T12:12:48+00:00"
+ },
+ {
+ "name": "php-http/discovery",
+ "version": "1.20.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-http/discovery.git",
+ "reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-http/discovery/zipball/82fe4c73ef3363caed49ff8dd1539ba06044910d",
+ "reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d",
+ "shasum": ""
+ },
+ "require": {
+ "composer-plugin-api": "^1.0|^2.0",
+ "php": "^7.1 || ^8.0"
+ },
+ "conflict": {
+ "nyholm/psr7": "<1.0",
+ "zendframework/zend-diactoros": "*"
+ },
+ "provide": {
+ "php-http/async-client-implementation": "*",
+ "php-http/client-implementation": "*",
+ "psr/http-client-implementation": "*",
+ "psr/http-factory-implementation": "*",
+ "psr/http-message-implementation": "*"
+ },
+ "require-dev": {
+ "composer/composer": "^1.0.2|^2.0",
+ "graham-campbell/phpspec-skip-example-extension": "^5.0",
+ "php-http/httplug": "^1.0 || ^2.0",
+ "php-http/message-factory": "^1.0",
+ "phpspec/phpspec": "^5.1 || ^6.1 || ^7.3",
+ "sebastian/comparator": "^3.0.5 || ^4.0.8",
+ "symfony/phpunit-bridge": "^6.4.4 || ^7.0.1"
+ },
+ "type": "composer-plugin",
+ "extra": {
+ "class": "Http\\Discovery\\Composer\\Plugin",
+ "plugin-optional": true
+ },
+ "autoload": {
+ "psr-4": {
+ "Http\\Discovery\\": "src/"
+ },
+ "exclude-from-classmap": [
+ "src/Composer/Plugin.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Márk Sági-Kazár",
+ "email": "mark.sagikazar@gmail.com"
+ }
+ ],
+ "description": "Finds and installs PSR-7, PSR-17, PSR-18 and HTTPlug implementations",
+ "homepage": "http://php-http.org",
+ "keywords": [
+ "adapter",
+ "client",
+ "discovery",
+ "factory",
+ "http",
+ "message",
+ "psr17",
+ "psr7"
+ ],
+ "support": {
+ "issues": "https://github.com/php-http/discovery/issues",
+ "source": "https://github.com/php-http/discovery/tree/1.20.0"
+ },
+ "time": "2024-10-02T11:20:13+00:00"
+ },
+ {
+ "name": "php-http/guzzle7-adapter",
+ "version": "1.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-http/guzzle7-adapter.git",
+ "reference": "03a415fde709c2f25539790fecf4d9a31bc3d0eb"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-http/guzzle7-adapter/zipball/03a415fde709c2f25539790fecf4d9a31bc3d0eb",
+ "reference": "03a415fde709c2f25539790fecf4d9a31bc3d0eb",
+ "shasum": ""
+ },
+ "require": {
+ "guzzlehttp/guzzle": "^7.0",
+ "php": "^7.3 | ^8.0",
+ "php-http/httplug": "^2.0",
+ "psr/http-client": "^1.0"
+ },
+ "provide": {
+ "php-http/async-client-implementation": "1.0",
+ "php-http/client-implementation": "1.0",
+ "psr/http-client-implementation": "1.0"
+ },
+ "require-dev": {
+ "php-http/client-integration-tests": "^3.0",
+ "php-http/message-factory": "^1.1",
+ "phpspec/prophecy-phpunit": "^2.0",
+ "phpunit/phpunit": "^8.0|^9.3"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Http\\Adapter\\Guzzle7\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Tobias Nyholm",
+ "email": "tobias.nyholm@gmail.com"
+ }
+ ],
+ "description": "Guzzle 7 HTTP Adapter",
+ "homepage": "http://httplug.io",
+ "keywords": [
+ "Guzzle",
+ "http"
+ ],
+ "support": {
+ "issues": "https://github.com/php-http/guzzle7-adapter/issues",
+ "source": "https://github.com/php-http/guzzle7-adapter/tree/1.1.0"
+ },
+ "time": "2024-11-26T11:14:36+00:00"
+ },
+ {
+ "name": "php-http/httplug",
+ "version": "2.4.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-http/httplug.git",
+ "reference": "5cad731844891a4c282f3f3e1b582c46839d22f4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-http/httplug/zipball/5cad731844891a4c282f3f3e1b582c46839d22f4",
+ "reference": "5cad731844891a4c282f3f3e1b582c46839d22f4",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1 || ^8.0",
+ "php-http/promise": "^1.1",
+ "psr/http-client": "^1.0",
+ "psr/http-message": "^1.0 || ^2.0"
+ },
+ "require-dev": {
+ "friends-of-phpspec/phpspec-code-coverage": "^4.1 || ^5.0 || ^6.0",
+ "phpspec/phpspec": "^5.1 || ^6.0 || ^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Http\\Client\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Eric GELOEN",
+ "email": "geloen.eric@gmail.com"
+ },
+ {
+ "name": "Márk Sági-Kazár",
+ "email": "mark.sagikazar@gmail.com",
+ "homepage": "https://sagikazarmark.hu"
+ }
+ ],
+ "description": "HTTPlug, the HTTP client abstraction for PHP",
+ "homepage": "http://httplug.io",
+ "keywords": [
+ "client",
+ "http"
+ ],
+ "support": {
+ "issues": "https://github.com/php-http/httplug/issues",
+ "source": "https://github.com/php-http/httplug/tree/2.4.1"
+ },
+ "time": "2024-09-23T11:39:58+00:00"
+ },
+ {
+ "name": "php-http/message",
+ "version": "1.16.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-http/message.git",
+ "reference": "06dd5e8562f84e641bf929bfe699ee0f5ce8080a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-http/message/zipball/06dd5e8562f84e641bf929bfe699ee0f5ce8080a",
+ "reference": "06dd5e8562f84e641bf929bfe699ee0f5ce8080a",
+ "shasum": ""
+ },
+ "require": {
+ "clue/stream-filter": "^1.5",
+ "php": "^7.2 || ^8.0",
+ "psr/http-message": "^1.1 || ^2.0"
+ },
+ "provide": {
+ "php-http/message-factory-implementation": "1.0"
+ },
+ "require-dev": {
+ "ergebnis/composer-normalize": "^2.6",
+ "ext-zlib": "*",
+ "guzzlehttp/psr7": "^1.0 || ^2.0",
+ "laminas/laminas-diactoros": "^2.0 || ^3.0",
+ "php-http/message-factory": "^1.0.2",
+ "phpspec/phpspec": "^5.1 || ^6.3 || ^7.1",
+ "slim/slim": "^3.0"
+ },
+ "suggest": {
+ "ext-zlib": "Used with compressor/decompressor streams",
+ "guzzlehttp/psr7": "Used with Guzzle PSR-7 Factories",
+ "laminas/laminas-diactoros": "Used with Diactoros Factories",
+ "slim/slim": "Used with Slim Framework PSR-7 implementation"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/filters.php"
+ ],
+ "psr-4": {
+ "Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Márk Sági-Kazár",
+ "email": "mark.sagikazar@gmail.com"
+ }
+ ],
+ "description": "HTTP Message related tools",
+ "homepage": "http://php-http.org",
+ "keywords": [
+ "http",
+ "message",
+ "psr-7"
+ ],
+ "support": {
+ "issues": "https://github.com/php-http/message/issues",
+ "source": "https://github.com/php-http/message/tree/1.16.2"
+ },
+ "time": "2024-10-02T11:34:13+00:00"
+ },
+ {
+ "name": "php-http/message-factory",
+ "version": "1.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-http/message-factory.git",
+ "reference": "4d8778e1c7d405cbb471574821c1ff5b68cc8f57"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-http/message-factory/zipball/4d8778e1c7d405cbb471574821c1ff5b68cc8f57",
+ "reference": "4d8778e1c7d405cbb471574821c1ff5b68cc8f57",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.4",
+ "psr/http-message": "^1.0 || ^2.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Márk Sági-Kazár",
+ "email": "mark.sagikazar@gmail.com"
+ }
+ ],
+ "description": "Factory interfaces for PSR-7 HTTP Message",
+ "homepage": "http://php-http.org",
+ "keywords": [
+ "factory",
+ "http",
+ "message",
+ "stream",
+ "uri"
+ ],
+ "support": {
+ "issues": "https://github.com/php-http/message-factory/issues",
+ "source": "https://github.com/php-http/message-factory/tree/1.1.0"
+ },
+ "abandoned": "psr/http-factory",
+ "time": "2023-04-14T14:16:17+00:00"
+ },
+ {
+ "name": "php-http/promise",
+ "version": "1.3.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-http/promise.git",
+ "reference": "fc85b1fba37c169a69a07ef0d5a8075770cc1f83"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-http/promise/zipball/fc85b1fba37c169a69a07ef0d5a8075770cc1f83",
+ "reference": "fc85b1fba37c169a69a07ef0d5a8075770cc1f83",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1 || ^8.0"
+ },
+ "require-dev": {
+ "friends-of-phpspec/phpspec-code-coverage": "^4.3.2 || ^6.3",
+ "phpspec/phpspec": "^5.1.2 || ^6.2 || ^7.4"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Http\\Promise\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Joel Wurtz",
+ "email": "joel.wurtz@gmail.com"
+ },
+ {
+ "name": "Márk Sági-Kazár",
+ "email": "mark.sagikazar@gmail.com"
+ }
+ ],
+ "description": "Promise used for asynchronous HTTP requests",
+ "homepage": "http://httplug.io",
+ "keywords": [
+ "promise"
+ ],
+ "support": {
+ "issues": "https://github.com/php-http/promise/issues",
+ "source": "https://github.com/php-http/promise/tree/1.3.1"
+ },
+ "time": "2024-03-15T13:55:21+00:00"
+ },
+ {
+ "name": "psr/cache",
+ "version": "3.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/cache.git",
+ "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf",
+ "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.0.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Cache\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for caching libraries",
+ "keywords": [
+ "cache",
+ "psr",
+ "psr-6"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/cache/tree/3.0.0"
+ },
+ "time": "2021-02-03T23:26:27+00:00"
+ },
+ {
+ "name": "psr/clock",
+ "version": "1.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/clock.git",
+ "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d",
+ "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.0 || ^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Psr\\Clock\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for reading the clock.",
+ "homepage": "https://github.com/php-fig/clock",
+ "keywords": [
+ "clock",
+ "now",
+ "psr",
+ "psr-20",
+ "time"
+ ],
+ "support": {
+ "issues": "https://github.com/php-fig/clock/issues",
+ "source": "https://github.com/php-fig/clock/tree/1.0.0"
+ },
+ "time": "2022-11-25T14:36:26+00:00"
+ },
+ {
+ "name": "psr/container",
+ "version": "2.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/container.git",
+ "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963",
+ "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.4.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Container\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common Container Interface (PHP FIG PSR-11)",
+ "homepage": "https://github.com/php-fig/container",
+ "keywords": [
+ "PSR-11",
+ "container",
+ "container-interface",
+ "container-interop",
+ "psr"
+ ],
+ "support": {
+ "issues": "https://github.com/php-fig/container/issues",
+ "source": "https://github.com/php-fig/container/tree/2.0.2"
+ },
+ "time": "2021-11-05T16:47:00+00:00"
+ },
+ {
+ "name": "psr/event-dispatcher",
+ "version": "1.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/event-dispatcher.git",
+ "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0",
+ "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\EventDispatcher\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "http://www.php-fig.org/"
+ }
+ ],
+ "description": "Standard interfaces for event handling.",
+ "keywords": [
+ "events",
+ "psr",
+ "psr-14"
+ ],
+ "support": {
+ "issues": "https://github.com/php-fig/event-dispatcher/issues",
+ "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0"
+ },
+ "time": "2019-01-08T18:20:26+00:00"
+ },
+ {
+ "name": "psr/http-client",
+ "version": "1.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-client.git",
+ "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90",
+ "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.0 || ^8.0",
+ "psr/http-message": "^1.0 || ^2.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Client\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP clients",
+ "homepage": "https://github.com/php-fig/http-client",
+ "keywords": [
+ "http",
+ "http-client",
+ "psr",
+ "psr-18"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-client"
+ },
+ "time": "2023-09-23T14:17:50+00:00"
+ },
+ {
+ "name": "psr/http-factory",
+ "version": "1.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-factory.git",
+ "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
+ "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1",
+ "psr/http-message": "^1.0 || ^2.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories",
+ "keywords": [
+ "factory",
+ "http",
+ "message",
+ "psr",
+ "psr-17",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-factory"
+ },
+ "time": "2024-04-15T12:06:14+00:00"
+ },
+ {
+ "name": "psr/http-message",
+ "version": "2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-message.git",
+ "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71",
+ "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP messages",
+ "homepage": "https://github.com/php-fig/http-message",
+ "keywords": [
+ "http",
+ "http-message",
+ "psr",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-message/tree/2.0"
+ },
+ "time": "2023-04-04T09:54:51+00:00"
+ },
+ {
+ "name": "psr/link",
+ "version": "2.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/link.git",
+ "reference": "84b159194ecfd7eaa472280213976e96415433f7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/link/zipball/84b159194ecfd7eaa472280213976e96415433f7",
+ "reference": "84b159194ecfd7eaa472280213976e96415433f7",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.0.0"
+ },
+ "suggest": {
+ "fig/link-util": "Provides some useful PSR-13 utilities"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Link\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "http://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interfaces for HTTP links",
+ "homepage": "https://github.com/php-fig/link",
+ "keywords": [
+ "http",
+ "http-link",
+ "link",
+ "psr",
+ "psr-13",
+ "rest"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/link/tree/2.0.1"
+ },
+ "time": "2021-03-11T23:00:27+00:00"
+ },
+ {
+ "name": "psr/log",
+ "version": "3.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/log.git",
+ "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
+ "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.0.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Log\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for logging libraries",
+ "homepage": "https://github.com/php-fig/log",
+ "keywords": [
+ "log",
+ "psr",
+ "psr-3"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/log/tree/3.0.2"
+ },
+ "time": "2024-09-11T13:17:53+00:00"
+ },
+ {
+ "name": "ralouphie/getallheaders",
+ "version": "3.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/ralouphie/getallheaders.git",
+ "reference": "120b605dfeb996808c31b6477290a714d356e822"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822",
+ "reference": "120b605dfeb996808c31b6477290a714d356e822",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.6"
+ },
+ "require-dev": {
+ "php-coveralls/php-coveralls": "^2.1",
+ "phpunit/phpunit": "^5 || ^6.5"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/getallheaders.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Ralph Khattar",
+ "email": "ralph.khattar@gmail.com"
+ }
+ ],
+ "description": "A polyfill for getallheaders.",
+ "support": {
+ "issues": "https://github.com/ralouphie/getallheaders/issues",
+ "source": "https://github.com/ralouphie/getallheaders/tree/develop"
+ },
+ "time": "2019-03-08T08:55:37+00:00"
+ },
+ {
+ "name": "ramsey/collection",
+ "version": "2.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/ramsey/collection.git",
+ "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/ramsey/collection/zipball/344572933ad0181accbf4ba763e85a0306a8c5e2",
+ "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.1"
+ },
+ "require-dev": {
+ "captainhook/plugin-composer": "^5.3",
+ "ergebnis/composer-normalize": "^2.45",
+ "fakerphp/faker": "^1.24",
+ "hamcrest/hamcrest-php": "^2.0",
+ "jangregor/phpstan-prophecy": "^2.1",
+ "mockery/mockery": "^1.6",
+ "php-parallel-lint/php-console-highlighter": "^1.0",
+ "php-parallel-lint/php-parallel-lint": "^1.4",
+ "phpspec/prophecy-phpunit": "^2.3",
+ "phpstan/extension-installer": "^1.4",
+ "phpstan/phpstan": "^2.1",
+ "phpstan/phpstan-mockery": "^2.0",
+ "phpstan/phpstan-phpunit": "^2.0",
+ "phpunit/phpunit": "^10.5",
+ "ramsey/coding-standard": "^2.3",
+ "ramsey/conventional-commits": "^1.6",
+ "roave/security-advisories": "dev-latest"
+ },
+ "type": "library",
+ "extra": {
+ "captainhook": {
+ "force-install": true
+ },
+ "ramsey/conventional-commits": {
+ "configFile": "conventional-commits.json"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Ramsey\\Collection\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Ben Ramsey",
+ "email": "ben@benramsey.com",
+ "homepage": "https://benramsey.com"
+ }
+ ],
+ "description": "A PHP library for representing and manipulating collections.",
+ "keywords": [
+ "array",
+ "collection",
+ "hash",
+ "map",
+ "queue",
+ "set"
+ ],
+ "support": {
+ "issues": "https://github.com/ramsey/collection/issues",
+ "source": "https://github.com/ramsey/collection/tree/2.1.1"
+ },
+ "time": "2025-03-22T05:38:12+00:00"
+ },
+ {
+ "name": "ramsey/uuid",
+ "version": "4.9.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/ramsey/uuid.git",
+ "reference": "75d73f48d02797c2c285a7e9f348fadc0102ffe2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/ramsey/uuid/zipball/75d73f48d02797c2c285a7e9f348fadc0102ffe2",
+ "reference": "75d73f48d02797c2c285a7e9f348fadc0102ffe2",
+ "shasum": ""
+ },
+ "require": {
+ "brick/math": "^0.8.16 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14 || ^0.15 || ^0.16 || ^0.17 || ^0.18 || ^0.19 || ^0.20 || ^1.0",
+ "php": "^8.0",
+ "ramsey/collection": "^1.2 || ^2.0"
+ },
+ "replace": {
+ "rhumsaa/uuid": "self.version"
+ },
+ "require-dev": {
+ "captainhook/captainhook": "^5.25",
+ "captainhook/plugin-composer": "^5.3",
+ "dealerdirect/phpcodesniffer-composer-installer": "^1.0",
+ "ergebnis/composer-normalize": "^2.47",
+ "mockery/mockery": "^1.6",
+ "paragonie/random-lib": "^2",
+ "php-mock/php-mock": "^2.6",
+ "php-mock/php-mock-mockery": "^1.5",
+ "php-parallel-lint/php-parallel-lint": "^1.4.0",
+ "phpbench/phpbench": "^1.2.14",
+ "phpstan/extension-installer": "^1.4",
+ "phpstan/phpstan": "^2.1",
+ "phpstan/phpstan-mockery": "^2.0",
+ "phpstan/phpstan-phpunit": "^2.0",
+ "phpunit/phpunit": "^9.6",
+ "slevomat/coding-standard": "^8.18",
+ "squizlabs/php_codesniffer": "^3.13"
+ },
+ "suggest": {
+ "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.",
+ "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.",
+ "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.",
+ "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter",
+ "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type."
+ },
+ "type": "library",
+ "extra": {
+ "captainhook": {
+ "force-install": true
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/functions.php"
+ ],
+ "psr-4": {
+ "Ramsey\\Uuid\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).",
+ "keywords": [
+ "guid",
+ "identifier",
+ "uuid"
+ ],
+ "support": {
+ "issues": "https://github.com/ramsey/uuid/issues",
+ "source": "https://github.com/ramsey/uuid/tree/4.9.4"
+ },
+ "time": "2026-09-16T11:39:30+00:00"
+ },
+ {
+ "name": "stof/doctrine-extensions-bundle",
+ "version": "v1.15.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/stof/StofDoctrineExtensionsBundle.git",
+ "reference": "0f464d3e298ba97bcad219727ca1ee09ec8b30ea"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/stof/StofDoctrineExtensionsBundle/zipball/0f464d3e298ba97bcad219727ca1ee09ec8b30ea",
+ "reference": "0f464d3e298ba97bcad219727ca1ee09ec8b30ea",
+ "shasum": ""
+ },
+ "require": {
+ "gedmo/doctrine-extensions": "^3.21.0",
+ "php": "^8.1",
+ "symfony/cache": "^6.4 || ^7.0 || ^8.0",
+ "symfony/config": "^6.4 || ^7.0 || ^8.0",
+ "symfony/dependency-injection": "^6.4 || ^7.0 || ^8.0",
+ "symfony/event-dispatcher": "^6.4 || ^7.0 || ^8.0",
+ "symfony/http-kernel": "^6.4 || ^7.0 || ^8.0",
+ "symfony/translation-contracts": "^2.5 || ^3.5"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^2.1",
+ "phpstan/phpstan-deprecation-rules": "^2.0",
+ "phpstan/phpstan-phpunit": "^2.0",
+ "phpstan/phpstan-strict-rules": "^2.0",
+ "phpstan/phpstan-symfony": "^2.0",
+ "phpunit/phpunit": "^9.6.31",
+ "symfony/mime": "^6.4 || ^7.0 || ^8.0",
+ "symfony/phpunit-bridge": "^v6.4.1 || ^7.0.1 || ^8.0",
+ "symfony/security-core": "^6.4 || ^7.0 || ^8.0"
+ },
+ "suggest": {
+ "doctrine/doctrine-bundle": "to use the ORM extensions",
+ "doctrine/mongodb-odm-bundle": "to use the MongoDB ODM extensions",
+ "symfony/mime": "To use the Mime component integration for Uploadable"
+ },
+ "type": "symfony-bundle",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Stof\\DoctrineExtensionsBundle\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Christophe Coevoet",
+ "email": "stof@notk.org"
+ }
+ ],
+ "description": "Integration of the gedmo/doctrine-extensions with Symfony",
+ "homepage": "https://github.com/stof/StofDoctrineExtensionsBundle",
+ "keywords": [
+ "behaviors",
+ "doctrine2",
+ "extensions",
+ "gedmo",
+ "loggable",
+ "nestedset",
+ "sluggable",
+ "sortable",
+ "timestampable",
+ "translatable",
+ "tree"
+ ],
+ "support": {
+ "issues": "https://github.com/stof/StofDoctrineExtensionsBundle/issues",
+ "source": "https://github.com/stof/StofDoctrineExtensionsBundle/tree/v1.15.3"
+ },
+ "time": "2026-01-23T08:45:07+00:00"
+ },
+ {
+ "name": "sylius-labs/association-hydrator",
+ "version": "v1.4.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/SyliusLabs/AssociationHydrator.git",
+ "reference": "8dde1cde9cee92a40b883a565316a82a7bbbd7c2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/SyliusLabs/AssociationHydrator/zipball/8dde1cde9cee92a40b883a565316a82a7bbbd7c2",
+ "reference": "8dde1cde9cee92a40b883a565316a82a7bbbd7c2",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/orm": "^2.20 || ^3.3",
+ "php": "^8.1",
+ "symfony/property-access": "^5.4 || ^6.4 || ^7.4 || ^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "SyliusLabs\\AssociationHydrator\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Kamil Kokot",
+ "email": "kamil@kokot.me",
+ "homepage": "http://kamil.kokot.me"
+ }
+ ],
+ "description": "Doctrine ORM hydration performance optimization made easier.",
+ "support": {
+ "issues": "https://github.com/SyliusLabs/AssociationHydrator/issues",
+ "source": "https://github.com/SyliusLabs/AssociationHydrator/tree/v1.4.0"
+ },
+ "time": "2026-02-06T14:09:00+00:00"
+ },
+ {
+ "name": "sylius-labs/doctrine-migrations-extra-bundle",
+ "version": "v0.2.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/SyliusLabs/DoctrineMigrationsExtraBundle.git",
+ "reference": "30a87e08937220999f91ed160c99458210a1d26c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/SyliusLabs/DoctrineMigrationsExtraBundle/zipball/30a87e08937220999f91ed160c99458210a1d26c",
+ "reference": "30a87e08937220999f91ed160c99458210a1d26c",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/doctrine-migrations-bundle": "^3.0",
+ "doctrine/migrations": "^3.0",
+ "marcj/topsort": "^1.1",
+ "php": "^8.1",
+ "symfony/framework-bundle": "^5.4 || ^6.4 || ^7.0"
+ },
+ "require-dev": {
+ "infection/infection": "^0.28",
+ "matthiasnoback/symfony-config-test": "^5.1",
+ "matthiasnoback/symfony-dependency-injection-test": "^5.1",
+ "phpstan/phpstan": "^1.10",
+ "phpunit/phpunit": "^9.5"
+ },
+ "type": "bundle",
+ "autoload": {
+ "psr-4": {
+ "SyliusLabs\\DoctrineMigrationsExtraBundle\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Kamil Kokot",
+ "email": "kamil@kokot.me",
+ "homepage": "https://kamilkokot.com"
+ }
+ ],
+ "support": {
+ "issues": "https://github.com/SyliusLabs/DoctrineMigrationsExtraBundle/issues",
+ "source": "https://github.com/SyliusLabs/DoctrineMigrationsExtraBundle/tree/v0.2.2"
+ },
+ "time": "2024-07-29T15:10:54+00:00"
+ },
+ {
+ "name": "sylius/fixtures-bundle",
+ "version": "v1.10.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Sylius/SyliusFixturesBundle.git",
+ "reference": "f25f5356e62b5468cd95f5f3844b14c2b5ab00bf"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Sylius/SyliusFixturesBundle/zipball/f25f5356e62b5468cd95f5f3844b14c2b5ab00bf",
+ "reference": "f25f5356e62b5468cd95f5f3844b14c2b5ab00bf",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/data-fixtures": "^1.2 || ^2.0",
+ "monolog/monolog": "^1.25 || ^2.1 || ^3",
+ "php": "^8.2",
+ "symfony/framework-bundle": "^6.4 || ^7.4 || ^8.0",
+ "symfony/monolog-bridge": "^6.4 || ^7.4 || ^8.0",
+ "webmozart/assert": "^1.9"
+ },
+ "require-dev": {
+ "doctrine/dbal": "^3.0 || ^4.0",
+ "doctrine/doctrine-bundle": "^2.1 || ^3.0",
+ "doctrine/orm": "^2.7 || ^3.0",
+ "matthiasnoback/symfony-config-test": "^4.2 || ^5.0 || ^6.0",
+ "matthiasnoback/symfony-dependency-injection-test": "^4.2 || ^5.0 || ^6.0",
+ "phpspec/phpspec": "^7.0 || ^8.0",
+ "phpstan/phpstan": "^2.0",
+ "phpstan/phpstan-doctrine": "^2.0",
+ "phpstan/phpstan-phpunit": "^2.0",
+ "phpstan/phpstan-symfony": "^2.0",
+ "phpstan/phpstan-webmozart-assert": "^2.0",
+ "phpunit/phpunit": "^9.4 || ^10.0 || ^11.0",
+ "rector/rector": "^2.0",
+ "sylius-labs/coding-standard": "^4.0"
+ },
+ "type": "symfony-bundle",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.8-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Sylius\\Bundle\\FixturesBundle\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Kamil Kokot",
+ "homepage": "https://kamilkokot.com"
+ },
+ {
+ "name": "Sylius project",
+ "homepage": "https://sylius.com"
+ },
+ {
+ "name": "Community contributions",
+ "homepage": "https://github.com/Sylius/Sylius/contributors"
+ }
+ ],
+ "description": "Configurable fixtures for Symfony applications.",
+ "homepage": "https://sylius.com",
+ "keywords": [
+ "fixtures",
+ "sylius",
+ "symfony"
+ ],
+ "support": {
+ "issues": "https://github.com/Sylius/SyliusFixturesBundle/issues",
+ "source": "https://github.com/Sylius/SyliusFixturesBundle/tree/v1.10.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sylius",
+ "type": "github"
+ }
+ ],
+ "time": "2026-02-09T11:04:40+00:00"
+ },
+ {
+ "name": "sylius/grid-bundle",
+ "version": "v1.16.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Sylius/SyliusGridBundle.git",
+ "reference": "22ca062fe16a2618a4e264f6c86162236d4ae50c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Sylius/SyliusGridBundle/zipball/22ca062fe16a2618a4e264f6c86162236d4ae50c",
+ "reference": "22ca062fe16a2618a4e264f6c86162236d4ae50c",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.2",
+ "sylius/registry": "^1.5",
+ "symfony/config": "^6.4 || ^7.2 || ^8.0",
+ "symfony/dependency-injection": "^6.4 || ^7.2 || ^8.0",
+ "symfony/deprecation-contracts": "^2.2 || ^3.1",
+ "symfony/event-dispatcher": "^6.4 || ^7.2 || ^8.0",
+ "symfony/form": "^6.4 || ^7.2 || ^8.0",
+ "symfony/framework-bundle": "^6.4 || ^7.2 || ^8.0",
+ "symfony/http-kernel": "^6.4 || ^7.2 || ^8.0",
+ "symfony/options-resolver": "^6.4 || ^7.2 || ^8.0",
+ "symfony/property-access": "^6.4 || ^7.2 || ^8.0",
+ "symfony/validator": "^6.4 || ^7.2 || ^8.0",
+ "webmozart/assert": "^1.9 || ^2.3"
+ },
+ "conflict": {
+ "badev/pagerfanta-bundle": "<4.4",
+ "doctrine/dbal": "<2.11",
+ "doctrine/doctrine-bundle": "<2.0",
+ "doctrine/orm": "<2.8",
+ "doctrine/persistence": "<2.0",
+ "doctrine/phpcr-odm": "<1.5",
+ "jackalope/jackalope-doctrine-dbal": "<2.0",
+ "pagerfanta/core": "<3.7",
+ "pagerfanta/doctrine-dbal-adapter": "<3.7",
+ "pagerfanta/doctrine-orm-adapter": "<3.7",
+ "pagerfanta/doctrine-phpcr-odm-adapter": "<3.7",
+ "pagerfanta/pagerfanta": "<3.7",
+ "twig/twig": "<2.12"
+ },
+ "replace": {
+ "sylius/grid": "self.version"
+ },
+ "require-dev": {
+ "babdev/pagerfanta-bundle": "^4.4",
+ "doctrine/dbal": "^2.11 || ^3.0 || ^4.0",
+ "doctrine/doctrine-bundle": "^2.0 || ^3.0 || ^4.0",
+ "doctrine/orm": "^2.8 || ^3.0",
+ "doctrine/persistence": "^1.3 || ^2.0 || ^3.1 || ^4.0",
+ "doctrine/phpcr-odm": "^1.5 || ^2.0",
+ "jackalope/jackalope-doctrine-dbal": "^2.0",
+ "matthiasnoback/symfony-config-test": "^6.1",
+ "matthiasnoback/symfony-dependency-injection-test": "^6.1",
+ "pagerfanta/pagerfanta": "^3.7 || ^4.0",
+ "phparkitect/phparkitect": "^1.2",
+ "phpstan/extension-installer": "^1.4",
+ "phpstan/phpstan": "^2.0",
+ "phpstan/phpstan-phpunit": "^2.0",
+ "phpstan/phpstan-symfony": "^2.0",
+ "phpstan/phpstan-webmozart-assert": "^2.0",
+ "phpunit/phpunit": "^11.0",
+ "rector/rector": "^2.0",
+ "sylius-labs/coding-standard": "^4.0",
+ "symfony/browser-kit": "^6.4 || ^7.2 || ^8.0",
+ "symfony/console": "^6.4 || ^7.2 || ^8.0",
+ "symfony/css-selector": "^6.4 || ^7.2 || ^8.0",
+ "symfony/dotenv": "^6.4 || ^7.2 || ^8.0",
+ "symfony/maker-bundle": "^1.36",
+ "symfony/polyfill-mbstring": "<1.22.0 || >1.22.0",
+ "symfony/security-csrf": "^6.4 || ^7.2 || ^8.0",
+ "symfony/translation": "^6.4 || ^7.2 || ^8.0",
+ "symfony/twig-bundle": "^6.4 || ^7.2 || ^8.0",
+ "symplify/easy-coding-standard": "^13.0",
+ "twig/twig": "^2.12 || ^3.0",
+ "zenstruck/foundry": "^2.8"
+ },
+ "suggest": {
+ "sylius/currency-bundle": "^1.7"
+ },
+ "type": "symfony-bundle",
+ "extra": {
+ "symfony": {
+ "require": "7.4.*"
+ },
+ "branch-alias": {
+ "dev-master": "1.11-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Sylius\\Component\\Grid\\": "src/Component/",
+ "Sylius\\Bundle\\GridBundle\\": "src/Bundle/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Paweł Jędrzejewski",
+ "homepage": "https://pjedrzejewski.com"
+ },
+ {
+ "name": "Sylius project",
+ "homepage": "https://sylius.com"
+ },
+ {
+ "name": "Community contributions",
+ "homepage": "https://github.com/Sylius/Sylius/contributors"
+ }
+ ],
+ "description": "Amazing grids with support of filters and custom fields integrated into Symfony.",
+ "homepage": "https://sylius.com",
+ "keywords": [
+ "admin",
+ "api",
+ "crud",
+ "doctrine",
+ "grid",
+ "resource",
+ "sylius"
+ ],
+ "support": {
+ "issues": "https://github.com/Sylius/SyliusGridBundle/issues",
+ "source": "https://github.com/Sylius/SyliusGridBundle/tree/v1.16.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sylius",
+ "type": "github"
+ }
+ ],
+ "time": "2026-07-13T09:44:06+00:00"
+ },
+ {
+ "name": "sylius/mailer-bundle",
+ "version": "v2.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Sylius/SyliusMailerBundle.git",
+ "reference": "fd25981bd6653590ab380bde88a2a69b88803d61"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Sylius/SyliusMailerBundle/zipball/fd25981bd6653590ab380bde88a2a69b88803d61",
+ "reference": "fd25981bd6653590ab380bde88a2a69b88803d61",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.2",
+ "symfony/config": "^6.4 || ^7.4 || ^8.0",
+ "symfony/dependency-injection": "^6.4 || ^7.4 || ^8.0",
+ "symfony/event-dispatcher-contracts": "^3.0",
+ "symfony/framework-bundle": "^6.4 || ^7.4 || ^8.0",
+ "symfony/http-kernel": "^6.4 || ^7.4 || ^8.0",
+ "twig/twig": "^3.3",
+ "webmozart/assert": "^1.9"
+ },
+ "replace": {
+ "sylius/mailer": "self.version"
+ },
+ "require-dev": {
+ "matthiasnoback/symfony-dependency-injection-test": "^6.0",
+ "phpstan/phpstan": "1.12.5",
+ "phpstan/phpstan-phpunit": "1.4.0",
+ "phpstan/phpstan-webmozart-assert": "1.2.11",
+ "phpunit/phpunit": "^11.5",
+ "sylius-labs/coding-standard": "^4.0",
+ "symfony/console": "^6.4 || ^7.4 || ^8.0",
+ "symfony/dotenv": "^6.4 || ^7.4 || ^8.0",
+ "symfony/event-dispatcher": "^6.4 || ^7.4 || ^8.0",
+ "symfony/mailer": "^6.4 || ^7.4 || ^8.0",
+ "symfony/twig-bundle": "^6.4 || ^7.4 || ^8.0"
+ },
+ "suggest": {
+ "symfony/translation": "To use the translation features for testing purposes"
+ },
+ "type": "symfony-bundle",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.8-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Sylius\\Component\\Mailer\\": "src/Component/",
+ "Sylius\\Bundle\\MailerBundle\\": "src/Bundle/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Paweł Jędrzejewski",
+ "homepage": "https://pjedrzejewski.com"
+ },
+ {
+ "name": "Sylius project",
+ "homepage": "https://sylius.com"
+ },
+ {
+ "name": "Community contributions",
+ "homepage": "https://github.com/Sylius/Sylius/contributors"
+ }
+ ],
+ "description": "Mailers and e-mail template management for Symfony projects.",
+ "homepage": "https://sylius.com",
+ "keywords": [
+ "email",
+ "mailer",
+ "symfony"
+ ],
+ "support": {
+ "issues": "https://github.com/Sylius/SyliusMailerBundle/issues",
+ "source": "https://github.com/Sylius/SyliusMailerBundle/tree/v2.2.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sylius",
+ "type": "github"
+ }
+ ],
+ "time": "2026-09-18T13:34:23+00:00"
+ },
+ {
+ "name": "sylius/pdf-generation-bundle",
+ "version": "v1.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Sylius/PdfGenerationBundle.git",
+ "reference": "465f0d9c54faebcb1828d6ee2495356053ed7177"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Sylius/PdfGenerationBundle/zipball/465f0d9c54faebcb1828d6ee2495356053ed7177",
+ "reference": "465f0d9c54faebcb1828d6ee2495356053ed7177",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2",
+ "symfony/config": "^6.4 || ^7.4 || ^8.0",
+ "symfony/dependency-injection": "^6.4 || ^7.4 || ^8.0",
+ "symfony/filesystem": "^6.4 || ^7.4 || ^8.0",
+ "symfony/http-kernel": "^6.4 || ^7.4 || ^8.0",
+ "symfony/twig-bundle": "^6.4 || ^7.4 || ^8.0",
+ "twig/twig": "^3.0"
+ },
+ "require-dev": {
+ "dompdf/dompdf": "^3.1",
+ "ext-curl": "*",
+ "gotenberg/gotenberg-php": "^2.17",
+ "knplabs/knp-gaufrette-bundle": "^0.9.0",
+ "knplabs/knp-snappy-bundle": "^1.10",
+ "league/flysystem-bundle": "^3.0",
+ "matthiasnoback/symfony-config-test": "^5.1 || ^6.0",
+ "matthiasnoback/symfony-dependency-injection-test": "^5.1 || ^6.0",
+ "phpstan/extension-installer": "^1.4",
+ "phpstan/phpstan": "^1.12",
+ "phpstan/phpstan-symfony": "^1.4",
+ "phpunit/phpunit": "^10.5",
+ "sylius-labs/coding-standard": "^4.4",
+ "symfony/console": "^6.4 || ^7.4 || ^8.0",
+ "symfony/framework-bundle": "^6.4 || ^7.4 || ^8.0",
+ "symfony/http-client": "^7.4"
+ },
+ "suggest": {
+ "dompdf/dompdf": "Required for the Dompdf adapter",
+ "knplabs/knp-gaufrette-bundle": "Required for the Gaufrette storage type",
+ "knplabs/knp-snappy-bundle": "Required for the KnpSnappy adapter (wkhtmltopdf)",
+ "league/flysystem-bundle": "Required for the Flysystem storage type"
+ },
+ "type": "symfony-bundle",
+ "autoload": {
+ "psr-4": {
+ "Sylius\\PdfGenerationBundle\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "PDF generation bundle with swappable renderer abstraction.",
+ "keywords": [
+ "pdf",
+ "symfony"
+ ],
+ "support": {
+ "issues": "https://github.com/Sylius/PdfGenerationBundle/issues",
+ "source": "https://github.com/Sylius/PdfGenerationBundle/tree/v1.0.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sylius",
+ "type": "github"
+ }
+ ],
+ "time": "2026-03-18T11:33:47+00:00"
+ },
+ {
+ "name": "sylius/refund-plugin",
+ "version": "v2.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Sylius/RefundPlugin.git",
+ "reference": "0ca6789ac408f43e05452ee8595c1fe219c3c3d2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Sylius/RefundPlugin/zipball/0ca6789ac408f43e05452ee8595c1fe219c3c3d2",
+ "reference": "0ca6789ac408f43e05452ee8595c1fe219c3c3d2",
+ "shasum": ""
+ },
+ "require": {
+ "knplabs/gaufrette": "^0.11 || ^1.0",
+ "knplabs/knp-gaufrette-bundle": "^0.9 || ^1.0",
+ "knplabs/knp-snappy-bundle": "^1.10",
+ "myclabs/php-enum": "^1.8",
+ "php": "^8.2",
+ "php-http/discovery": "^1.20",
+ "sylius/pdf-generation-bundle": "^1.0",
+ "sylius/resource-bundle": "^1.12",
+ "sylius/sylius": "^2.0",
+ "sylius/telemetry": "^1.0",
+ "symfony/messenger": "^6.4 || ^7.4 || ^8.0"
+ },
+ "require-dev": {
+ "behat/behat": "^3.22 || 4.0.0-alpha1",
+ "behat/mink": "^1.10 || ^1.13",
+ "dmore/chrome-mink-driver": "^2.7",
+ "friends-of-behat/mink-browserkit-driver": "^1.6.3",
+ "friends-of-behat/mink-debug-extension": "^2.2",
+ "friends-of-behat/mink-extension": "^2.7 || 3.0.0-alpha1",
+ "friends-of-behat/page-object-extension": "^0.3",
+ "friends-of-behat/symfony-extension": "^2.6.2",
+ "friends-of-behat/variadic-extension": "^1.6",
+ "matthiasnoback/symfony-config-test": "^6.1",
+ "matthiasnoback/symfony-dependency-injection-test": "^6.2",
+ "phpstan/phpstan": "^2.2",
+ "phpstan/phpstan-webmozart-assert": "^2.0",
+ "phpunit/phpunit": "^10.5",
+ "sylius-labs/behat-chrome-extension": "^1.5",
+ "sylius-labs/coding-standard": "^4.2",
+ "sylius/test-application": "^2.0.0@alpha",
+ "symfony/browser-kit": "^6.4 || ^7.4 || ^8.0",
+ "symfony/debug-bundle": "^6.4 || ^7.4 || ^8.0",
+ "symfony/dotenv": "^6.4 || ^7.4 || ^8.0",
+ "symfony/intl": "^6.4 || ^7.4 || ^8.0",
+ "symfony/web-profiler-bundle": "^6.4 || ^7.4 || ^8.0",
+ "symfony/webpack-encore-bundle": "^2.2"
+ },
+ "type": "sylius-plugin",
+ "extra": {
+ "symfony": {
+ "require": "~7.4.0",
+ "allow-contrib": false
+ },
+ "public-dir": "vendor/sylius/test-application/public",
+ "branch-alias": {
+ "dev-2.0": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Sylius\\RefundPlugin\\": "src/",
+ "Tests\\Sylius\\RefundPlugin\\": "tests/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Mateusz Zalewski",
+ "homepage": "http://mpzalewski.com.pl"
+ },
+ {
+ "name": "Bartosz Pietrzak",
+ "homepage": "https://github.com/bartoszpietrzak1994"
+ },
+ {
+ "name": "Sylius Team",
+ "email": "team@sylius.com"
+ }
+ ],
+ "description": "Plugin provides basic refunds functionality for Sylius application.",
+ "keywords": [
+ "e-commerce",
+ "refunds",
+ "sylius",
+ "sylius-plugin",
+ "symfony"
+ ],
+ "support": {
+ "issues": "https://github.com/Sylius/RefundPlugin/issues",
+ "source": "https://github.com/Sylius/RefundPlugin/tree/v2.2.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sylius",
+ "type": "github"
+ }
+ ],
+ "time": "2026-10-06T09:28:15+00:00"
+ },
+ {
+ "name": "sylius/registry",
+ "version": "v1.6.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Sylius/Registry.git",
+ "reference": "e4f44f418f48d43e1b969e878fd962e7034aa3ad"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Sylius/Registry/zipball/e4f44f418f48d43e1b969e878fd962e7034aa3ad",
+ "reference": "e4f44f418f48d43e1b969e878fd962e7034aa3ad",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4 || ^8.0"
+ },
+ "require-dev": {
+ "phpspec/phpspec": "^7.0",
+ "sylius-labs/coding-standard": "^3.2.2",
+ "vimeo/psalm": "4.4.1"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.6-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Sylius\\Component\\Registry\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Paweł Jędrzejewski",
+ "homepage": "http://pjedrzejewski.com"
+ },
+ {
+ "name": "Sylius project",
+ "homepage": "http://sylius.com"
+ },
+ {
+ "name": "Community contributions",
+ "homepage": "http://github.com/Sylius/Sylius/contributors"
+ }
+ ],
+ "description": "Services registry.",
+ "homepage": "http://sylius.com",
+ "keywords": [
+ "registry",
+ "services"
+ ],
+ "support": {
+ "issues": "https://github.com/Sylius/Registry/issues",
+ "source": "https://github.com/Sylius/Registry/tree/v1.6.0"
+ },
+ "time": "2021-02-01T15:36:19+00:00"
+ },
+ {
+ "name": "sylius/resource-bundle",
+ "version": "v1.14.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Sylius/SyliusResourceBundle.git",
+ "reference": "9ec90cdc58a053cccdd81ebf9584bdf5b66563f1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Sylius/SyliusResourceBundle/zipball/9ec90cdc58a053cccdd81ebf9584bdf5b66563f1",
+ "reference": "9ec90cdc58a053cccdd81ebf9584bdf5b66563f1",
+ "shasum": ""
+ },
+ "require": {
+ "babdev/pagerfanta-bundle": "^4.4",
+ "doctrine/collections": "^2.2",
+ "doctrine/event-manager": "^1.1 || ^2.0",
+ "doctrine/inflector": "^2.0",
+ "doctrine/persistence": "^3.3 || ^4.0",
+ "php": "^8.2",
+ "sylius/registry": "^1.2",
+ "symfony/config": "^6.4 || ^7.4 || ^8.0",
+ "symfony/deprecation-contracts": "^3.5",
+ "symfony/expression-language": "^6.4 || ^7.4 || ^8.0",
+ "symfony/form": "^6.4 || ^7.4 || ^8.0",
+ "symfony/framework-bundle": "^6.4 || ^7.4 || ^8.0",
+ "symfony/http-foundation": "^6.4 || ^7.4 || ^8.0",
+ "symfony/intl": "^6.4 || ^7.4 || ^8.0",
+ "symfony/routing": "^6.4 || ^7.4 || ^8.0",
+ "symfony/security-core": "^6.4 || ^7.4 || ^8.0",
+ "symfony/security-csrf": "^6.4 || ^7.4 || ^8.0",
+ "symfony/string": "^6.4 || ^7.4 || ^8.0",
+ "symfony/translation": "^6.4 || ^7.4 || ^8.0",
+ "symfony/twig-bundle": "^6.4 || ^7.4 || ^8.0",
+ "symfony/validator": "^6.4 || ^7.4 || ^8.0",
+ "symfony/yaml": "^6.4 || ^7.4 || ^8.0",
+ "webmozart/assert": "^1.11",
+ "willdurand/negotiation": "^3.1"
+ },
+ "conflict": {
+ "behat/transliterator": "<1.2",
+ "doctrine/doctrine-bundle": "<2.0",
+ "doctrine/orm": "<2.18",
+ "doctrine/phpcr-odm": "<2.1",
+ "friendsofsymfony/rest-bundle": "<3.7",
+ "gedmo/doctrine-extensions": "<3.17.1",
+ "jms/serializer-bundle": "<5.5",
+ "pagerfanta/pagerfanta": "<4.4",
+ "symfony/workflow": "<6.4 || >=7.0,<7.4",
+ "twig/twig": "<3.0",
+ "willdurand/hateoas-bundle": "<2.5 || ^3.0",
+ "winzou/state-machine-bundle": "<0.6.2"
+ },
+ "replace": {
+ "sylius/resource": "self.version"
+ },
+ "require-dev": {
+ "coduo/php-matcher": "^6.0",
+ "doctrine/data-fixtures": "^2.0",
+ "doctrine/doctrine-bundle": "^2.13 || ^3.0 || ^4.0",
+ "doctrine/orm": "^2.18 || ^3.3",
+ "jackalope/jackalope": "^2.0",
+ "jackalope/jackalope-doctrine-dbal": "^2.0",
+ "matthiasnoback/symfony-dependency-injection-test": "^6.1.0",
+ "openlss/lib-array2xml": "^1.0",
+ "pagerfanta/pagerfanta": "^4.4",
+ "phpcr/phpcr": "^2.1",
+ "phpstan/phpstan": "^1.12",
+ "phpstan/phpstan-phpunit": "^1.4",
+ "phpstan/phpstan-webmozart-assert": "^1.2",
+ "phpunit/phpunit": "^10.0",
+ "rector/rector": "^0.18.2",
+ "sylius-labs/coding-standard": "^4.4",
+ "sylius/grid-bundle": "^1.13 || ^1.15@alpha",
+ "symfony/browser-kit": "^6.4 || ^7.4 || ^8.0",
+ "symfony/console": "^6.4 || ^7.4 || ^8.0",
+ "symfony/css-selector": "^6.4 || ^7.4 || ^8.0",
+ "symfony/dependency-injection": "^6.4 || ^7.4 || ^8.0",
+ "symfony/dotenv": "^6.4 || ^7.4 || ^8.0",
+ "symfony/http-kernel": "^6.4 || ^7.4 || ^8.0",
+ "symfony/messenger": "^6.4 || ^7.4 || ^8.0",
+ "symfony/security-bundle": "^6.4 || ^7.4 || ^8.0",
+ "symfony/serializer": "^6.4 || ^7.4 || ^8.0",
+ "symfony/stopwatch": "^6.4 || ^7.4 || ^8.0",
+ "symfony/uid": "^6.4 || ^7.4 || ^8.0",
+ "twig/twig": "^3.14",
+ "zenstruck/foundry": "^2.3"
+ },
+ "suggest": {
+ "doctrine/orm": "^2.20",
+ "sylius/locale": "^1.0"
+ },
+ "type": "symfony-bundle",
+ "extra": {
+ "symfony": {
+ "require": "8.0.*"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Sylius\\Resource\\": "src/Component/src/",
+ "Sylius\\Component\\Resource\\": "src/Component/legacy/src/",
+ "Sylius\\Bundle\\ResourceBundle\\": "src/Bundle/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Paweł Jędrzejewski",
+ "homepage": "https://pjedrzejewski.com"
+ },
+ {
+ "name": "Sylius project",
+ "homepage": "https://sylius.com"
+ },
+ {
+ "name": "Community contributions",
+ "homepage": "https://github.com/Sylius/Sylius/contributors"
+ }
+ ],
+ "description": "Resource component for Sylius.",
+ "homepage": "https://sylius.com",
+ "keywords": [
+ "persistence",
+ "resource",
+ "storage",
+ "sylius"
+ ],
+ "support": {
+ "issues": "https://github.com/Sylius/SyliusResourceBundle/issues",
+ "source": "https://github.com/Sylius/SyliusResourceBundle/tree/v1.14.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sylius",
+ "type": "github"
+ }
+ ],
+ "time": "2026-06-22T05:19:23+00:00"
+ },
+ {
+ "name": "sylius/sylius",
+ "version": "v2.2.10",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Sylius/Sylius.git",
+ "reference": "1a5d6da030245dbb23be1feff055f2c125bd9b12"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Sylius/Sylius/zipball/1a5d6da030245dbb23be1feff055f2c125bd9b12",
+ "reference": "1a5d6da030245dbb23be1feff055f2c125bd9b12",
+ "shasum": ""
+ },
+ "require": {
+ "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",
+ "doctrine/dbal": "^3.9",
+ "doctrine/doctrine-bundle": "^2.13",
+ "doctrine/doctrine-migrations-bundle": "^3.3",
+ "doctrine/event-manager": "^2.0",
+ "doctrine/inflector": "^2.0",
+ "doctrine/migrations": "^3.8",
+ "doctrine/orm": "^2.18 || ^3.5",
+ "doctrine/persistence": "^3.3",
+ "egulias/email-validator": "^4.0",
+ "ext-dom": "*",
+ "ext-exif": "*",
+ "ext-fileinfo": "*",
+ "ext-filter": "*",
+ "ext-gd": "*",
+ "ext-hash": "*",
+ "ext-intl": "*",
+ "ext-json": "*",
+ "ext-simplexml": "*",
+ "ext-sodium": "*",
+ "fakerphp/faker": "^1.23",
+ "friendsofphp/proxy-manager-lts": "^1.0",
+ "gedmo/doctrine-extensions": "^3.20",
+ "guzzlehttp/guzzle": "^7.9",
+ "guzzlehttp/psr7": "^2.5",
+ "knplabs/gaufrette": "^0.11",
+ "knplabs/knp-gaufrette-bundle": "^0.9",
+ "knplabs/knp-menu": "^3.5",
+ "knplabs/knp-menu-bundle": "^3.4",
+ "laminas/laminas-stdlib": "^3.19",
+ "league/flysystem-bundle": "^3.3",
+ "lexik/jwt-authentication-bundle": "^3.1",
+ "liip/imagine-bundle": "^2.15",
+ "pagerfanta/pagerfanta": "^4.0",
+ "paragonie/halite": "^5.0",
+ "payum/offline": "^1.7.5",
+ "payum/payum-bundle": "^2.6",
+ "php": "^8.2",
+ "php-http/discovery": "^1.20",
+ "php-http/guzzle7-adapter": "^1.0",
+ "php-http/httplug": "^2.4",
+ "php-http/message-factory": "^1.1",
+ "psr/cache": "^3.0",
+ "psr/http-client": "^1.0",
+ "psr/http-message": "^1.0 || ^2.0",
+ "psr/log": "^3.0",
+ "ramsey/uuid": "^4.7",
+ "stof/doctrine-extensions-bundle": "^1.12",
+ "sylius-labs/association-hydrator": "^1.2",
+ "sylius-labs/doctrine-migrations-extra-bundle": "^0.2",
+ "sylius/fixtures-bundle": "^1.9",
+ "sylius/grid": "^1.13",
+ "sylius/grid-bundle": "^1.13",
+ "sylius/mailer": "^2.1",
+ "sylius/mailer-bundle": "^2.1",
+ "sylius/registry": "^1.6",
+ "sylius/resource": "^1.12",
+ "sylius/resource-bundle": "^1.12",
+ "sylius/theme-bundle": "^2.4",
+ "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.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.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.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.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.25",
+ "symfony/string": "^6.4 || ^7.4",
+ "symfony/translation": "^6.4 || ^7.4",
+ "symfony/translation-contracts": "^3.3",
+ "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.4",
+ "symfony/yaml": "^6.4 || ^7.4",
+ "symfonycasts/dynamic-forms": "^0.1",
+ "twig/extra-bundle": "^3.16",
+ "twig/intl-extra": "^3.16",
+ "twig/string-extra": "^3.16",
+ "twig/twig": "^3.16",
+ "webmozart/assert": "^1.11"
+ },
+ "conflict": {
+ "api-platform/serializer": "4.2.17",
+ "api-platform/symfony": "4.3.16",
+ "doctrine/orm": "2.20.7 || 3.5.3 || 3.6.8 || >=3.7",
+ "symfony/ux-live-component": "2.28.0 || 2.28.1"
+ },
+ "replace": {
+ "sylius/addressing": "self.version",
+ "sylius/addressing-bundle": "self.version",
+ "sylius/admin-bundle": "self.version",
+ "sylius/api-bundle": "self.version",
+ "sylius/attribute": "self.version",
+ "sylius/attribute-bundle": "self.version",
+ "sylius/channel": "self.version",
+ "sylius/channel-bundle": "self.version",
+ "sylius/core": "self.version",
+ "sylius/core-bundle": "self.version",
+ "sylius/currency": "self.version",
+ "sylius/currency-bundle": "self.version",
+ "sylius/customer": "self.version",
+ "sylius/customer-bundle": "self.version",
+ "sylius/inventory": "self.version",
+ "sylius/inventory-bundle": "self.version",
+ "sylius/locale": "self.version",
+ "sylius/locale-bundle": "self.version",
+ "sylius/money-bundle": "self.version",
+ "sylius/order": "self.version",
+ "sylius/order-bundle": "self.version",
+ "sylius/payment": "self.version",
+ "sylius/payment-bundle": "self.version",
+ "sylius/payum-bundle": "self.version",
+ "sylius/product": "self.version",
+ "sylius/product-bundle": "self.version",
+ "sylius/promotion": "self.version",
+ "sylius/promotion-bundle": "self.version",
+ "sylius/review": "self.version",
+ "sylius/review-bundle": "self.version",
+ "sylius/shipping": "self.version",
+ "sylius/shipping-bundle": "self.version",
+ "sylius/shop-bundle": "self.version",
+ "sylius/state-machine-abstraction": "self.version",
+ "sylius/taxation": "self.version",
+ "sylius/taxation-bundle": "self.version",
+ "sylius/taxonomy": "self.version",
+ "sylius/taxonomy-bundle": "self.version",
+ "sylius/ui-bundle": "self.version",
+ "sylius/user": "self.version",
+ "sylius/user-bundle": "self.version"
+ },
+ "require-dev": {
+ "behat/behat": "^3.22",
+ "behat/mink-selenium2-driver": "^1.7",
+ "consolidation/robo": "^4.0 || ^5.0",
+ "dbrekelmans/bdi": "^1.3",
+ "dmore/behat-chrome-extension": "^1.4",
+ "dmore/chrome-mink-driver": "^2.9",
+ "doctrine/cache": "^2.2",
+ "doctrine/data-fixtures": "^1.7",
+ "friends-of-behat/mink": "^1.11",
+ "friends-of-behat/mink-browserkit-driver": "^1.6",
+ "friends-of-behat/mink-debug-extension": "^2.1",
+ "friends-of-behat/mink-extension": "^2.7",
+ "friends-of-behat/page-object-extension": "^0.3",
+ "friends-of-behat/symfony-extension": "^2.6.2",
+ "friends-of-behat/variadic-extension": "^1.6",
+ "hwi/oauth-bundle": "^2.2",
+ "lchrusciel/api-test-case": "^5.3",
+ "matthiasnoback/symfony-config-test": "^6.0",
+ "matthiasnoback/symfony-dependency-injection-test": "^6.0",
+ "nyholm/psr7": "^1.8",
+ "phparkitect/phparkitect": "^0.6",
+ "phpstan/extension-installer": "^1.4",
+ "phpstan/phpstan": "^2.0",
+ "phpstan/phpstan-doctrine": "^2.0",
+ "phpstan/phpstan-symfony": "^2.0",
+ "phpstan/phpstan-webmozart-assert": "^2.0",
+ "phpunit/phpunit": "^11.5",
+ "psr/event-dispatcher": "^1.0",
+ "rector/rector": "^2.0",
+ "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.4",
+ "symfony/debug-bundle": "^6.4 || ^7.4",
+ "symfony/dotenv": "^6.4 || ^7.4",
+ "symfony/flex": "^2.4",
+ "symfony/runtime": "^6.4 || ^7.4",
+ "symfony/web-profiler-bundle": "^6.4 || ^7.4",
+ "symplify/monorepo-builder": "^11.0"
+ },
+ "suggest": {
+ "ext-iconv": "For better performance than using Symfony Polyfill Component",
+ "ext-intl": "For better performance than using Symfony Polyfill Component",
+ "ext-mbstring": "For better performance than using Symfony Polyfill Component",
+ "hwi/oauth-bundle": "If you want to use Facebook login (see https://docs.sylius.com/en/latest/cookbook/shop/facebook-login.html)",
+ "winzou/state-machine": "If you want to use Winzou State Machine (^0.4)",
+ "winzou/state-machine-bundle": "If you want to use Winzou State Machine (^0.6)"
+ },
+ "type": "library",
+ "extra": {
+ "symfony": {
+ "require": "^7.4",
+ "allow-contrib": false
+ },
+ "branch-alias": {
+ "dev-main": "2.2-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Sylius\\Behat\\": "src/Sylius/Behat/",
+ "Sylius\\Bundle\\": "src/Sylius/Bundle/",
+ "Sylius\\Component\\": "src/Sylius/Component/",
+ "Sylius\\Abstraction\\StateMachine\\": "src/Sylius/Abstraction/StateMachine/src"
+ },
+ "exclude-from-classmap": [
+ "src/Sylius/*/*/Tests/",
+ "src/Sylius/Component/Core/Test/Tests/",
+ "src/Sylius/*/*/test/",
+ "src/Sylius/*/*/phparkitect.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Paweł Jędrzejewski",
+ "homepage": "https://pjedrzejewski.com"
+ },
+ {
+ "name": "Sylius project",
+ "homepage": "https://sylius.com"
+ },
+ {
+ "name": "Community contributions",
+ "homepage": "https://github.com/Sylius/Sylius/contributors"
+ }
+ ],
+ "description": "E-Commerce platform for PHP, based on Symfony framework.",
+ "homepage": "https://sylius.com",
+ "support": {
+ "issues": "https://github.com/Sylius/Sylius/issues",
+ "source": "https://github.com/Sylius/Sylius/tree/v2.2.10"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sylius",
+ "type": "github"
+ }
+ ],
+ "time": "2026-09-28T12:53:01+00:00"
+ },
+ {
+ "name": "sylius/telemetry",
+ "version": "v1.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Sylius/Telemetry.git",
+ "reference": "45a3f6fa35bf8c56374d71acb525b55fe65def76"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Sylius/Telemetry/zipball/45a3f6fa35bf8c56374d71acb525b55fe65def76",
+ "reference": "45a3f6fa35bf8c56374d71acb525b55fe65def76",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.0",
+ "sylius/sylius": "^1.12 || ^2.0",
+ "symfony/config": "^5.4 || ^6.0 || ^7.0 || ^8.0",
+ "symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0 || ^8.0"
+ },
+ "require-dev": {
+ "doctrine/cache": "^2.2",
+ "phpunit/phpunit": "^10.5 || ^11.0",
+ "sylius/test-application": "~1.14.0@alpha || ^2.0.0@alpha",
+ "symfony/browser-kit": "^6.4 || ^7.4",
+ "symfony/debug-bundle": "^6.4 || ^7.4",
+ "symfony/dotenv": "^6.4 || ^7.4",
+ "symfony/http-kernel": "^6.4 || ^7.4",
+ "symfony/runtime": "^6.4 || ^7.4",
+ "symfony/var-exporter": "^6.4 || ^7.0",
+ "symfony/web-profiler-bundle": "^6.4 || ^7.4"
+ },
+ "type": "library",
+ "extra": {
+ "public-dir": "vendor/sylius/test-application/public"
+ },
+ "autoload": {
+ "psr-4": {
+ "Sylius\\Telemetry\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Telemetry integration for official Sylius plugins",
+ "support": {
+ "issues": "https://github.com/Sylius/Telemetry/issues",
+ "source": "https://github.com/Sylius/Telemetry/tree/v1.0.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sylius",
+ "type": "github"
+ }
+ ],
+ "time": "2026-02-03T09:10:53+00:00"
+ },
+ {
+ "name": "sylius/theme-bundle",
+ "version": "v2.5.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Sylius/SyliusThemeBundle.git",
+ "reference": "3249ad4ef6d60ccc96fe5238707572eb78e3aa99"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Sylius/SyliusThemeBundle/zipball/3249ad4ef6d60ccc96fe5238707572eb78e3aa99",
+ "reference": "3249ad4ef6d60ccc96fe5238707572eb78e3aa99",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.2",
+ "symfony/asset": "^6.4 || ^7.4 || ^8.0",
+ "symfony/config": "^6.4 || ^7.4 || ^8.0",
+ "symfony/console": "^6.4 || ^7.4 || ^8.0",
+ "symfony/dependency-injection": "^6.4 || ^7.4 || ^8.0",
+ "symfony/filesystem": "^6.4 || ^7.4 || ^8.0",
+ "symfony/finder": "^6.4 || ^7.4 || ^8.0",
+ "symfony/form": "^6.4 || ^7.4 || ^8.0",
+ "symfony/framework-bundle": "^6.4 || ^7.4 || ^8.0",
+ "symfony/http-foundation": "^6.4 || ^7.4 || ^8.0",
+ "symfony/http-kernel": "^6.4 || ^7.4 || ^8.0",
+ "symfony/options-resolver": "^6.4 || ^7.4 || ^8.0",
+ "symfony/service-contracts": "^3.0",
+ "symfony/translation": "^6.4 || ^7.4 || ^8.0",
+ "symfony/translation-contracts": "^3.0"
+ },
+ "require-dev": {
+ "matthiasnoback/symfony-config-test": "^5.0 || ^6.1",
+ "matthiasnoback/symfony-dependency-injection-test": "^5.0 || ^6.1",
+ "mikey179/vfsstream": "^1.6",
+ "phpspec/phpspec": "^7.0 || ^8.0",
+ "phpunit/phpunit": "^10.5 || ^11.0",
+ "rector/rector": "^1.0",
+ "sylius-labs/coding-standard": "^4.0.2",
+ "symfony/browser-kit": "^6.4 || ^7.4 || ^8.0",
+ "symfony/security-csrf": "^6.4 || ^7.4 || ^8.0",
+ "symfony/twig-bundle": "^6.4 || ^7.4 || ^8.0",
+ "twig/twig": "^3.0"
+ },
+ "type": "symfony-bundle",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.5-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Sylius\\Bundle\\ThemeBundle\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Kamil Kokot",
+ "homepage": "https://kamilkokot.com"
+ },
+ {
+ "name": "Sylius project",
+ "homepage": "https://sylius.com"
+ },
+ {
+ "name": "Community contributions",
+ "homepage": "https://github.com/Sylius/Sylius/contributors"
+ }
+ ],
+ "description": "Themes management for Symfony projects.",
+ "homepage": "https://sylius.com",
+ "keywords": [
+ "themes",
+ "theming"
+ ],
+ "support": {
+ "issues": "https://github.com/Sylius/SyliusThemeBundle/issues",
+ "source": "https://github.com/Sylius/SyliusThemeBundle/tree/v2.5.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sylius",
+ "type": "github"
+ }
+ ],
+ "time": "2026-02-06T13:40:45+00:00"
+ },
+ {
+ "name": "sylius/twig-extra",
+ "version": "v0.9.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Sylius/TwigExtra.git",
+ "reference": "57b582c9ca33f454e1b24f49ecbc442053cb3ac2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Sylius/TwigExtra/zipball/57b582c9ca33f454e1b24f49ecbc442053cb3ac2",
+ "reference": "57b582c9ca33f454e1b24f49ecbc442053cb3ac2",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.1",
+ "symfony/http-kernel": "^6.4 || ^7.0",
+ "symfony/twig-bundle": "^6.4 || ^7.0",
+ "symfony/ux-twig-component": "^2.17"
+ },
+ "conflict": {
+ "sylius/ui-bundle": "<2.0"
+ },
+ "require-dev": {
+ "matthiasnoback/symfony-dependency-injection-test": "^5.1",
+ "phpunit/phpunit": "^9.6",
+ "symfony/browser-kit": "^6.4 || ^7.0",
+ "symfony/console": "^6.4 || ^7.0",
+ "symfony/css-selector": "^6.4 || ^7.0",
+ "symfony/dotenv": "^6.4 || ^7.0",
+ "symfony/framework-bundle": "^6.4 || ^7.0",
+ "symfony/runtime": "^6.4 || ^7.0",
+ "symfony/yaml": "^6.4 || ^7.0"
+ },
+ "type": "library",
+ "extra": {
+ "symfony": {
+ "require": "7.1.*"
+ },
+ "branch-alias": {
+ "dev-main": "0.9-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Sylius\\TwigExtra\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Sylius project",
+ "homepage": "https://sylius.com"
+ },
+ {
+ "name": "Community contributions",
+ "homepage": "https://github.com/Sylius/Stack/contributors"
+ }
+ ],
+ "description": "Additional Twig extensions for your Symfony projects",
+ "support": {
+ "issues": "https://github.com/Sylius/TwigExtra/issues",
+ "source": "https://github.com/Sylius/TwigExtra/tree/v0.9.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sylius",
+ "type": "github"
+ }
+ ],
+ "time": "2025-09-26T08:18:45+00:00"
+ },
+ {
+ "name": "sylius/twig-hooks",
+ "version": "v0.9.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Sylius/TwigHooks.git",
+ "reference": "c047961d09c0bc084e241bae137630bcbf40667b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Sylius/TwigHooks/zipball/c047961d09c0bc084e241bae137630bcbf40667b",
+ "reference": "c047961d09c0bc084e241bae137630bcbf40667b",
+ "shasum": ""
+ },
+ "require": {
+ "laminas/laminas-stdlib": "^3.18",
+ "php": "^8.1",
+ "symfony/config": "^6.4 || ^7.0",
+ "symfony/dependency-injection": "^6.4 || ^7.0",
+ "symfony/expression-language": "^6.4 || ^7.0",
+ "symfony/http-kernel": "^6.4 || ^7.0",
+ "symfony/stopwatch": "^6.4 || ^7.0",
+ "symfony/twig-bundle": "^6.4 || ^7.0",
+ "symfony/ux-live-component": "^2.17",
+ "symfony/ux-twig-component": "^2.17",
+ "twig/twig": "^2.15 || ^3.0",
+ "webmozart/assert": "^1.9"
+ },
+ "require-dev": {
+ "matthiasnoback/symfony-config-test": "^5.1",
+ "matthiasnoback/symfony-dependency-injection-test": "^5.1",
+ "phpunit/phpunit": "^9.6",
+ "symfony/console": "^6.4 || ^7.0",
+ "symfony/dom-crawler": "^6.4 || ^7.0",
+ "symfony/dotenv": "^6.4 || ^7.0",
+ "symfony/framework-bundle": "^6.4 || ^7.0",
+ "symfony/runtime": "^6.4 || ^7.0",
+ "symfony/twig-bundle": "^6.4 || ^7.0",
+ "symfony/web-profiler-bundle": "^6.4 || ^7.0",
+ "symfony/yaml": "^6.4 || ^7.0"
+ },
+ "suggest": {
+ "symfony/ux-twig-component": "Symfony's package providing Twig components."
+ },
+ "type": "library",
+ "extra": {
+ "symfony": {
+ "require": "7.1.*"
+ },
+ "branch-alias": {
+ "dev-main": "0.9-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Sylius\\TwigHooks\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Sylius project",
+ "homepage": "https://sylius.com"
+ },
+ {
+ "name": "Community contributions",
+ "homepage": "https://github.com/Sylius/Stack/contributors"
+ }
+ ],
+ "description": "Composable Twig layouts",
+ "support": {
+ "source": "https://github.com/Sylius/TwigHooks/tree/v0.9.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sylius",
+ "type": "github"
+ }
+ ],
+ "time": "2025-09-26T08:18:45+00:00"
+ },
+ {
+ "name": "symfony/asset",
+ "version": "v6.4.34",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/asset.git",
+ "reference": "1bd59aa278691b6310ca56b996cf6e2619a6a347"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/asset/zipball/1bd59aa278691b6310ca56b996cf6e2619a6a347",
+ "reference": "1bd59aa278691b6310ca56b996cf6e2619a6a347",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "conflict": {
+ "symfony/http-foundation": "<5.4"
+ },
+ "require-dev": {
+ "symfony/http-client": "^5.4|^6.0|^7.0",
+ "symfony/http-foundation": "^5.4|^6.0|^7.0",
+ "symfony/http-kernel": "^5.4|^6.0|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Asset\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Manages URL generation and versioning of web assets such as CSS stylesheets, JavaScript files and image files",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/asset/tree/v6.4.34"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-02-07T09:15:39+00:00"
+ },
+ {
+ "name": "symfony/cache",
+ "version": "v6.4.47",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/cache.git",
+ "reference": "7ce0de35c828d29a5602134cdcf82b9c301b198f"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/cache/zipball/7ce0de35c828d29a5602134cdcf82b9c301b198f",
+ "reference": "7ce0de35c828d29a5602134cdcf82b9c301b198f",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "psr/cache": "^2.0|^3.0",
+ "psr/log": "^1.1|^2|^3",
+ "symfony/cache-contracts": "^2.5|^3",
+ "symfony/service-contracts": "^2.5|^3",
+ "symfony/var-exporter": "^6.3.6|^7.0"
+ },
+ "conflict": {
+ "doctrine/dbal": "<2.13.1",
+ "symfony/dependency-injection": "<5.4",
+ "symfony/http-kernel": "<5.4",
+ "symfony/var-dumper": "<5.4"
+ },
+ "provide": {
+ "psr/cache-implementation": "2.0|3.0",
+ "psr/simple-cache-implementation": "1.0|2.0|3.0",
+ "symfony/cache-implementation": "1.1|2.0|3.0"
+ },
+ "require-dev": {
+ "cache/integration-tests": "^0.18",
+ "doctrine/dbal": "^2.13.1|^3|^4",
+ "predis/predis": "^1.1|^2.0",
+ "psr/simple-cache": "^1.0|^2.0|^3.0",
+ "symfony/config": "^5.4|^6.0|^7.0",
+ "symfony/dependency-injection": "^5.4|^6.0|^7.0",
+ "symfony/filesystem": "^5.4|^6.0|^7.0",
+ "symfony/http-kernel": "^5.4|^6.0|^7.0",
+ "symfony/messenger": "^5.4|^6.0|^7.0",
+ "symfony/var-dumper": "^5.4|^6.0|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Cache\\": ""
+ },
+ "classmap": [
+ "Traits/ValueWrapper.php"
+ ],
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides extended PSR-6, PSR-16 (and tags) implementations",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "caching",
+ "psr6"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/cache/tree/v6.4.47"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-09-29T12:25:26+00:00"
+ },
+ {
+ "name": "symfony/cache-contracts",
+ "version": "v3.7.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/cache-contracts.git",
+ "reference": "9789738bc19af1106dc54d6afba9a0b467516cf2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/9789738bc19af1106dc54d6afba9a0b467516cf2",
+ "reference": "9789738bc19af1106dc54d6afba9a0b467516cf2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "psr/cache": "^3.0"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/contracts",
+ "name": "symfony/contracts"
+ },
+ "branch-alias": {
+ "dev-main": "3.7-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Contracts\\Cache\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Generic abstractions related to caching",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "abstractions",
+ "contracts",
+ "decoupling",
+ "interfaces",
+ "interoperability",
+ "standards"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/cache-contracts/tree/v3.7.1"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-06-05T06:23:12+00:00"
+ },
+ {
+ "name": "symfony/clock",
+ "version": "v6.4.30",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/clock.git",
+ "reference": "fb2df4bc9e3037c4765ba7fd29e00167001a9b68"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/clock/zipball/fb2df4bc9e3037c4765ba7fd29e00167001a9b68",
+ "reference": "fb2df4bc9e3037c4765ba7fd29e00167001a9b68",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "psr/clock": "^1.0",
+ "symfony/polyfill-php83": "^1.28"
+ },
+ "provide": {
+ "psr/clock-implementation": "1.0"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "Resources/now.php"
+ ],
+ "psr-4": {
+ "Symfony\\Component\\Clock\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Decouples applications from the system clock",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "clock",
+ "psr20",
+ "time"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/clock/tree/v6.4.30"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-11-11T21:24:34+00:00"
+ },
+ {
+ "name": "symfony/config",
+ "version": "v6.4.44",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/config.git",
+ "reference": "6a5abf67fd5b138df96984fe6f8fd736fb7d6b29"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/config/zipball/6a5abf67fd5b138df96984fe6f8fd736fb7d6b29",
+ "reference": "6a5abf67fd5b138df96984fe6f8fd736fb7d6b29",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/filesystem": "^5.4|^6.0|^7.0",
+ "symfony/polyfill-ctype": "~1.8"
+ },
+ "conflict": {
+ "symfony/finder": "<5.4",
+ "symfony/service-contracts": "<2.5"
+ },
+ "require-dev": {
+ "symfony/event-dispatcher": "^5.4|^6.0|^7.0",
+ "symfony/finder": "^5.4|^6.0|^7.0",
+ "symfony/messenger": "^5.4|^6.0|^7.0",
+ "symfony/service-contracts": "^2.5|^3",
+ "symfony/yaml": "^5.4|^6.0|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Config\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Helps you find, load, combine, autofill and validate configuration values of any kind",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/config/tree/v6.4.44"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-08-20T09:38:57+00:00"
+ },
+ {
+ "name": "symfony/console",
+ "version": "v6.4.47",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/console.git",
+ "reference": "0f758dfab4608cac6a342b7e7f80cb84c9cc98ca"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/console/zipball/0f758dfab4608cac6a342b7e7f80cb84c9cc98ca",
+ "reference": "0f758dfab4608cac6a342b7e7f80cb84c9cc98ca",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/polyfill-mbstring": "~1.0",
+ "symfony/service-contracts": "^2.5|^3",
+ "symfony/string": "^5.4|^6.0|^7.0"
+ },
+ "conflict": {
+ "symfony/dependency-injection": "<5.4",
+ "symfony/dotenv": "<5.4",
+ "symfony/event-dispatcher": "<5.4",
+ "symfony/lock": "<5.4",
+ "symfony/process": "<5.4"
+ },
+ "provide": {
+ "psr/log-implementation": "1.0|2.0|3.0"
+ },
+ "require-dev": {
+ "psr/log": "^1|^2|^3",
+ "symfony/config": "^5.4|^6.0|^7.0",
+ "symfony/dependency-injection": "^5.4|^6.0|^7.0",
+ "symfony/event-dispatcher": "^5.4|^6.0|^7.0",
+ "symfony/http-foundation": "^6.4|^7.0",
+ "symfony/http-kernel": "^6.4|^7.0",
+ "symfony/lock": "^5.4|^6.0|^7.0",
+ "symfony/messenger": "^5.4|^6.0|^7.0",
+ "symfony/process": "^5.4|^6.0|^7.0",
+ "symfony/stopwatch": "^5.4|^6.0|^7.0",
+ "symfony/var-dumper": "^5.4|^6.0|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Console\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Eases the creation of beautiful and testable command line interfaces",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "cli",
+ "command-line",
+ "console",
+ "terminal"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/console/tree/v6.4.47"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-09-18T10:23:48+00:00"
+ },
+ {
+ "name": "symfony/dependency-injection",
+ "version": "v6.4.47",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/dependency-injection.git",
+ "reference": "c7ffb6da26739b461ae18dca3438bc68c35cd2a0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/c7ffb6da26739b461ae18dca3438bc68c35cd2a0",
+ "reference": "c7ffb6da26739b461ae18dca3438bc68c35cd2a0",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "psr/container": "^1.1|^2.0",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/service-contracts": "^2.5|^3.0",
+ "symfony/var-exporter": "^6.4.20|^7.2.5"
+ },
+ "conflict": {
+ "ext-psr": "<1.1|>=2",
+ "symfony/config": "<6.1",
+ "symfony/finder": "<5.4",
+ "symfony/proxy-manager-bridge": "<6.3",
+ "symfony/yaml": "<5.4"
+ },
+ "provide": {
+ "psr/container-implementation": "1.1|2.0",
+ "symfony/service-implementation": "1.1|2.0|3.0"
+ },
+ "require-dev": {
+ "symfony/config": "^6.1|^7.0",
+ "symfony/expression-language": "^5.4|^6.0|^7.0",
+ "symfony/yaml": "^5.4|^6.0|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\DependencyInjection\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Allows you to standardize and centralize the way objects are constructed in your application",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/dependency-injection/tree/v6.4.47"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-09-29T10:29:26+00:00"
+ },
+ {
+ "name": "symfony/deprecation-contracts",
+ "version": "v3.7.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/deprecation-contracts.git",
+ "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/f3202fa1b5097b0af062dc978b32ecf63404e31d",
+ "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/contracts",
+ "name": "symfony/contracts"
+ },
+ "branch-alias": {
+ "dev-main": "3.7-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "function.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "A generic function and convention to trigger deprecation notices",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.1"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-06-05T06:23:12+00:00"
+ },
+ {
+ "name": "symfony/doctrine-bridge",
+ "version": "v6.4.47",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/doctrine-bridge.git",
+ "reference": "1fa8f2328ad79314c036bc61754d2f86112e7360"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/1fa8f2328ad79314c036bc61754d2f86112e7360",
+ "reference": "1fa8f2328ad79314c036bc61754d2f86112e7360",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/event-manager": "^1.2|^2",
+ "doctrine/persistence": "^2.5|^3.1|^4",
+ "php": ">=8.1",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/polyfill-ctype": "~1.8",
+ "symfony/polyfill-mbstring": "~1.0",
+ "symfony/service-contracts": "^2.5|^3"
+ },
+ "conflict": {
+ "doctrine/dbal": "<2.13.1",
+ "doctrine/lexer": "<1.1",
+ "doctrine/orm": "<2.15",
+ "symfony/cache": "<5.4",
+ "symfony/dependency-injection": "<6.2",
+ "symfony/form": "<5.4.38|>=6,<6.4.6|>=7,<7.0.6",
+ "symfony/http-foundation": "<6.3",
+ "symfony/http-kernel": "<6.2",
+ "symfony/lock": "<6.3",
+ "symfony/messenger": "<5.4",
+ "symfony/property-info": "<5.4|>=8",
+ "symfony/security-bundle": "<5.4",
+ "symfony/security-core": "<6.4",
+ "symfony/validator": "<6.4.44|>=7.0,<7.4.17|>=8.0,<8.1.5"
+ },
+ "require-dev": {
+ "doctrine/collections": "^1.0|^2.0",
+ "doctrine/data-fixtures": "^1.1|^2",
+ "doctrine/dbal": "^2.13.1|^3|^4",
+ "doctrine/orm": "^2.15|^3",
+ "psr/log": "^1|^2|^3",
+ "symfony/cache": "^5.4|^6.0|^7.0",
+ "symfony/config": "^5.4|^6.0|^7.0",
+ "symfony/dependency-injection": "^6.2|^7.0",
+ "symfony/doctrine-messenger": "^5.4|^6.0|^7.0",
+ "symfony/expression-language": "^5.4|^6.0|^7.0",
+ "symfony/form": "^5.4.38|^6.4.6|^7.0.6",
+ "symfony/http-kernel": "^6.3|^7.0",
+ "symfony/lock": "^6.3|^7.0",
+ "symfony/messenger": "^5.4|^6.0|^7.0",
+ "symfony/property-access": "^5.4|^6.0|^7.0",
+ "symfony/property-info": "^5.4|^6.0|^7.0",
+ "symfony/proxy-manager-bridge": "^6.4",
+ "symfony/security-core": "^6.4|^7.0",
+ "symfony/stopwatch": "^5.4|^6.0|^7.0",
+ "symfony/translation": "^5.4|^6.0|^7.0",
+ "symfony/uid": "^5.4|^6.0|^7.0",
+ "symfony/validator": "^6.4|^7.0",
+ "symfony/var-dumper": "^5.4|^6.0|^7.0"
+ },
+ "type": "symfony-bridge",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Bridge\\Doctrine\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides integration for Doctrine with various Symfony components",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/doctrine-bridge/tree/v6.4.47"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-09-28T17:32:40+00:00"
+ },
+ {
+ "name": "symfony/doctrine-messenger",
+ "version": "v6.4.47",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/doctrine-messenger.git",
+ "reference": "77d265ac40864f9f0488a0233ac940ad5ff4b0e1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/doctrine-messenger/zipball/77d265ac40864f9f0488a0233ac940ad5ff4b0e1",
+ "reference": "77d265ac40864f9f0488a0233ac940ad5ff4b0e1",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/dbal": "^2.13|^3|^4",
+ "php": ">=8.1",
+ "symfony/messenger": "^5.4|^6.0|^7.0",
+ "symfony/service-contracts": "^2.5|^3"
+ },
+ "conflict": {
+ "doctrine/persistence": "<1.3"
+ },
+ "require-dev": {
+ "doctrine/persistence": "^1.3|^2|^3|^4",
+ "symfony/property-access": "^5.4|^6.0|^7.0",
+ "symfony/serializer": "^5.4|^6.0|^7.0"
+ },
+ "type": "symfony-messenger-bridge",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Messenger\\Bridge\\Doctrine\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony Doctrine Messenger Bridge",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/doctrine-messenger/tree/v6.4.47"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-09-24T07:57:43+00:00"
+ },
+ {
+ "name": "symfony/error-handler",
+ "version": "v6.4.47",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/error-handler.git",
+ "reference": "0b7be4e3f15ad53b46941bed1e59863245de8647"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/error-handler/zipball/0b7be4e3f15ad53b46941bed1e59863245de8647",
+ "reference": "0b7be4e3f15ad53b46941bed1e59863245de8647",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "psr/log": "^1|^2|^3",
+ "symfony/var-dumper": "^5.4|^6.0|^7.0"
+ },
+ "conflict": {
+ "symfony/deprecation-contracts": "<2.5",
+ "symfony/http-kernel": "<6.4"
+ },
+ "require-dev": {
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/http-kernel": "^6.4|^7.0",
+ "symfony/serializer": "^5.4|^6.0|^7.0"
+ },
+ "bin": [
+ "Resources/bin/patch-type-declarations"
+ ],
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\ErrorHandler\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides tools to manage errors and ease debugging PHP code",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/error-handler/tree/v6.4.47"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-09-22T18:37:41+00:00"
+ },
+ {
+ "name": "symfony/event-dispatcher",
+ "version": "v6.4.44",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/event-dispatcher.git",
+ "reference": "d7110a878e7e7cbd8aaebe9f55da8af885b8c0af"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/d7110a878e7e7cbd8aaebe9f55da8af885b8c0af",
+ "reference": "d7110a878e7e7cbd8aaebe9f55da8af885b8c0af",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/event-dispatcher-contracts": "^2.5|^3"
+ },
+ "conflict": {
+ "symfony/dependency-injection": "<5.4",
+ "symfony/service-contracts": "<2.5"
+ },
+ "provide": {
+ "psr/event-dispatcher-implementation": "1.0",
+ "symfony/event-dispatcher-implementation": "2.0|3.0"
+ },
+ "require-dev": {
+ "psr/log": "^1|^2|^3",
+ "symfony/config": "^5.4|^6.0|^7.0",
+ "symfony/dependency-injection": "^5.4|^6.0|^7.0",
+ "symfony/error-handler": "^5.4|^6.0|^7.0",
+ "symfony/expression-language": "^5.4|^6.0|^7.0",
+ "symfony/http-foundation": "^5.4|^6.0|^7.0",
+ "symfony/service-contracts": "^2.5|^3",
+ "symfony/stopwatch": "^5.4|^6.0|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\EventDispatcher\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/event-dispatcher/tree/v6.4.44"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-08-21T14:01:40+00:00"
+ },
+ {
+ "name": "symfony/event-dispatcher-contracts",
+ "version": "v3.7.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/event-dispatcher-contracts.git",
+ "reference": "c7de7a00ffb67842132da02ea92988a39ccd9f4e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/c7de7a00ffb67842132da02ea92988a39ccd9f4e",
+ "reference": "c7de7a00ffb67842132da02ea92988a39ccd9f4e",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "psr/event-dispatcher": "^1"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/contracts",
+ "name": "symfony/contracts"
+ },
+ "branch-alias": {
+ "dev-main": "3.7-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Contracts\\EventDispatcher\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Generic abstractions related to dispatching event",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "abstractions",
+ "contracts",
+ "decoupling",
+ "interfaces",
+ "interoperability",
+ "standards"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.7.1"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-06-05T06:23:12+00:00"
+ },
+ {
+ "name": "symfony/expression-language",
+ "version": "v6.4.45",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/expression-language.git",
+ "reference": "91edabba1aff9da3326eee2a6e7666d1c1270751"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/expression-language/zipball/91edabba1aff9da3326eee2a6e7666d1c1270751",
+ "reference": "91edabba1aff9da3326eee2a6e7666d1c1270751",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/cache": "^5.4|^6.0|^7.0",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/service-contracts": "^2.5|^3"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\ExpressionLanguage\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides an engine that can compile and evaluate expressions",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/expression-language/tree/v6.4.45"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-08-30T00:27:22+00:00"
+ },
+ {
+ "name": "symfony/filesystem",
+ "version": "v6.4.45",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/filesystem.git",
+ "reference": "6dabf41c21957a2060f101bd7bf7e322989866bd"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/filesystem/zipball/6dabf41c21957a2060f101bd7bf7e322989866bd",
+ "reference": "6dabf41c21957a2060f101bd7bf7e322989866bd",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/polyfill-ctype": "~1.8",
+ "symfony/polyfill-mbstring": "~1.8"
+ },
+ "require-dev": {
+ "symfony/process": "^5.4|^6.4|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Filesystem\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides basic utilities for the filesystem",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/filesystem/tree/v6.4.45"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-08-23T07:59:31+00:00"
+ },
+ {
+ "name": "symfony/finder",
+ "version": "v6.4.47",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/finder.git",
+ "reference": "c8d1b2a09bb820175d1f2f9a3cf30303200b10a7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/finder/zipball/c8d1b2a09bb820175d1f2f9a3cf30303200b10a7",
+ "reference": "c8d1b2a09bb820175d1f2f9a3cf30303200b10a7",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "require-dev": {
+ "symfony/filesystem": "^6.0|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Finder\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Finds files and directories via an intuitive fluent interface",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/finder/tree/v6.4.47"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-09-25T09:03:59+00:00"
+ },
+ {
+ "name": "symfony/form",
+ "version": "v6.4.47",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/form.git",
+ "reference": "5753fe93e0e83dfea93b697d70e52bcb36a3f3e4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/form/zipball/5753fe93e0e83dfea93b697d70e52bcb36a3f3e4",
+ "reference": "5753fe93e0e83dfea93b697d70e52bcb36a3f3e4",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/event-dispatcher": "^5.4|^6.0|^7.0",
+ "symfony/options-resolver": "^5.4|^6.0|^7.0",
+ "symfony/polyfill-ctype": "~1.8",
+ "symfony/polyfill-intl-icu": "^1.21",
+ "symfony/polyfill-mbstring": "~1.0",
+ "symfony/property-access": "^5.4|^6.0|^7.0",
+ "symfony/service-contracts": "^2.5|^3"
+ },
+ "conflict": {
+ "symfony/console": "<5.4",
+ "symfony/dependency-injection": "<5.4",
+ "symfony/doctrine-bridge": "<5.4.21|>=6,<6.2.7",
+ "symfony/error-handler": "<5.4",
+ "symfony/framework-bundle": "<5.4",
+ "symfony/http-kernel": "<5.4",
+ "symfony/translation": "<5.4.35|>=6.0,<6.3.12|>=6.4,<6.4.3|>=7.0,<7.0.3",
+ "symfony/translation-contracts": "<2.5",
+ "symfony/twig-bridge": "<6.3"
+ },
+ "require-dev": {
+ "doctrine/collections": "^1.0|^2.0",
+ "symfony/config": "^5.4|^6.0|^7.0",
+ "symfony/console": "^5.4|^6.0|^7.0",
+ "symfony/dependency-injection": "^5.4|^6.0|^7.0",
+ "symfony/expression-language": "^5.4|^6.0|^7.0",
+ "symfony/html-sanitizer": "^6.1|^7.0",
+ "symfony/http-foundation": "^5.4|^6.0|^7.0",
+ "symfony/http-kernel": "^5.4|^6.0|^7.0",
+ "symfony/intl": "^5.4|^6.0|^7.0",
+ "symfony/security-core": "^6.2|^7.0",
+ "symfony/security-csrf": "^5.4|^6.0|^7.0",
+ "symfony/translation": "^5.4.35|~6.3.12|^6.4.3|^7.0.3",
+ "symfony/uid": "^5.4|^6.0|^7.0",
+ "symfony/validator": "^5.4|^6.0|^7.0",
+ "symfony/var-dumper": "^5.4|^6.0|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Form\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Allows to easily create, process and reuse HTML forms",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/form/tree/v6.4.47"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-09-29T11:58:14+00:00"
+ },
+ {
+ "name": "symfony/framework-bundle",
+ "version": "v6.4.47",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/framework-bundle.git",
+ "reference": "a513e4e5d964599331811657c0e2f2c5fb21c293"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/a513e4e5d964599331811657c0e2f2c5fb21c293",
+ "reference": "a513e4e5d964599331811657c0e2f2c5fb21c293",
+ "shasum": ""
+ },
+ "require": {
+ "composer-runtime-api": ">=2.1",
+ "ext-xml": "*",
+ "php": ">=8.1",
+ "symfony/cache": "^5.4|^6.0|^7.0",
+ "symfony/config": "^6.1|^7.0",
+ "symfony/dependency-injection": "^6.4.12|^7.0",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/error-handler": "^6.1|^7.0",
+ "symfony/event-dispatcher": "^5.4|^6.0|^7.0",
+ "symfony/filesystem": "^5.4|^6.0|^7.0",
+ "symfony/finder": "^5.4|^6.0|^7.0",
+ "symfony/http-foundation": "^6.4|^7.0",
+ "symfony/http-kernel": "^6.4",
+ "symfony/polyfill-mbstring": "~1.0",
+ "symfony/routing": "^6.4|^7.0"
+ },
+ "conflict": {
+ "doctrine/annotations": "<1.13.1",
+ "doctrine/persistence": "<1.3",
+ "phpdocumentor/reflection-docblock": "<3.2.2",
+ "phpdocumentor/type-resolver": "<1.4.0",
+ "symfony/asset": "<5.4",
+ "symfony/asset-mapper": "<6.4",
+ "symfony/clock": "<6.3",
+ "symfony/console": "<6.4.43|>=7.0",
+ "symfony/dom-crawler": "<6.4",
+ "symfony/dotenv": "<5.4",
+ "symfony/form": "<5.4",
+ "symfony/http-client": "<6.3",
+ "symfony/lock": "<5.4",
+ "symfony/mailer": "<6.4.44|>=7.0,<7.4.17|>=8.0,<8.1.5",
+ "symfony/messenger": "<6.3",
+ "symfony/mime": "<6.4.37|>=7.0,<7.4.9",
+ "symfony/property-access": "<5.4",
+ "symfony/property-info": "<5.4",
+ "symfony/runtime": "<5.4.45|>=6.0,<6.4.13|>=7.0,<7.1.6",
+ "symfony/scheduler": "<6.4.4|>=7.0.0,<7.0.4",
+ "symfony/security-core": "<5.4",
+ "symfony/security-csrf": "<5.4",
+ "symfony/serializer": "<6.4",
+ "symfony/stopwatch": "<5.4",
+ "symfony/translation": "<6.4",
+ "symfony/twig-bridge": "<5.4",
+ "symfony/twig-bundle": "<5.4",
+ "symfony/validator": "<6.4",
+ "symfony/web-profiler-bundle": "<6.4",
+ "symfony/workflow": "<6.4"
+ },
+ "require-dev": {
+ "doctrine/annotations": "^1.13.1|^2",
+ "doctrine/persistence": "^1.3|^2|^3|^4",
+ "dragonmantank/cron-expression": "^3.1",
+ "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0",
+ "seld/jsonlint": "^1.10",
+ "symfony/asset": "^5.4|^6.0|^7.0",
+ "symfony/asset-mapper": "^6.4|^7.0",
+ "symfony/browser-kit": "^5.4|^6.0|^7.0",
+ "symfony/clock": "^6.2|^7.0",
+ "symfony/console": "^6.4.43|^7.0",
+ "symfony/css-selector": "^5.4|^6.0|^7.0",
+ "symfony/dom-crawler": "^6.4|^7.0",
+ "symfony/dotenv": "^5.4|^6.0|^7.0",
+ "symfony/expression-language": "^5.4|^6.0|^7.0",
+ "symfony/form": "^5.4|^6.0|^7.0",
+ "symfony/html-sanitizer": "^6.1|^7.0",
+ "symfony/http-client": "^6.3|^7.0",
+ "symfony/lock": "^5.4|^6.0|^7.0",
+ "symfony/mailer": "^5.4|^6.0|^7.0",
+ "symfony/messenger": "^6.3|^7.0",
+ "symfony/mime": "^6.4.37|^7.4.9",
+ "symfony/notifier": "^5.4|^6.0|^7.0",
+ "symfony/polyfill-intl-icu": "~1.0",
+ "symfony/process": "^5.4|^6.0|^7.0",
+ "symfony/property-info": "^5.4|^6.0|^7.0",
+ "symfony/rate-limiter": "^5.4|^6.0|^7.0",
+ "symfony/scheduler": "^6.4.4|^7.0.4",
+ "symfony/security-bundle": "^5.4|^6.0|^7.0",
+ "symfony/semaphore": "^5.4|^6.0|^7.0",
+ "symfony/serializer": "^6.4|^7.0",
+ "symfony/stopwatch": "^5.4|^6.0|^7.0",
+ "symfony/string": "^5.4|^6.0|^7.0",
+ "symfony/translation": "^6.4|^7.0",
+ "symfony/twig-bundle": "^5.4|^6.0|^7.0",
+ "symfony/uid": "^5.4|^6.0|^7.0",
+ "symfony/validator": "^6.4|^7.0",
+ "symfony/web-link": "^5.4|^6.0|^7.0",
+ "symfony/workflow": "^6.4|^7.0",
+ "symfony/yaml": "^5.4|^6.0|^7.0",
+ "twig/twig": "^2.10|^3.0.4|^4.0"
+ },
+ "type": "symfony-bundle",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Bundle\\FrameworkBundle\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides a tight integration between Symfony components and the Symfony full-stack framework",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/framework-bundle/tree/v6.4.47"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-09-28T18:03:43+00:00"
+ },
+ {
+ "name": "symfony/http-client",
+ "version": "v6.4.47",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/http-client.git",
+ "reference": "c064327ae0f64b7d5b27ed16fd8a7fa20350180c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/http-client/zipball/c064327ae0f64b7d5b27ed16fd8a7fa20350180c",
+ "reference": "c064327ae0f64b7d5b27ed16fd8a7fa20350180c",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "psr/log": "^1|^2|^3",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/http-client-contracts": "~3.4.4|^3.5.2",
+ "symfony/polyfill-php83": "^1.29",
+ "symfony/service-contracts": "^2.5|^3"
+ },
+ "conflict": {
+ "php-http/discovery": "<1.15",
+ "symfony/http-foundation": "<6.3"
+ },
+ "provide": {
+ "php-http/async-client-implementation": "*",
+ "php-http/client-implementation": "*",
+ "psr/http-client-implementation": "1.0",
+ "symfony/http-client-implementation": "3.0"
+ },
+ "require-dev": {
+ "amphp/amp": "^2.5",
+ "amphp/http-client": "^4.2.1",
+ "amphp/http-tunnel": "^1.0",
+ "amphp/socket": "^1.1",
+ "guzzlehttp/promises": "^1.4|^2.0",
+ "nyholm/psr7": "^1.0",
+ "php-http/httplug": "^1.0|^2.0",
+ "psr/http-client": "^1.0",
+ "symfony/dependency-injection": "^5.4|^6.0|^7.0",
+ "symfony/http-kernel": "^5.4|^6.0|^7.0",
+ "symfony/messenger": "^5.4|^6.0|^7.0",
+ "symfony/process": "^5.4|^6.0|^7.0",
+ "symfony/stopwatch": "^5.4|^6.0|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\HttpClient\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "http"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/http-client/tree/v6.4.47"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-09-24T10:58:56+00:00"
+ },
+ {
+ "name": "symfony/http-client-contracts",
+ "version": "v3.7.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/http-client-contracts.git",
+ "reference": "35be0019e2c2c9fba80f9dc033290a5240f7b44f"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/35be0019e2c2c9fba80f9dc033290a5240f7b44f",
+ "reference": "35be0019e2c2c9fba80f9dc033290a5240f7b44f",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/contracts",
+ "name": "symfony/contracts"
+ },
+ "branch-alias": {
+ "dev-main": "3.7-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Contracts\\HttpClient\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Test/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Generic abstractions related to HTTP clients",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "abstractions",
+ "contracts",
+ "decoupling",
+ "interfaces",
+ "interoperability",
+ "standards"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/http-client-contracts/tree/v3.7.3"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-08-04T08:41:16+00:00"
+ },
+ {
+ "name": "symfony/http-foundation",
+ "version": "v6.4.47",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/http-foundation.git",
+ "reference": "8c9d7b59c9315ad6801f3ac49234591d92f95634"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/http-foundation/zipball/8c9d7b59c9315ad6801f3ac49234591d92f95634",
+ "reference": "8c9d7b59c9315ad6801f3ac49234591d92f95634",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/polyfill-mbstring": "~1.1",
+ "symfony/polyfill-php83": "^1.27"
+ },
+ "conflict": {
+ "symfony/cache": "<6.4.12|>=7.0,<7.1.5"
+ },
+ "require-dev": {
+ "doctrine/dbal": "^2.13.1|^3|^4",
+ "predis/predis": "^1.1|^2.0",
+ "symfony/cache": "^6.4.12|^7.1.5",
+ "symfony/dependency-injection": "^5.4|^6.0|^7.0",
+ "symfony/expression-language": "^5.4|^6.0|^7.0",
+ "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4|^7.0",
+ "symfony/mime": "^5.4|^6.0|^7.0",
+ "symfony/rate-limiter": "^5.4|^6.0|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\HttpFoundation\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Defines an object-oriented layer for the HTTP specification",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/http-foundation/tree/v6.4.47"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-09-24T07:12:21+00:00"
+ },
+ {
+ "name": "symfony/http-kernel",
+ "version": "v6.4.47",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/http-kernel.git",
+ "reference": "238e4681af2765043b4ed139843b3bded4220eb6"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/http-kernel/zipball/238e4681af2765043b4ed139843b3bded4220eb6",
+ "reference": "238e4681af2765043b4ed139843b3bded4220eb6",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "psr/log": "^1|^2|^3",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/error-handler": "^6.4|^7.0",
+ "symfony/event-dispatcher": "^5.4|^6.0|^7.0",
+ "symfony/http-foundation": "^6.4|^7.0",
+ "symfony/polyfill-ctype": "^1.8"
+ },
+ "conflict": {
+ "symfony/browser-kit": "<5.4",
+ "symfony/cache": "<5.4",
+ "symfony/config": "<6.1",
+ "symfony/console": "<5.4",
+ "symfony/dependency-injection": "<6.4",
+ "symfony/doctrine-bridge": "<5.4",
+ "symfony/form": "<5.4",
+ "symfony/http-client": "<5.4",
+ "symfony/http-client-contracts": "<2.5",
+ "symfony/mailer": "<5.4",
+ "symfony/messenger": "<5.4",
+ "symfony/translation": "<5.4",
+ "symfony/translation-contracts": "<2.5",
+ "symfony/twig-bridge": "<5.4",
+ "symfony/validator": "<6.4",
+ "symfony/var-dumper": "<6.3",
+ "twig/twig": "<2.13"
+ },
+ "provide": {
+ "psr/log-implementation": "1.0|2.0|3.0"
+ },
+ "require-dev": {
+ "psr/cache": "^1.0|^2.0|^3.0",
+ "symfony/browser-kit": "^5.4|^6.0|^7.0",
+ "symfony/clock": "^6.2|^7.0",
+ "symfony/config": "^6.1|^7.0",
+ "symfony/console": "^5.4|^6.0|^7.0",
+ "symfony/css-selector": "^5.4|^6.0|^7.0",
+ "symfony/dependency-injection": "^6.4.1|^7.0.1",
+ "symfony/dom-crawler": "^5.4|^6.0|^7.0",
+ "symfony/expression-language": "^5.4|^6.0|^7.0",
+ "symfony/finder": "^5.4|^6.0|^7.0",
+ "symfony/http-client-contracts": "^2.5|^3",
+ "symfony/process": "^5.4|^6.0|^7.0",
+ "symfony/property-access": "^5.4.5|^6.0.5|^7.0",
+ "symfony/routing": "^5.4|^6.0|^7.0",
+ "symfony/serializer": "^6.4.4|^7.0.4",
+ "symfony/stopwatch": "^5.4|^6.0|^7.0",
+ "symfony/translation": "^5.4|^6.0|^7.0",
+ "symfony/translation-contracts": "^2.5|^3",
+ "symfony/uid": "^5.4|^6.0|^7.0",
+ "symfony/validator": "^6.4|^7.0",
+ "symfony/var-dumper": "^5.4|^6.4|^7.0",
+ "symfony/var-exporter": "^6.2|^7.0",
+ "twig/twig": "^2.13|^3.0.4|^4.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\HttpKernel\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides a structured process for converting a Request into a Response",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/http-kernel/tree/v6.4.47"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-09-29T14:02:38+00:00"
+ },
+ {
+ "name": "symfony/intl",
+ "version": "v6.4.44",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/intl.git",
+ "reference": "d79905d256652f1353b26e1a59d89f835b2d31cf"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/intl/zipball/d79905d256652f1353b26e1a59d89f835b2d31cf",
+ "reference": "d79905d256652f1353b26e1a59d89f835b2d31cf",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "require-dev": {
+ "symfony/filesystem": "^5.4|^6.0|^7.0",
+ "symfony/finder": "^5.4|^6.0|^7.0",
+ "symfony/var-exporter": "^5.4|^6.0|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Intl\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/",
+ "/Resources/data/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@gmail.com"
+ },
+ {
+ "name": "Eriksen Costa",
+ "email": "eriksen.costa@infranology.com.br"
+ },
+ {
+ "name": "Igor Wiedler",
+ "email": "igor@wiedler.ch"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides access to the localization data of the ICU library",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "i18n",
+ "icu",
+ "internationalization",
+ "intl",
+ "l10n",
+ "localization"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/intl/tree/v6.4.44"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-08-20T17:31:16+00:00"
+ },
+ {
+ "name": "symfony/lock",
+ "version": "v6.4.47",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/lock.git",
+ "reference": "94ea4a8415f40e5146d8559f74f632895505863b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/lock/zipball/94ea4a8415f40e5146d8559f74f632895505863b",
+ "reference": "94ea4a8415f40e5146d8559f74f632895505863b",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "psr/log": "^1|^2|^3",
+ "symfony/deprecation-contracts": "^2.5|^3"
+ },
+ "conflict": {
+ "doctrine/dbal": "<2.13.1",
+ "symfony/cache": "<6.2"
+ },
+ "require-dev": {
+ "doctrine/dbal": "^2.13.1|^3|^4",
+ "predis/predis": "^1.1|^2.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Lock\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jérémy Derussé",
+ "email": "jeremy@derusse.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Creates and manages locks, a mechanism to provide exclusive access to a shared resource",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "cas",
+ "flock",
+ "locking",
+ "mutex",
+ "redlock",
+ "semaphore"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/lock/tree/v6.4.47"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-09-18T06:15:40+00:00"
+ },
+ {
+ "name": "symfony/mailer",
+ "version": "v6.4.46",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/mailer.git",
+ "reference": "21cc391f6f6a20b8765125cabee0c6195b5b8f10"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/mailer/zipball/21cc391f6f6a20b8765125cabee0c6195b5b8f10",
+ "reference": "21cc391f6f6a20b8765125cabee0c6195b5b8f10",
+ "shasum": ""
+ },
+ "require": {
+ "egulias/email-validator": "^2.1.10|^3|^4",
+ "php": ">=8.1",
+ "psr/event-dispatcher": "^1",
+ "psr/log": "^1|^2|^3",
+ "symfony/event-dispatcher": "^5.4|^6.0|^7.0",
+ "symfony/mime": "^6.2|^7.0",
+ "symfony/service-contracts": "^2.5|^3"
+ },
+ "conflict": {
+ "symfony/http-client-contracts": "<2.5",
+ "symfony/http-kernel": "<5.4",
+ "symfony/messenger": "<6.2",
+ "symfony/mime": "<6.2",
+ "symfony/twig-bridge": "<6.2.1"
+ },
+ "require-dev": {
+ "symfony/console": "^5.4|^6.0|^7.0",
+ "symfony/http-client": "^5.4|^6.0|^7.0",
+ "symfony/messenger": "^6.2|^7.0",
+ "symfony/twig-bridge": "^6.2|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Mailer\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Helps sending emails",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/mailer/tree/v6.4.46"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-09-14T23:26:17+00:00"
+ },
+ {
+ "name": "symfony/messenger",
+ "version": "v6.4.47",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/messenger.git",
+ "reference": "19a98a82b4452f820034fb88815dac3783975d17"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/messenger/zipball/19a98a82b4452f820034fb88815dac3783975d17",
+ "reference": "19a98a82b4452f820034fb88815dac3783975d17",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "psr/log": "^1|^2|^3",
+ "symfony/clock": "^6.3|^7.0",
+ "symfony/deprecation-contracts": "^2.5|^3"
+ },
+ "conflict": {
+ "symfony/console": "<6.3",
+ "symfony/event-dispatcher": "<5.4",
+ "symfony/event-dispatcher-contracts": "<2.5",
+ "symfony/framework-bundle": "<5.4",
+ "symfony/http-kernel": "<5.4",
+ "symfony/serializer": "<5.4"
+ },
+ "require-dev": {
+ "psr/cache": "^1.0|^2.0|^3.0",
+ "symfony/console": "^6.3|^7.0",
+ "symfony/dependency-injection": "^5.4|^6.0|^7.0",
+ "symfony/event-dispatcher": "^5.4|^6.0|^7.0",
+ "symfony/http-kernel": "^5.4|^6.0|^7.0",
+ "symfony/process": "^5.4|^6.0|^7.0",
+ "symfony/property-access": "^5.4|^6.0|^7.0",
+ "symfony/rate-limiter": "^5.4|^6.0|^7.0",
+ "symfony/routing": "^5.4|^6.0|^7.0",
+ "symfony/serializer": "^5.4|^6.0|^7.0",
+ "symfony/service-contracts": "^2.5|^3",
+ "symfony/stopwatch": "^5.4|^6.0|^7.0",
+ "symfony/validator": "^5.4|^6.0|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Messenger\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Samuel Roze",
+ "email": "samuel.roze@gmail.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Helps applications send and receive messages to/from other applications or via message queues",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/messenger/tree/v6.4.47"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-09-21T15:42:22+00:00"
+ },
+ {
+ "name": "symfony/mime",
+ "version": "v6.4.46",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/mime.git",
+ "reference": "e700a9819829e1395ae95b508c6355053b5644fe"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/mime/zipball/e700a9819829e1395ae95b508c6355053b5644fe",
+ "reference": "e700a9819829e1395ae95b508c6355053b5644fe",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/polyfill-intl-idn": "^1.10",
+ "symfony/polyfill-mbstring": "^1.0"
+ },
+ "conflict": {
+ "egulias/email-validator": "~3.0.0",
+ "phpdocumentor/reflection-docblock": "<3.2.2",
+ "phpdocumentor/type-resolver": "<1.4.0",
+ "symfony/mailer": "<5.4",
+ "symfony/serializer": "<6.4.3|>7.0,<7.0.3"
+ },
+ "require-dev": {
+ "egulias/email-validator": "^2.1.10|^3.1|^4",
+ "league/html-to-markdown": "^5.0",
+ "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0",
+ "symfony/dependency-injection": "^5.4|^6.0|^7.0",
+ "symfony/process": "^5.4|^6.4|^7.0",
+ "symfony/property-access": "^5.4|^6.0|^7.0",
+ "symfony/property-info": "^5.4|^6.0|^7.0",
+ "symfony/serializer": "^6.4.44|^7.4.17"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Mime\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Allows manipulating MIME messages",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "mime",
+ "mime-type"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/mime/tree/v6.4.46"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-09-04T10:11:03+00:00"
+ },
+ {
+ "name": "symfony/monolog-bridge",
+ "version": "v6.4.45",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/monolog-bridge.git",
+ "reference": "d3f5dd9133dc5723c7ee77e37b3db07e6934576d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/monolog-bridge/zipball/d3f5dd9133dc5723c7ee77e37b3db07e6934576d",
+ "reference": "d3f5dd9133dc5723c7ee77e37b3db07e6934576d",
+ "shasum": ""
+ },
+ "require": {
+ "monolog/monolog": "^1.25.1|^2|^3",
+ "php": ">=8.1",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/http-kernel": "^5.4|^6.0|^7.0",
+ "symfony/service-contracts": "^2.5|^3"
+ },
+ "conflict": {
+ "symfony/console": "<5.4",
+ "symfony/http-foundation": "<5.4",
+ "symfony/security-core": "<5.4"
+ },
+ "require-dev": {
+ "symfony/console": "^5.4|^6.0|^7.0",
+ "symfony/http-client": "^5.4|^6.0|^7.0",
+ "symfony/mailer": "^5.4|^6.0|^7.0",
+ "symfony/messenger": "^5.4|^6.0|^7.0",
+ "symfony/mime": "^5.4|^6.0|^7.0",
+ "symfony/notifier": "^5.4|^6.0|^7.0",
+ "symfony/security-core": "^5.4|^6.0|^7.0",
+ "symfony/var-dumper": "^5.4|^6.0|^7.0"
+ },
+ "type": "symfony-bridge",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Bridge\\Monolog\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides integration for Monolog with various Symfony components",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/monolog-bridge/tree/v6.4.45"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-08-30T01:18:12+00:00"
+ },
+ {
+ "name": "symfony/monolog-bundle",
+ "version": "v3.11.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/monolog-bundle.git",
+ "reference": "d87468010570b2ec766152184918ee8d267c7411"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/monolog-bundle/zipball/d87468010570b2ec766152184918ee8d267c7411",
+ "reference": "d87468010570b2ec766152184918ee8d267c7411",
+ "shasum": ""
+ },
+ "require": {
+ "composer-runtime-api": "^2.0",
+ "monolog/monolog": "^1.25.1 || ^2.0 || ^3.0",
+ "php": ">=8.1",
+ "symfony/config": "^6.4 || ^7.0",
+ "symfony/dependency-injection": "^6.4 || ^7.0",
+ "symfony/deprecation-contracts": "^2.5 || ^3.0",
+ "symfony/http-kernel": "^6.4 || ^7.0",
+ "symfony/monolog-bridge": "^6.4 || ^7.0",
+ "symfony/polyfill-php84": "^1.30"
+ },
+ "require-dev": {
+ "symfony/console": "^6.4 || ^7.0",
+ "symfony/phpunit-bridge": "^7.3.3",
+ "symfony/yaml": "^6.4 || ^7.0"
+ },
+ "type": "symfony-bundle",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Bundle\\MonologBundle\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony MonologBundle",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "log",
+ "logging"
+ ],
+ "support": {
+ "issues": "https://github.com/symfony/monolog-bundle/issues",
+ "source": "https://github.com/symfony/monolog-bundle/tree/v3.11.2"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-04-02T18:23:01+00:00"
+ },
+ {
+ "name": "symfony/options-resolver",
+ "version": "v6.4.30",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/options-resolver.git",
+ "reference": "eeaa8cabe54c7b3516938c72a4a161c0cc80a34f"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/options-resolver/zipball/eeaa8cabe54c7b3516938c72a4a161c0cc80a34f",
+ "reference": "eeaa8cabe54c7b3516938c72a4a161c0cc80a34f",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/deprecation-contracts": "^2.5|^3"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\OptionsResolver\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides an improved replacement for the array_replace PHP function",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "config",
+ "configuration",
+ "options"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/options-resolver/tree/v6.4.30"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-11-12T13:06:53+00:00"
+ },
+ {
+ "name": "symfony/password-hasher",
+ "version": "v6.4.32",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/password-hasher.git",
+ "reference": "fbdfa5a2ca218ec8bb9029517426df2d780bdba9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/password-hasher/zipball/fbdfa5a2ca218ec8bb9029517426df2d780bdba9",
+ "reference": "fbdfa5a2ca218ec8bb9029517426df2d780bdba9",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "conflict": {
+ "symfony/security-core": "<5.4"
+ },
+ "require-dev": {
+ "symfony/console": "^5.4|^6.0|^7.0",
+ "symfony/security-core": "^5.4|^6.0|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\PasswordHasher\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Robin Chalas",
+ "email": "robin.chalas@gmail.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides password hashing utilities",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "hashing",
+ "password"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/password-hasher/tree/v6.4.32"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-01T21:24:53+00:00"
+ },
+ {
+ "name": "symfony/polyfill-ctype",
+ "version": "v1.37.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-ctype.git",
+ "reference": "141046a8f9477948ff284fa65be2095baafb94f2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2",
+ "reference": "141046a8f9477948ff284fa65be2095baafb94f2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "provide": {
+ "ext-ctype": "*"
+ },
+ "suggest": {
+ "ext-ctype": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Ctype\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Gert de Pagter",
+ "email": "BackEndTea@gmail.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for ctype functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "ctype",
+ "polyfill",
+ "portable"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-04-10T16:19:22+00:00"
+ },
+ {
+ "name": "symfony/polyfill-iconv",
+ "version": "v1.43.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-iconv.git",
+ "reference": "254724d95fe01b679cd97d88786e9623ee00bfb7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/254724d95fe01b679cd97d88786e9623ee00bfb7",
+ "reference": "254724d95fe01b679cd97d88786e9623ee00bfb7",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "provide": {
+ "ext-iconv": "*"
+ },
+ "suggest": {
+ "ext-iconv": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Iconv\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for the Iconv extension",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "iconv",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-iconv/tree/v1.43.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-09-29T06:25:14+00:00"
+ },
+ {
+ "name": "symfony/polyfill-intl-grapheme",
+ "version": "v1.43.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-intl-grapheme.git",
+ "reference": "5cac8f4db97208823aa353f59a01faad2da52c75"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/5cac8f4db97208823aa353f59a01faad2da52c75",
+ "reference": "5cac8f4db97208823aa353f59a01faad2da52c75",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "suggest": {
+ "ext-intl": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Intl\\Grapheme\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for intl's grapheme_* functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "grapheme",
+ "intl",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.43.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-09-25T15:46:47+00:00"
+ },
+ {
+ "name": "symfony/polyfill-intl-icu",
+ "version": "v1.43.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-intl-icu.git",
+ "reference": "d13cf82f40af144e5e5a867dc54ec3c7b4544b2e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-icu/zipball/d13cf82f40af144e5e5a867dc54ec3c7b4544b2e",
+ "reference": "d13cf82f40af144e5e5a867dc54ec3c7b4544b2e",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "suggest": {
+ "ext-intl": "For best performance and support of other locales than \"en\""
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Intl\\Icu\\": ""
+ },
+ "classmap": [
+ "Resources/stubs"
+ ],
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for intl's ICU-related data and classes",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "icu",
+ "intl",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-intl-icu/tree/v1.43.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-09-25T15:46:47+00:00"
+ },
+ {
+ "name": "symfony/polyfill-intl-idn",
+ "version": "v1.43.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-intl-idn.git",
+ "reference": "533d68cf99c8f8ee2e8893c59ed59192e92cfd74"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/533d68cf99c8f8ee2e8893c59ed59192e92cfd74",
+ "reference": "533d68cf99c8f8ee2e8893c59ed59192e92cfd74",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2",
+ "symfony/polyfill-intl-normalizer": "^1.10"
+ },
+ "suggest": {
+ "ext-intl": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Intl\\Idn\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Laurent Bassin",
+ "email": "laurent@bassin.info"
+ },
+ {
+ "name": "Trevor Rowbotham",
+ "email": "trevor.rowbotham@pm.me"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "idn",
+ "intl",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.43.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-09-23T14:40:04+00:00"
+ },
+ {
+ "name": "symfony/polyfill-intl-normalizer",
+ "version": "v1.43.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-intl-normalizer.git",
+ "reference": "ebd57ccc7edb93d632e184efbe7693d2517bab3b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/ebd57ccc7edb93d632e184efbe7693d2517bab3b",
+ "reference": "ebd57ccc7edb93d632e184efbe7693d2517bab3b",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "suggest": {
+ "ext-intl": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Intl\\Normalizer\\": ""
+ },
+ "classmap": [
+ "Resources/stubs"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for intl's Normalizer class and related functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "intl",
+ "normalizer",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.43.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-09-23T22:02:09+00:00"
+ },
+ {
+ "name": "symfony/polyfill-mbstring",
+ "version": "v1.43.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-mbstring.git",
+ "reference": "4d5d331bf731e0aedbc6b2f5e61135b7b7996a72"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/4d5d331bf731e0aedbc6b2f5e61135b7b7996a72",
+ "reference": "4d5d331bf731e0aedbc6b2f5e61135b7b7996a72",
+ "shasum": ""
+ },
+ "require": {
+ "ext-iconv": "*",
+ "php": ">=7.2"
+ },
+ "provide": {
+ "ext-mbstring": "*"
+ },
+ "suggest": {
+ "ext-mbstring": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Mbstring\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for the Mbstring extension",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "mbstring",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.43.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-09-24T15:19:04+00:00"
+ },
+ {
+ "name": "symfony/polyfill-php80",
+ "version": "v1.43.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-php80.git",
+ "reference": "9c6a5d6b01ca51d486c813e9a9c0ed55f208bb74"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/9c6a5d6b01ca51d486c813e9a9c0ed55f208bb74",
+ "reference": "9c6a5d6b01ca51d486c813e9a9c0ed55f208bb74",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Php80\\": ""
+ },
+ "classmap": [
+ "Resources/stubs"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Ion Bazan",
+ "email": "ion.bazan@gmail.com"
+ },
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-php80/tree/v1.43.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-09-25T15:50:05+00:00"
+ },
+ {
+ "name": "symfony/polyfill-php83",
+ "version": "v1.43.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-php83.git",
+ "reference": "80ccff923a8d61f73ebe9da0aa94e25232f004ce"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/80ccff923a8d61f73ebe9da0aa94e25232f004ce",
+ "reference": "80ccff923a8d61f73ebe9da0aa94e25232f004ce",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Php83\\": ""
+ },
+ "classmap": [
+ "Resources/stubs"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-php83/tree/v1.43.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-09-25T15:50:05+00:00"
+ },
+ {
+ "name": "symfony/polyfill-php84",
+ "version": "v1.43.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-php84.git",
+ "reference": "22c6548499cbe5c5bb072fc0c42312dff062a015"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/22c6548499cbe5c5bb072fc0c42312dff062a015",
+ "reference": "22c6548499cbe5c5bb072fc0c42312dff062a015",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Php84\\": ""
+ },
+ "classmap": [
+ "Resources/stubs"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-php84/tree/v1.43.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-09-23T22:01:09+00:00"
+ },
+ {
+ "name": "symfony/polyfill-uuid",
+ "version": "v1.43.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-uuid.git",
+ "reference": "b23ff8245cc5c4479ea2be3d0132d2e6dd433993"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/b23ff8245cc5c4479ea2be3d0132d2e6dd433993",
+ "reference": "b23ff8245cc5c4479ea2be3d0132d2e6dd433993",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "provide": {
+ "ext-uuid": "*"
+ },
+ "suggest": {
+ "ext-uuid": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Uuid\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Grégoire Pineau",
+ "email": "lyrixx@lyrixx.info"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for uuid functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "polyfill",
+ "portable",
+ "uuid"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-uuid/tree/v1.43.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-09-24T05:20:52+00:00"
+ },
+ {
+ "name": "symfony/process",
+ "version": "v6.4.46",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/process.git",
+ "reference": "b9f3d9ddf67543b92dbfa6d210c407d7a7982781"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/process/zipball/b9f3d9ddf67543b92dbfa6d210c407d7a7982781",
+ "reference": "b9f3d9ddf67543b92dbfa6d210c407d7a7982781",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Process\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Executes commands in sub-processes",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/process/tree/v6.4.46"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-09-02T12:35:55+00:00"
+ },
+ {
+ "name": "symfony/property-access",
+ "version": "v6.4.44",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/property-access.git",
+ "reference": "57793e8d1e3ede2d4a65604e8f213a3cfa8d1721"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/property-access/zipball/57793e8d1e3ede2d4a65604e8f213a3cfa8d1721",
+ "reference": "57793e8d1e3ede2d4a65604e8f213a3cfa8d1721",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/property-info": "^6.4.32|~7.3.10|^7.4.4"
+ },
+ "require-dev": {
+ "symfony/cache": "^5.4|^6.0|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\PropertyAccess\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides functions to read and write from/to an object or array using a simple string notation",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "access",
+ "array",
+ "extraction",
+ "index",
+ "injection",
+ "object",
+ "property",
+ "property-path",
+ "reflection"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/property-access/tree/v6.4.44"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-07-30T11:04:25+00:00"
+ },
+ {
+ "name": "symfony/property-info",
+ "version": "v6.4.46",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/property-info.git",
+ "reference": "4789c9a2650f9285da6db0363ef40e5a21b42ce9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/property-info/zipball/4789c9a2650f9285da6db0363ef40e5a21b42ce9",
+ "reference": "4789c9a2650f9285da6db0363ef40e5a21b42ce9",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/string": "^5.4|^6.0|^7.0"
+ },
+ "conflict": {
+ "doctrine/annotations": "<1.12",
+ "phpdocumentor/reflection-docblock": "<5.2|>=6",
+ "phpdocumentor/type-resolver": "<1.5.1",
+ "symfony/cache": "<5.4",
+ "symfony/dependency-injection": "<5.4|>=6.0,<6.4",
+ "symfony/serializer": "<5.4"
+ },
+ "require-dev": {
+ "doctrine/annotations": "^1.12|^2",
+ "phpdocumentor/reflection-docblock": "^5.2",
+ "phpstan/phpdoc-parser": "^1.0|^2.0",
+ "symfony/cache": "^5.4|^6.0|^7.0",
+ "symfony/dependency-injection": "^5.4|^6.0|^7.0",
+ "symfony/serializer": "^5.4|^6.4|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\PropertyInfo\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Kévin Dunglas",
+ "email": "dunglas@gmail.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Extracts information about PHP class' properties using metadata of popular sources",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "doctrine",
+ "phpdoc",
+ "property",
+ "symfony",
+ "type",
+ "validator"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/property-info/tree/v6.4.46"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-09-03T19:31:46+00:00"
+ },
+ {
+ "name": "symfony/proxy-manager-bridge",
+ "version": "v6.4.28",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/proxy-manager-bridge.git",
+ "reference": "9ecac7f98ad685d474394dbd06dab29bab4e18a6"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/proxy-manager-bridge/zipball/9ecac7f98ad685d474394dbd06dab29bab4e18a6",
+ "reference": "9ecac7f98ad685d474394dbd06dab29bab4e18a6",
+ "shasum": ""
+ },
+ "require": {
+ "friendsofphp/proxy-manager-lts": "^1.0.2",
+ "php": ">=8.1",
+ "symfony/dependency-injection": "^6.3|^7.0",
+ "symfony/deprecation-contracts": "^2.5|^3"
+ },
+ "require-dev": {
+ "symfony/config": "^6.1|^7.0"
+ },
+ "type": "symfony-bridge",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Bridge\\ProxyManager\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides integration for ProxyManager with various Symfony components",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/proxy-manager-bridge/tree/v6.4.28"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-11-02T18:11:54+00:00"
+ },
+ {
+ "name": "symfony/routing",
+ "version": "v6.4.47",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/routing.git",
+ "reference": "641a94df9ad73671abd86bc4303aa301ca555b2b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/routing/zipball/641a94df9ad73671abd86bc4303aa301ca555b2b",
+ "reference": "641a94df9ad73671abd86bc4303aa301ca555b2b",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/deprecation-contracts": "^2.5|^3"
+ },
+ "conflict": {
+ "doctrine/annotations": "<1.12",
+ "symfony/config": "<6.2",
+ "symfony/dependency-injection": "<5.4",
+ "symfony/yaml": "<5.4"
+ },
+ "require-dev": {
+ "doctrine/annotations": "^1.12|^2",
+ "psr/log": "^1|^2|^3",
+ "symfony/config": "^6.2|^7.0",
+ "symfony/dependency-injection": "^5.4|^6.0|^7.0",
+ "symfony/expression-language": "^5.4|^6.0|^7.0",
+ "symfony/http-foundation": "^5.4|^6.0|^7.0",
+ "symfony/yaml": "^5.4|^6.0|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Routing\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Maps an HTTP request to a set of configuration variables",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "router",
+ "routing",
+ "uri",
+ "url"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/routing/tree/v6.4.47"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-09-28T18:59:50+00:00"
+ },
+ {
+ "name": "symfony/security-bundle",
+ "version": "v6.4.47",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/security-bundle.git",
+ "reference": "f3ffaf6d5e6fad7f50c6ae5c021003ba6e374622"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/security-bundle/zipball/f3ffaf6d5e6fad7f50c6ae5c021003ba6e374622",
+ "reference": "f3ffaf6d5e6fad7f50c6ae5c021003ba6e374622",
+ "shasum": ""
+ },
+ "require": {
+ "composer-runtime-api": ">=2.1",
+ "ext-xml": "*",
+ "php": ">=8.1",
+ "symfony/clock": "^6.3|^7.0",
+ "symfony/config": "^6.1|^7.0",
+ "symfony/dependency-injection": "^6.4.11|^7.1.4",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/event-dispatcher": "^5.4|^6.0|^7.0",
+ "symfony/http-foundation": "^6.2|^7.0",
+ "symfony/http-kernel": "^6.2",
+ "symfony/password-hasher": "^5.4|^6.0|^7.0",
+ "symfony/security-core": "^6.2|^7.0",
+ "symfony/security-csrf": "^5.4|^6.0|^7.0",
+ "symfony/security-http": "^6.3.6|^7.0",
+ "symfony/service-contracts": "^2.5|^3"
+ },
+ "conflict": {
+ "symfony/browser-kit": "<5.4",
+ "symfony/console": "<5.4",
+ "symfony/framework-bundle": "<6.4",
+ "symfony/http-client": "<5.4",
+ "symfony/ldap": "<5.4",
+ "symfony/serializer": "<6.4",
+ "symfony/twig-bundle": "<5.4",
+ "symfony/validator": "<6.4"
+ },
+ "require-dev": {
+ "symfony/asset": "^5.4|^6.0|^7.0",
+ "symfony/browser-kit": "^5.4|^6.0|^7.0",
+ "symfony/console": "^5.4|^6.0|^7.0",
+ "symfony/css-selector": "^5.4|^6.0|^7.0",
+ "symfony/dom-crawler": "^5.4|^6.0|^7.0",
+ "symfony/expression-language": "^5.4|^6.0|^7.0",
+ "symfony/form": "^5.4|^6.0|^7.0",
+ "symfony/framework-bundle": "^6.4|^7.0",
+ "symfony/http-client": "^5.4|^6.0|^7.0",
+ "symfony/ldap": "^5.4|^6.0|^7.0",
+ "symfony/process": "^5.4|^6.0|^7.0",
+ "symfony/rate-limiter": "^5.4|^6.0|^7.0",
+ "symfony/serializer": "^6.4|^7.0",
+ "symfony/translation": "^5.4|^6.0|^7.0",
+ "symfony/twig-bridge": "^5.4|^6.0|^7.0",
+ "symfony/twig-bundle": "^5.4|^6.0|^7.0",
+ "symfony/validator": "^6.4|^7.0",
+ "symfony/yaml": "^5.4|^6.0|^7.0",
+ "twig/twig": "^2.13|^3.0.4|^4.0",
+ "web-token/jwt-checker": "^3.1",
+ "web-token/jwt-signature-algorithm-ecdsa": "^3.1",
+ "web-token/jwt-signature-algorithm-eddsa": "^3.1",
+ "web-token/jwt-signature-algorithm-hmac": "^3.1",
+ "web-token/jwt-signature-algorithm-none": "^3.1",
+ "web-token/jwt-signature-algorithm-rsa": "^3.1"
+ },
+ "type": "symfony-bundle",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Bundle\\SecurityBundle\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides a tight integration of the Security component into the Symfony full-stack framework",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/security-bundle/tree/v6.4.47"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-09-24T08:57:57+00:00"
+ },
+ {
+ "name": "symfony/security-core",
+ "version": "v6.4.47",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/security-core.git",
+ "reference": "8479fccf23be745d7a05beb0d31f989ea515529a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/security-core/zipball/8479fccf23be745d7a05beb0d31f989ea515529a",
+ "reference": "8479fccf23be745d7a05beb0d31f989ea515529a",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/event-dispatcher-contracts": "^2.5|^3",
+ "symfony/password-hasher": "^5.4|^6.0|^7.0",
+ "symfony/service-contracts": "^2.5|^3"
+ },
+ "conflict": {
+ "symfony/event-dispatcher": "<5.4",
+ "symfony/http-foundation": "<5.4",
+ "symfony/ldap": "<5.4",
+ "symfony/security-guard": "<5.4",
+ "symfony/translation": "<5.4.35|>=6.0,<6.3.12|>=6.4,<6.4.3|>=7.0,<7.0.3",
+ "symfony/validator": "<5.4"
+ },
+ "require-dev": {
+ "psr/cache": "^1.0|^2.0|^3.0",
+ "psr/container": "^1.1|^2.0",
+ "psr/log": "^1|^2|^3",
+ "symfony/cache": "^5.4|^6.0|^7.0",
+ "symfony/event-dispatcher": "^5.4|^6.0|^7.0",
+ "symfony/expression-language": "^5.4|^6.0|^7.0",
+ "symfony/http-foundation": "^5.4|^6.0|^7.0",
+ "symfony/ldap": "^5.4|^6.0|^7.0",
+ "symfony/property-access": "^5.4|^6.0|^7.0",
+ "symfony/string": "^5.4|^6.0|^7.0",
+ "symfony/translation": "^5.4.35|~6.3.12|^6.4.3|^7.0.3",
+ "symfony/validator": "^6.4|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Security\\Core\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony Security Component - Core Library",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/security-core/tree/v6.4.47"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-09-21T20:17:28+00:00"
+ },
+ {
+ "name": "symfony/security-csrf",
+ "version": "v6.4.31",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/security-csrf.git",
+ "reference": "52f62836fcb19cd351ef3a2aa9cf61a489e8990f"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/security-csrf/zipball/52f62836fcb19cd351ef3a2aa9cf61a489e8990f",
+ "reference": "52f62836fcb19cd351ef3a2aa9cf61a489e8990f",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/security-core": "^5.4|^6.0|^7.0"
+ },
+ "conflict": {
+ "symfony/http-foundation": "<5.4"
+ },
+ "require-dev": {
+ "symfony/http-foundation": "^5.4|^6.0|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Security\\Csrf\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony Security Component - CSRF Library",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/security-csrf/tree/v6.4.31"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-12-17T22:32:13+00:00"
+ },
+ {
+ "name": "symfony/security-http",
+ "version": "v6.4.47",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/security-http.git",
+ "reference": "44c7c1afb0537eedfe4670079eaf89ee179095b1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/security-http/zipball/44c7c1afb0537eedfe4670079eaf89ee179095b1",
+ "reference": "44c7c1afb0537eedfe4670079eaf89ee179095b1",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/http-foundation": "^6.2|^7.0",
+ "symfony/http-kernel": "^6.3|^7.0",
+ "symfony/polyfill-mbstring": "~1.0",
+ "symfony/property-access": "^5.4|^6.0|^7.0",
+ "symfony/security-core": "^6.4|^7.0",
+ "symfony/service-contracts": "^2.5|^3"
+ },
+ "conflict": {
+ "symfony/clock": "<6.3",
+ "symfony/event-dispatcher": "<5.4.9|>=6,<6.0.9",
+ "symfony/http-client-contracts": "<3.0",
+ "symfony/security-bundle": "<5.4",
+ "symfony/security-csrf": "<5.4"
+ },
+ "require-dev": {
+ "psr/log": "^1|^2|^3",
+ "symfony/cache": "^5.4|^6.0|^7.0",
+ "symfony/clock": "^6.3|^7.0",
+ "symfony/expression-language": "^5.4|^6.0|^7.0",
+ "symfony/http-client-contracts": "^3.0",
+ "symfony/rate-limiter": "^5.4|^6.0|^7.0",
+ "symfony/routing": "^5.4|^6.0|^7.0",
+ "symfony/security-csrf": "^5.4|^6.0|^7.0",
+ "symfony/translation": "^5.4|^6.0|^7.0",
+ "web-token/jwt-checker": "^3.1",
+ "web-token/jwt-signature-algorithm-ecdsa": "^3.1"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Security\\Http\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony Security Component - HTTP Integration",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/security-http/tree/v6.4.47"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-09-15T16:35:01+00:00"
+ },
+ {
+ "name": "symfony/serializer",
+ "version": "v6.4.47",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/serializer.git",
+ "reference": "67e177a3fe8cae15decfb004563f9b4988296be3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/serializer/zipball/67e177a3fe8cae15decfb004563f9b4988296be3",
+ "reference": "67e177a3fe8cae15decfb004563f9b4988296be3",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/polyfill-ctype": "~1.8"
+ },
+ "conflict": {
+ "doctrine/annotations": "<1.12",
+ "phpdocumentor/reflection-docblock": "<3.2.2",
+ "phpdocumentor/type-resolver": "<1.4.0",
+ "symfony/dependency-injection": "<5.4",
+ "symfony/property-access": "<5.4",
+ "symfony/property-info": "<6.4.43",
+ "symfony/uid": "<5.4",
+ "symfony/validator": "<6.4",
+ "symfony/yaml": "<5.4"
+ },
+ "require-dev": {
+ "doctrine/annotations": "^1.12|^2",
+ "phpdocumentor/reflection-docblock": "^3.2|^4.0|^5.0",
+ "seld/jsonlint": "^1.10",
+ "symfony/cache": "^5.4|^6.0|^7.0",
+ "symfony/config": "^5.4|^6.0|^7.0",
+ "symfony/console": "^5.4|^6.0|^7.0",
+ "symfony/dependency-injection": "^5.4|^6.0|^7.0",
+ "symfony/error-handler": "^5.4|^6.0|^7.0",
+ "symfony/filesystem": "^5.4|^6.0|^7.0",
+ "symfony/form": "^5.4|^6.0|^7.0",
+ "symfony/http-foundation": "^5.4|^6.0|^7.0",
+ "symfony/http-kernel": "^5.4|^6.0|^7.0",
+ "symfony/messenger": "^5.4|^6.0|^7.0",
+ "symfony/mime": "^5.4|^6.0|^7.0",
+ "symfony/property-access": "^5.4.26|^6.3|^7.0",
+ "symfony/property-info": "^6.4.43|^7.4.15",
+ "symfony/translation-contracts": "^2.5|^3",
+ "symfony/uid": "^5.4|^6.0|^7.0",
+ "symfony/validator": "^6.4|^7.0",
+ "symfony/var-dumper": "^5.4|^6.0|^7.0",
+ "symfony/var-exporter": "^5.4|^6.0|^7.0",
+ "symfony/yaml": "^5.4|^6.0|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Serializer\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/serializer/tree/v6.4.47"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-09-28T08:44:55+00:00"
+ },
+ {
+ "name": "symfony/service-contracts",
+ "version": "v3.7.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/service-contracts.git",
+ "reference": "15e6a07ec2a2c75ceb1b21dd98105ee8456d2257"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/service-contracts/zipball/15e6a07ec2a2c75ceb1b21dd98105ee8456d2257",
+ "reference": "15e6a07ec2a2c75ceb1b21dd98105ee8456d2257",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "psr/container": "^1.1|^2.0",
+ "symfony/deprecation-contracts": "^2.5|^3"
+ },
+ "conflict": {
+ "ext-psr": "<1.1|>=2"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/contracts",
+ "name": "symfony/contracts"
+ },
+ "branch-alias": {
+ "dev-main": "3.7-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Contracts\\Service\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Test/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Generic abstractions related to writing services",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "abstractions",
+ "contracts",
+ "decoupling",
+ "interfaces",
+ "interoperability",
+ "standards"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/service-contracts/tree/v3.7.3"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-07-27T15:39:01+00:00"
+ },
+ {
+ "name": "symfony/stimulus-bundle",
+ "version": "v2.36.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/stimulus-bundle.git",
+ "reference": "377a3d1ec5834631a7db53bd275276ff3c5b49df"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/stimulus-bundle/zipball/377a3d1ec5834631a7db53bd275276ff3c5b49df",
+ "reference": "377a3d1ec5834631a7db53bd275276ff3c5b49df",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/config": "^5.4|^6.0|^7.0|^8.0",
+ "symfony/dependency-injection": "^5.4|^6.0|^7.0|^8.0",
+ "symfony/deprecation-contracts": "^2.0|^3.0",
+ "symfony/finder": "^5.4|^6.0|^7.0|^8.0",
+ "symfony/http-kernel": "^5.4|^6.0|^7.0|^8.0",
+ "twig/twig": "^2.15.3|^3.8"
+ },
+ "require-dev": {
+ "symfony/asset-mapper": "^6.3|^7.0|^8.0",
+ "symfony/framework-bundle": "^5.4|^6.0|^7.0|^8.0",
+ "symfony/phpunit-bridge": "^5.4|^6.0|^7.0|^8.0",
+ "symfony/twig-bundle": "^5.4|^6.0|^7.0|^8.0",
+ "zenstruck/browser": "^1.4"
+ },
+ "type": "symfony-bundle",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\UX\\StimulusBundle\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Integration with your Symfony app & Stimulus!",
+ "keywords": [
+ "symfony-ux"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/stimulus-bundle/tree/v2.36.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-05-06T04:31:36+00:00"
+ },
+ {
+ "name": "symfony/stopwatch",
+ "version": "v6.4.24",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/stopwatch.git",
+ "reference": "b67e94e06a05d9572c2fa354483b3e13e3cb1898"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/stopwatch/zipball/b67e94e06a05d9572c2fa354483b3e13e3cb1898",
+ "reference": "b67e94e06a05d9572c2fa354483b3e13e3cb1898",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/service-contracts": "^2.5|^3"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Stopwatch\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides a way to profile code",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/stopwatch/tree/v6.4.24"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-07-10T08:14:14+00:00"
+ },
+ {
+ "name": "symfony/string",
+ "version": "v6.4.46",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/string.git",
+ "reference": "4513ab2b0ad2d7845a2f920e2f30b61633c9e606"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/string/zipball/4513ab2b0ad2d7845a2f920e2f30b61633c9e606",
+ "reference": "4513ab2b0ad2d7845a2f920e2f30b61633c9e606",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/polyfill-ctype": "~1.8",
+ "symfony/polyfill-intl-grapheme": "~1.0",
+ "symfony/polyfill-intl-normalizer": "~1.0",
+ "symfony/polyfill-mbstring": "~1.0"
+ },
+ "conflict": {
+ "symfony/translation-contracts": "<2.5"
+ },
+ "require-dev": {
+ "symfony/http-client": "^5.4|^6.0|^7.0",
+ "symfony/intl": "^6.2|^7.0",
+ "symfony/translation-contracts": "^2.5|^3.0",
+ "symfony/var-exporter": "^5.4|^6.0|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "Resources/functions.php"
+ ],
+ "psr-4": {
+ "Symfony\\Component\\String\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "grapheme",
+ "i18n",
+ "string",
+ "unicode",
+ "utf-8",
+ "utf8"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/string/tree/v6.4.46"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-09-11T09:03:57+00:00"
+ },
+ {
+ "name": "symfony/translation",
+ "version": "v6.4.44",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/translation.git",
+ "reference": "fa2235501e2cf6b1d38ab42954b248a5e93c8e89"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/translation/zipball/fa2235501e2cf6b1d38ab42954b248a5e93c8e89",
+ "reference": "fa2235501e2cf6b1d38ab42954b248a5e93c8e89",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/polyfill-mbstring": "~1.0",
+ "symfony/translation-contracts": "^2.5|^3.0"
+ },
+ "conflict": {
+ "symfony/config": "<5.4",
+ "symfony/console": "<5.4",
+ "symfony/dependency-injection": "<5.4",
+ "symfony/http-client-contracts": "<2.5",
+ "symfony/http-kernel": "<5.4",
+ "symfony/service-contracts": "<2.5",
+ "symfony/twig-bundle": "<5.4",
+ "symfony/yaml": "<5.4"
+ },
+ "provide": {
+ "symfony/translation-implementation": "2.3|3.0"
+ },
+ "require-dev": {
+ "nikic/php-parser": "^4.18|^5.0",
+ "psr/log": "^1|^2|^3",
+ "symfony/config": "^5.4|^6.0|^7.0",
+ "symfony/console": "^5.4|^6.0|^7.0",
+ "symfony/dependency-injection": "^5.4|^6.0|^7.0",
+ "symfony/finder": "^5.4|^6.0|^7.0",
+ "symfony/http-client-contracts": "^2.5|^3.0",
+ "symfony/http-kernel": "^5.4|^6.0|^7.0",
+ "symfony/intl": "^5.4|^6.0|^7.0",
+ "symfony/polyfill-intl-icu": "^1.21",
+ "symfony/routing": "^5.4|^6.0|^7.0",
+ "symfony/service-contracts": "^2.5|^3",
+ "symfony/yaml": "^5.4|^6.0|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "Resources/functions.php"
+ ],
+ "psr-4": {
+ "Symfony\\Component\\Translation\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides tools to internationalize your application",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/translation/tree/v6.4.44"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-08-20T19:22:13+00:00"
+ },
+ {
+ "name": "symfony/translation-contracts",
+ "version": "v3.7.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/translation-contracts.git",
+ "reference": "ccb206b98faccc511ebae8e5fad50f2dc0b30621"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/ccb206b98faccc511ebae8e5fad50f2dc0b30621",
+ "reference": "ccb206b98faccc511ebae8e5fad50f2dc0b30621",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/contracts",
+ "name": "symfony/contracts"
+ },
+ "branch-alias": {
+ "dev-main": "3.7-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Contracts\\Translation\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Test/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Generic abstractions related to translation",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "abstractions",
+ "contracts",
+ "decoupling",
+ "interfaces",
+ "interoperability",
+ "standards"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/translation-contracts/tree/v3.7.1"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-06-05T06:23:12+00:00"
+ },
+ {
+ "name": "symfony/twig-bridge",
+ "version": "v6.4.46",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/twig-bridge.git",
+ "reference": "5b38d19f327f32507f96c9316aef90e7ad4962ac"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/5b38d19f327f32507f96c9316aef90e7ad4962ac",
+ "reference": "5b38d19f327f32507f96c9316aef90e7ad4962ac",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/translation-contracts": "^2.5|^3",
+ "twig/twig": "^2.13|^3.0.4|^4.0"
+ },
+ "conflict": {
+ "phpdocumentor/reflection-docblock": "<3.2.2",
+ "phpdocumentor/type-resolver": "<1.4.0",
+ "symfony/console": "<5.4",
+ "symfony/form": "<6.4.32|>7,<7.3.10|>7.4,<7.4.4",
+ "symfony/http-foundation": "<5.4",
+ "symfony/http-kernel": "<6.4",
+ "symfony/mime": "<6.4.37|>=7.0,<7.4.9",
+ "symfony/serializer": "<6.4",
+ "symfony/translation": "<5.4",
+ "symfony/workflow": "<5.4"
+ },
+ "require-dev": {
+ "egulias/email-validator": "^2.1.10|^3|^4",
+ "league/html-to-markdown": "^5.0",
+ "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0",
+ "symfony/asset": "^5.4|^6.0|^7.0",
+ "symfony/asset-mapper": "^6.3|^7.0",
+ "symfony/console": "^5.4|^6.0|^7.0",
+ "symfony/dependency-injection": "^5.4|^6.0|^7.0",
+ "symfony/expression-language": "^5.4|^6.0|^7.0",
+ "symfony/finder": "^5.4|^6.0|^7.0",
+ "symfony/form": "^6.4.32|~7.3.10|^7.4.4",
+ "symfony/html-sanitizer": "^6.1|^7.0",
+ "symfony/http-foundation": "^5.4|^6.0|^7.0",
+ "symfony/http-kernel": "^6.4|^7.0",
+ "symfony/intl": "^5.4|^6.0|^7.0",
+ "symfony/mime": "^6.4.37|^7.4.9",
+ "symfony/polyfill-intl-icu": "~1.0",
+ "symfony/property-info": "^5.4|^6.0|^7.0",
+ "symfony/routing": "^5.4|^6.0|^7.0",
+ "symfony/security-acl": "^2.8|^3.0",
+ "symfony/security-core": "^5.4|^6.0|^7.0",
+ "symfony/security-csrf": "^5.4|^6.0|^7.0",
+ "symfony/security-http": "^5.4|^6.0|^7.0",
+ "symfony/serializer": "^6.4.44|^7.4.17",
+ "symfony/stopwatch": "^5.4|^6.0|^7.0",
+ "symfony/translation": "^6.1|^7.0",
+ "symfony/web-link": "^5.4|^6.0|^7.0",
+ "symfony/workflow": "^5.4|^6.0|^7.0",
+ "symfony/yaml": "^5.4|^6.0|^7.0",
+ "twig/cssinliner-extra": "^2.12|^3",
+ "twig/inky-extra": "^2.12|^3",
+ "twig/markdown-extra": "^2.12|^3"
+ },
+ "type": "symfony-bridge",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Bridge\\Twig\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides integration for Twig with various Symfony components",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/twig-bridge/tree/v6.4.46"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-09-14T10:25:06+00:00"
+ },
+ {
+ "name": "symfony/twig-bundle",
+ "version": "v6.4.46",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/twig-bundle.git",
+ "reference": "d5996b3226a441ff558751646aa7c0775b90e2ee"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/twig-bundle/zipball/d5996b3226a441ff558751646aa7c0775b90e2ee",
+ "reference": "d5996b3226a441ff558751646aa7c0775b90e2ee",
+ "shasum": ""
+ },
+ "require": {
+ "composer-runtime-api": ">=2.1",
+ "php": ">=8.1",
+ "symfony/config": "^6.1|^7.0",
+ "symfony/dependency-injection": "^6.1|^7.0",
+ "symfony/http-foundation": "^5.4|^6.0|^7.0",
+ "symfony/http-kernel": "^6.2",
+ "symfony/twig-bridge": "^6.4",
+ "twig/twig": "^2.13|^3.0.4|^4.0"
+ },
+ "conflict": {
+ "symfony/framework-bundle": "<5.4",
+ "symfony/translation": "<5.4"
+ },
+ "require-dev": {
+ "symfony/asset": "^5.4|^6.0|^7.0",
+ "symfony/expression-language": "^5.4|^6.0|^7.0",
+ "symfony/finder": "^5.4|^6.0|^7.0",
+ "symfony/form": "^5.4|^6.0|^7.0",
+ "symfony/framework-bundle": "^5.4|^6.0|^7.0",
+ "symfony/routing": "^5.4|^6.0|^7.0",
+ "symfony/stopwatch": "^5.4|^6.0|^7.0",
+ "symfony/translation": "^5.4|^6.0|^7.0",
+ "symfony/web-link": "^5.4|^6.0|^7.0",
+ "symfony/yaml": "^5.4|^6.0|^7.0"
+ },
+ "type": "symfony-bundle",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Bundle\\TwigBundle\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides a tight integration of Twig into the Symfony full-stack framework",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/twig-bundle/tree/v6.4.46"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-09-11T10:34:36+00:00"
+ },
+ {
+ "name": "symfony/type-info",
+ "version": "v7.4.20",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/type-info.git",
+ "reference": "bbaaf3366400f5c85f66800b955c77b46e8abad2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/type-info/zipball/bbaaf3366400f5c85f66800b955c77b46e8abad2",
+ "reference": "bbaaf3366400f5c85f66800b955c77b46e8abad2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2",
+ "psr/container": "^1.1|^2.0",
+ "symfony/deprecation-contracts": "^2.5|^3"
+ },
+ "conflict": {
+ "phpstan/phpdoc-parser": "<1.30"
+ },
+ "require-dev": {
+ "phpstan/phpdoc-parser": "^1.30|^2.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\TypeInfo\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Mathias Arlaud",
+ "email": "mathias.arlaud@gmail.com"
+ },
+ {
+ "name": "Baptiste LEDUC",
+ "email": "baptiste.leduc@gmail.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Extracts PHP types information.",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "PHPStan",
+ "phpdoc",
+ "symfony",
+ "type"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/type-info/tree/v7.4.20"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-09-28T12:19:37+00:00"
+ },
+ {
+ "name": "symfony/uid",
+ "version": "v6.4.47",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/uid.git",
+ "reference": "2266f787b76bbbc339f4fdb40111ca461782f282"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/uid/zipball/2266f787b76bbbc339f4fdb40111ca461782f282",
+ "reference": "2266f787b76bbbc339f4fdb40111ca461782f282",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/polyfill-uuid": "^1.15"
+ },
+ "require-dev": {
+ "symfony/console": "^5.4|^6.0|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Uid\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Grégoire Pineau",
+ "email": "lyrixx@lyrixx.info"
+ },
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides an object-oriented API to generate and represent UIDs",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "UID",
+ "ulid",
+ "uuid"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/uid/tree/v6.4.47"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-09-23T16:05:37+00:00"
+ },
+ {
+ "name": "symfony/ux-autocomplete",
+ "version": "v2.36.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/ux-autocomplete.git",
+ "reference": "832f66056959aba68faef3a6cabc8d8ff606b2e4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/ux-autocomplete/zipball/832f66056959aba68faef3a6cabc8d8ff606b2e4",
+ "reference": "832f66056959aba68faef3a6cabc8d8ff606b2e4",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/dependency-injection": "^6.3|^7.0|^8.0",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/http-foundation": "^6.3|^7.0|^8.0",
+ "symfony/http-kernel": "^6.3|^7.0|^8.0",
+ "symfony/property-access": "^6.3|^7.0|^8.0"
+ },
+ "conflict": {
+ "doctrine/orm": "2.9.0 || 2.9.1"
+ },
+ "require-dev": {
+ "doctrine/collections": "^1.6.8|^2.0",
+ "doctrine/doctrine-bundle": "^2.4.3|^3.0|^4.0",
+ "doctrine/orm": "^2.9.4|^3.0",
+ "fakerphp/faker": "^1.22",
+ "mtdowling/jmespath.php": "^2.6",
+ "symfony/form": "^6.3|^7.0|^8.0",
+ "symfony/framework-bundle": "^6.3|^7.0|^8.0",
+ "symfony/maker-bundle": "^1.40",
+ "symfony/options-resolver": "^6.3|^7.0|^8.0",
+ "symfony/phpunit-bridge": "^6.3|^7.0|^8.0",
+ "symfony/process": "^6.3|^7.0|^8.0",
+ "symfony/security-bundle": "^6.3|^7.0|^8.0",
+ "symfony/twig-bundle": "^6.3|^7.0|^8.0",
+ "symfony/uid": "^6.3|^7.0|^8.0",
+ "twig/twig": "^2.14.7|^3.0.4",
+ "zenstruck/browser": "^1.1",
+ "zenstruck/foundry": "^2.0"
+ },
+ "type": "symfony-bundle",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/ux",
+ "name": "symfony/ux"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\UX\\Autocomplete\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "JavaScript Autocomplete functionality for Symfony",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "symfony-ux"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/ux-autocomplete/tree/v2.36.2"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-07-08T13:49:33+00:00"
+ },
+ {
+ "name": "symfony/ux-icons",
+ "version": "v2.36.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/ux-icons.git",
+ "reference": "567f33ddffc25504788abc61977565381e424ada"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/ux-icons/zipball/567f33ddffc25504788abc61977565381e424ada",
+ "reference": "567f33ddffc25504788abc61977565381e424ada",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/framework-bundle": "^6.4|^7.0|^8.0",
+ "symfony/twig-bundle": "^6.4|^7.0|^8.0"
+ },
+ "conflict": {
+ "symfony/flex": "<1.13",
+ "symfony/ux-twig-component": "<2.21"
+ },
+ "require-dev": {
+ "psr/log": "^2|^3",
+ "symfony/asset-mapper": "^6.4|^7.0|^8.0",
+ "symfony/console": "^6.4|^7.0|^8.0",
+ "symfony/http-client": "^6.4|^7.0|^8.0",
+ "symfony/phpunit-bridge": "^6.3|^7.0|^8.0",
+ "symfony/ux-twig-component": "^2.14|^3.0",
+ "zenstruck/console-test": "^1.5"
+ },
+ "type": "symfony-bundle",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/ux",
+ "name": "symfony/ux"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\UX\\Icons\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Kevin Bond",
+ "email": "kevinbond@gmail.com"
+ },
+ {
+ "name": "Simon André",
+ "email": "smn.andre@gmail.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Renders local and remote SVG icons in your Twig templates.",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "icons",
+ "svg",
+ "symfony-ux",
+ "twig"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/ux-icons/tree/v2.36.1"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-06-16T05:43:46+00:00"
+ },
+ {
+ "name": "symfony/ux-live-component",
+ "version": "v2.36.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/ux-live-component.git",
+ "reference": "a3fb5f24ff430db6a78019294086c16f70c86192"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/ux-live-component/zipball/a3fb5f24ff430db6a78019294086c16f70c86192",
+ "reference": "a3fb5f24ff430db6a78019294086c16f70c86192",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/deprecation-contracts": "^2.5|^3.0",
+ "symfony/property-access": "^5.4.5|^6.0|^7.0|^8.0",
+ "symfony/property-info": "^5.4|^6.0|^7.0|^8.0",
+ "symfony/stimulus-bundle": "^2.9|^3.0",
+ "symfony/ux-twig-component": "^2.33.0|^3.0",
+ "twig/twig": "^3.10.3"
+ },
+ "conflict": {
+ "symfony/config": "<5.4.0",
+ "symfony/property-info": "~7.0.0",
+ "symfony/type-info": "<7.2"
+ },
+ "require-dev": {
+ "doctrine/annotations": "^1.0|^2.0",
+ "doctrine/collections": "^1.6.8|^2.0",
+ "doctrine/doctrine-bundle": "^2.4.3|^3.0|^4.0",
+ "doctrine/orm": "^2.9.4|^3.0",
+ "doctrine/persistence": "^2.5.2|^3.0|^4.0",
+ "phpdocumentor/reflection-docblock": "^5.6.2",
+ "symfony/config": "^6.3|^7.0|^8.0",
+ "symfony/dependency-injection": "^5.4|^6.0|^7.0|^8.0",
+ "symfony/expression-language": "^5.4|^6.0|^7.0|^8.0",
+ "symfony/form": "^5.4|^6.0|^7.0|^8.0",
+ "symfony/framework-bundle": "^5.4|^6.1|^7.0|^8.0",
+ "symfony/http-kernel": "^6.1|^7.0|^8.0",
+ "symfony/options-resolver": "^5.4|^6.0|^7.0|^8.0",
+ "symfony/phpunit-bridge": "^6.1|^7.0|^8.0",
+ "symfony/security-bundle": "^5.4|^6.0|^7.0|^8.0",
+ "symfony/serializer": "^5.4|^6.0|^7.0|^8.0",
+ "symfony/twig-bundle": "^5.4|^6.0|^7.0|^8.0",
+ "symfony/uid": "^5.4|^6.0|^7.0|^8.0",
+ "symfony/validator": "^5.4|^6.0|^7.0|^8.0",
+ "zenstruck/browser": "^1.2.0",
+ "zenstruck/foundry": "^2.0"
+ },
+ "type": "symfony-bundle",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/ux",
+ "name": "symfony/ux"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\UX\\LiveComponent\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Live components for Symfony",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "components",
+ "symfony-ux",
+ "twig"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/ux-live-component/tree/v2.36.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-05-29T06:59:30+00:00"
+ },
+ {
+ "name": "symfony/ux-twig-component",
+ "version": "v2.36.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/ux-twig-component.git",
+ "reference": "d64b068d8339e905cd48974bdd6e9ba54dc8f247"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/ux-twig-component/zipball/d64b068d8339e905cd48974bdd6e9ba54dc8f247",
+ "reference": "d64b068d8339e905cd48974bdd6e9ba54dc8f247",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/dependency-injection": "^5.4|^6.0|^7.0|^8.0",
+ "symfony/deprecation-contracts": "^2.2|^3.0",
+ "symfony/event-dispatcher": "^5.4|^6.0|^7.0|^8.0",
+ "symfony/property-access": "^5.4|^6.0|^7.0|^8.0",
+ "twig/twig": "^3.10.3"
+ },
+ "conflict": {
+ "symfony/config": "<5.4.0"
+ },
+ "require-dev": {
+ "symfony/console": "^5.4|^6.0|^7.0|^8.0",
+ "symfony/css-selector": "^5.4|^6.0|^7.0|^8.0",
+ "symfony/dom-crawler": "^5.4|^6.0|^7.0|^8.0",
+ "symfony/framework-bundle": "^5.4|^6.0|^7.0|^8.0",
+ "symfony/phpunit-bridge": "^6.0|^7.0|^8.0",
+ "symfony/stimulus-bundle": "^2.9.1|^3.0",
+ "symfony/twig-bundle": "^5.4|^6.0|^7.0|^8.0",
+ "symfony/webpack-encore-bundle": "^1.15|^2.3.0",
+ "twig/extra-bundle": "^3.10.3",
+ "twig/html-extra": "^3.10.3"
+ },
+ "type": "symfony-bundle",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/ux",
+ "name": "symfony/ux"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\UX\\TwigComponent\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Twig components for Symfony",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "components",
+ "symfony-ux",
+ "twig"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/ux-twig-component/tree/v2.36.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-04-03T05:13:59+00:00"
+ },
+ {
+ "name": "symfony/validator",
+ "version": "v6.4.47",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/validator.git",
+ "reference": "022729aef78cefd4ac8b745f557abd57dc910204"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/validator/zipball/022729aef78cefd4ac8b745f557abd57dc910204",
+ "reference": "022729aef78cefd4ac8b745f557abd57dc910204",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/polyfill-ctype": "~1.8",
+ "symfony/polyfill-mbstring": "~1.0",
+ "symfony/polyfill-php83": "^1.27",
+ "symfony/translation-contracts": "^2.5|^3"
+ },
+ "conflict": {
+ "doctrine/annotations": "<1.13",
+ "doctrine/lexer": "<1.1",
+ "symfony/dependency-injection": "<5.4",
+ "symfony/expression-language": "<5.4",
+ "symfony/http-kernel": "<5.4",
+ "symfony/intl": "<5.4",
+ "symfony/property-info": "<5.4",
+ "symfony/translation": "<5.4.35|>=6.0,<6.3.12|>=6.4,<6.4.3|>=7.0,<7.0.3",
+ "symfony/yaml": "<5.4"
+ },
+ "require-dev": {
+ "doctrine/annotations": "^1.13|^2",
+ "egulias/email-validator": "^2.1.10|^3|^4",
+ "symfony/cache": "^5.4|^6.0|^7.0",
+ "symfony/config": "^5.4|^6.0|^7.0",
+ "symfony/console": "^5.4|^6.0|^7.0",
+ "symfony/dependency-injection": "^5.4|^6.0|^7.0",
+ "symfony/expression-language": "^5.4|^6.0|^7.0",
+ "symfony/finder": "^5.4|^6.0|^7.0",
+ "symfony/http-client": "^5.4|^6.0|^7.0",
+ "symfony/http-foundation": "^5.4|^6.0|^7.0",
+ "symfony/http-kernel": "^5.4|^6.0|^7.0",
+ "symfony/intl": "^5.4|^6.0|^7.0",
+ "symfony/mime": "^5.4|^6.0|^7.0",
+ "symfony/property-access": "^5.4|^6.0|^7.0",
+ "symfony/property-info": "^5.4|^6.0|^7.0",
+ "symfony/translation": "^5.4.35|~6.3.12|^6.4.3|^7.0.3",
+ "symfony/yaml": "^5.4|^6.0|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Validator\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/",
+ "/Resources/bin/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides tools to validate values",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/validator/tree/v6.4.47"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-09-29T11:58:14+00:00"
+ },
+ {
+ "name": "symfony/var-dumper",
+ "version": "v6.4.45",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/var-dumper.git",
+ "reference": "86a9b2f1bd81c9780a809632238ab6ba10292166"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/var-dumper/zipball/86a9b2f1bd81c9780a809632238ab6ba10292166",
+ "reference": "86a9b2f1bd81c9780a809632238ab6ba10292166",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/polyfill-mbstring": "~1.0"
+ },
+ "conflict": {
+ "symfony/console": "<5.4"
+ },
+ "require-dev": {
+ "symfony/console": "^5.4|^6.0|^7.0",
+ "symfony/error-handler": "^6.3|^7.0",
+ "symfony/http-kernel": "^5.4|^6.0|^7.0",
+ "symfony/process": "^5.4|^6.0|^7.0",
+ "symfony/uid": "^5.4|^6.0|^7.0",
+ "twig/twig": "^2.13|^3.0.4|^4.0"
+ },
+ "bin": [
+ "Resources/bin/var-dump-server"
+ ],
+ "type": "library",
+ "autoload": {
+ "files": [
+ "Resources/functions/dump.php"
+ ],
+ "psr-4": {
+ "Symfony\\Component\\VarDumper\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides mechanisms for walking through any arbitrary PHP variable",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "debug",
+ "dump"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/var-dumper/tree/v6.4.45"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-08-30T20:10:39+00:00"
+ },
+ {
+ "name": "symfony/var-exporter",
+ "version": "v6.4.45",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/var-exporter.git",
+ "reference": "a4e458999ee554a667cd19227f0b97a751bd61e6"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/var-exporter/zipball/a4e458999ee554a667cd19227f0b97a751bd61e6",
+ "reference": "a4e458999ee554a667cd19227f0b97a751bd61e6",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/deprecation-contracts": "^2.5|^3"
+ },
+ "require-dev": {
+ "symfony/property-access": "^6.4|^7.0",
+ "symfony/serializer": "^6.4|^7.0",
+ "symfony/var-dumper": "^5.4|^6.0|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\VarExporter\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Allows exporting any serializable PHP data structure to plain PHP code",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "clone",
+ "construct",
+ "export",
+ "hydrate",
+ "instantiate",
+ "lazy-loading",
+ "proxy",
+ "serialize"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/var-exporter/tree/v6.4.45"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-08-23T09:39:49+00:00"
+ },
+ {
+ "name": "symfony/web-link",
+ "version": "v6.4.32",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/web-link.git",
+ "reference": "636d5e34cd5c4a2538b02ba48a3c02989bfdf06b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/web-link/zipball/636d5e34cd5c4a2538b02ba48a3c02989bfdf06b",
+ "reference": "636d5e34cd5c4a2538b02ba48a3c02989bfdf06b",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "psr/link": "^1.1|^2.0"
+ },
+ "conflict": {
+ "symfony/http-kernel": "<5.4"
+ },
+ "provide": {
+ "psr/link-implementation": "1.0|2.0"
+ },
+ "require-dev": {
+ "symfony/http-kernel": "^5.4|^6.0|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\WebLink\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Kévin Dunglas",
+ "email": "dunglas@gmail.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Manages links between resources",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "dns-prefetch",
+ "http",
+ "http2",
+ "link",
+ "performance",
+ "prefetch",
+ "preload",
+ "prerender",
+ "psr13",
+ "push"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/web-link/tree/v6.4.32"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-01T13:45:34+00:00"
+ },
+ {
+ "name": "symfony/webpack-encore-bundle",
+ "version": "2.4.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/webpack-encore-bundle.git",
+ "reference": "0cbc3485f127cd9f85e395f8177607ef00535634"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/webpack-encore-bundle/zipball/0cbc3485f127cd9f85e395f8177607ef00535634",
+ "reference": "0cbc3485f127cd9f85e395f8177607ef00535634",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1.0",
+ "symfony/asset": "^5.4 || ^6.2 || ^7.0 || ^8.0",
+ "symfony/config": "^5.4 || ^6.2 || ^7.0 || ^8.0",
+ "symfony/dependency-injection": "^5.4 || ^6.2 || ^7.0 || ^8.0",
+ "symfony/http-kernel": "^5.4 || ^6.2 || ^7.0 || ^8.0",
+ "symfony/service-contracts": "^1.1.9 || ^2.1.3 || ^3.0"
+ },
+ "require-dev": {
+ "symfony/framework-bundle": "^5.4 || ^6.2 || ^7.0 || ^8.0",
+ "symfony/http-client": "^5.4 || ^6.2 || ^7.0 || ^8.0",
+ "symfony/phpunit-bridge": "^5.4 || ^6.2 || ^7.0 || ^8.0",
+ "symfony/twig-bundle": "^5.4 || ^6.2 || ^7.0 || ^8.0",
+ "symfony/web-link": "^5.4 || ^6.2 || ^7.0 || ^8.0"
+ },
+ "type": "symfony-bundle",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/webpack-encore",
+ "name": "symfony/webpack-encore"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\WebpackEncoreBundle\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Integration of your Symfony app with Webpack Encore",
+ "support": {
+ "issues": "https://github.com/symfony/webpack-encore-bundle/issues",
+ "source": "https://github.com/symfony/webpack-encore-bundle/tree/2.4.2"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-09-17T12:45:02+00:00"
+ },
+ {
+ "name": "symfony/workflow",
+ "version": "v6.4.47",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/workflow.git",
+ "reference": "f44fcf2b5b712a9a9c734a2d076007e0afec28d4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/workflow/zipball/f44fcf2b5b712a9a9c734a2d076007e0afec28d4",
+ "reference": "f44fcf2b5b712a9a9c734a2d076007e0afec28d4",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/deprecation-contracts": "^2.5|^3"
+ },
+ "conflict": {
+ "symfony/event-dispatcher": "<5.4"
+ },
+ "require-dev": {
+ "psr/log": "^1|^2|^3",
+ "symfony/dependency-injection": "^5.4|^6.0|^7.0",
+ "symfony/error-handler": "^6.4|^7.0",
+ "symfony/event-dispatcher": "^5.4|^6.0|^7.0",
+ "symfony/expression-language": "^5.4|^6.0|^7.0",
+ "symfony/http-kernel": "^6.4|^7.0",
+ "symfony/security-core": "^5.4|^6.0|^7.0",
+ "symfony/stopwatch": "^5.4|^6.0|^7.0",
+ "symfony/validator": "^5.4|^6.0|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Workflow\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Grégoire Pineau",
+ "email": "lyrixx@lyrixx.info"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides tools for managing a workflow or finite state machine",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "petrinet",
+ "place",
+ "state",
+ "statemachine",
+ "transition",
+ "workflow"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/workflow/tree/v6.4.47"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-09-21T16:55:19+00:00"
+ },
+ {
+ "name": "symfony/yaml",
+ "version": "v6.4.47",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/yaml.git",
+ "reference": "928e3ef5d46756da90cc2f54c3bd4a21bc18ceff"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/yaml/zipball/928e3ef5d46756da90cc2f54c3bd4a21bc18ceff",
+ "reference": "928e3ef5d46756da90cc2f54c3bd4a21bc18ceff",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/polyfill-ctype": "^1.8"
+ },
+ "conflict": {
+ "symfony/console": "<5.4"
+ },
+ "require-dev": {
+ "symfony/console": "^5.4|^6.0|^7.0"
+ },
+ "bin": [
+ "Resources/bin/yaml-lint"
+ ],
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Yaml\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Loads and dumps YAML files",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/yaml/tree/v6.4.47"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-09-26T06:15:25+00:00"
+ },
+ {
+ "name": "symfonycasts/dynamic-forms",
+ "version": "v0.1.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/SymfonyCasts/dynamic-forms.git",
+ "reference": "4c86c48f18a707e451c4dfffe87f3710b2052be6"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/SymfonyCasts/dynamic-forms/zipball/4c86c48f18a707e451c4dfffe87f3710b2052be6",
+ "reference": "4c86c48f18a707e451c4dfffe87f3710b2052be6",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/form": "^5.4|^6.3|^7.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.6",
+ "symfony/framework-bundle": "^6.3|^7.0",
+ "symfony/options-resolver": "^5.4|^6.3|^7.0",
+ "symfony/phpunit-bridge": "^5.4.32|^6.3.9|^7.0",
+ "symfony/twig-bundle": "^5.4|^6.3|^7.0",
+ "twig/twig": "^2.15|^3.0",
+ "zenstruck/browser": "^1.4"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfonycasts\\DynamicForms\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Ryan Weaver",
+ "homepage": "https://symfonycasts.com"
+ }
+ ],
+ "description": "Add dynamic/dependent fields to Symfony forms",
+ "keywords": [
+ "Forms",
+ "symfony"
+ ],
+ "support": {
+ "issues": "https://github.com/SymfonyCasts/dynamic-forms/issues",
+ "source": "https://github.com/SymfonyCasts/dynamic-forms/tree/v0.1.3"
+ },
+ "time": "2024-10-22T16:59:02+00:00"
+ },
+ {
+ "name": "twig/extra-bundle",
+ "version": "v3.29.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/twigphp/twig-extra-bundle.git",
+ "reference": "aaa2993e19293a99240c4c61aa461d743b0dd569"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/twigphp/twig-extra-bundle/zipball/aaa2993e19293a99240c4c61aa461d743b0dd569",
+ "reference": "aaa2993e19293a99240c4c61aa461d743b0dd569",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1.0",
+ "symfony/framework-bundle": "^5.4|^6.4|^7.0|^8.0",
+ "symfony/twig-bundle": "^5.4|^6.4|^7.0|^8.0",
+ "twig/twig": "^3.2|^4.0"
+ },
+ "require-dev": {
+ "league/commonmark": "^2.7",
+ "symfony/phpunit-bridge": "^6.4|^7.0",
+ "twig/cache-extra": "^3.0",
+ "twig/cssinliner-extra": "^3.0",
+ "twig/html-extra": "^3.0",
+ "twig/inky-extra": "^3.0",
+ "twig/intl-extra": "^3.0",
+ "twig/markdown-extra": "^3.0",
+ "twig/string-extra": "^3.0"
+ },
+ "type": "symfony-bundle",
+ "autoload": {
+ "psr-4": {
+ "Twig\\Extra\\TwigExtraBundle\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com",
+ "homepage": "http://fabien.potencier.org",
+ "role": "Lead Developer"
+ }
+ ],
+ "description": "A Symfony bundle for extra Twig extensions",
+ "homepage": "https://twig.symfony.com",
+ "keywords": [
+ "bundle",
+ "extra",
+ "twig"
+ ],
+ "support": {
+ "source": "https://github.com/twigphp/twig-extra-bundle/tree/v3.29.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/twig/twig",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-09-11T08:59:50+00:00"
+ },
+ {
+ "name": "twig/intl-extra",
+ "version": "v3.30.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/twigphp/intl-extra.git",
+ "reference": "211c603b3f7d5a8f6c6543a02fe9f82c09b18025"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/twigphp/intl-extra/zipball/211c603b3f7d5a8f6c6543a02fe9f82c09b18025",
+ "reference": "211c603b3f7d5a8f6c6543a02fe9f82c09b18025",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1.0",
+ "symfony/intl": "^5.4|^6.4|^7.0|^8.0",
+ "twig/twig": "^3.13|^4.0"
+ },
+ "require-dev": {
+ "symfony/phpunit-bridge": "^6.4|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Twig\\Extra\\Intl\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com",
+ "homepage": "http://fabien.potencier.org",
+ "role": "Lead Developer"
+ }
+ ],
+ "description": "A Twig extension for Intl",
+ "homepage": "https://twig.symfony.com",
+ "keywords": [
+ "intl",
+ "twig"
+ ],
+ "support": {
+ "source": "https://github.com/twigphp/intl-extra/tree/v3.30.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/twig/twig",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-09-25T05:50:11+00:00"
+ },
+ {
+ "name": "twig/string-extra",
+ "version": "v3.24.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/twigphp/string-extra.git",
+ "reference": "6ec8f2e8ca9b2193221a02cb599dc92c36384368"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/twigphp/string-extra/zipball/6ec8f2e8ca9b2193221a02cb599dc92c36384368",
+ "reference": "6ec8f2e8ca9b2193221a02cb599dc92c36384368",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1.0",
+ "symfony/string": "^5.4|^6.4|^7.0|^8.0",
+ "symfony/translation-contracts": "^1.1|^2|^3",
+ "twig/twig": "^3.13|^4.0"
+ },
+ "require-dev": {
+ "symfony/phpunit-bridge": "^6.4|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Twig\\Extra\\String\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com",
+ "homepage": "http://fabien.potencier.org",
+ "role": "Lead Developer"
+ }
+ ],
+ "description": "A Twig extension for Symfony String",
+ "homepage": "https://twig.symfony.com",
+ "keywords": [
+ "html",
+ "string",
+ "twig",
+ "unicode"
+ ],
+ "support": {
+ "source": "https://github.com/twigphp/string-extra/tree/v3.24.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/twig/twig",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-12-02T14:45:16+00:00"
+ },
+ {
+ "name": "twig/twig",
+ "version": "v3.30.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/twigphp/Twig.git",
+ "reference": "8c737079b726af72ff8ef3c595be9f6a810ea1ef"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/twigphp/Twig/zipball/8c737079b726af72ff8ef3c595be9f6a810ea1ef",
+ "reference": "8c737079b726af72ff8ef3c595be9f6a810ea1ef",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1.0",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/polyfill-ctype": "^1.8",
+ "symfony/polyfill-mbstring": "^1.3"
+ },
+ "require-dev": {
+ "php-cs-fixer/shim": "^3.0@stable",
+ "phpstan/phpstan": "^2.0@stable",
+ "psr/container": "^1.0|^2.0",
+ "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/Resources/core.php",
+ "src/Resources/debug.php",
+ "src/Resources/escaper.php",
+ "src/Resources/string_loader.php"
+ ],
+ "psr-4": {
+ "Twig\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com",
+ "homepage": "http://fabien.potencier.org",
+ "role": "Lead Developer"
+ },
+ {
+ "name": "Twig Team",
+ "role": "Contributors"
+ },
+ {
+ "name": "Armin Ronacher",
+ "email": "armin.ronacher@active-4.com",
+ "role": "Project Founder"
+ }
+ ],
+ "description": "Twig, the flexible, fast, and secure template language for PHP",
+ "homepage": "https://twig.symfony.com",
+ "keywords": [
+ "templating"
+ ],
+ "support": {
+ "issues": "https://github.com/twigphp/Twig/issues",
+ "source": "https://github.com/twigphp/Twig/tree/v3.30.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/twig/twig",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-09-25T13:20:01+00:00"
+ },
+ {
+ "name": "webmozart/assert",
+ "version": "1.12.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/webmozarts/assert.git",
+ "reference": "9be6926d8b485f55b9229203f962b51ed377ba68"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/webmozarts/assert/zipball/9be6926d8b485f55b9229203f962b51ed377ba68",
+ "reference": "9be6926d8b485f55b9229203f962b51ed377ba68",
+ "shasum": ""
+ },
+ "require": {
+ "ext-ctype": "*",
+ "ext-date": "*",
+ "ext-filter": "*",
+ "php": "^7.2 || ^8.0"
+ },
+ "suggest": {
+ "ext-intl": "",
+ "ext-simplexml": "",
+ "ext-spl": ""
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.10-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Webmozart\\Assert\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@gmail.com"
+ }
+ ],
+ "description": "Assertions to validate method input/output with nice error messages.",
+ "keywords": [
+ "assert",
+ "check",
+ "validate"
+ ],
+ "support": {
+ "issues": "https://github.com/webmozarts/assert/issues",
+ "source": "https://github.com/webmozarts/assert/tree/1.12.1"
+ },
+ "time": "2025-10-29T15:56:20+00:00"
+ },
+ {
+ "name": "willdurand/negotiation",
+ "version": "3.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/willdurand/Negotiation.git",
+ "reference": "68e9ea0553ef6e2ee8db5c1d98829f111e623ec2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/willdurand/Negotiation/zipball/68e9ea0553ef6e2ee8db5c1d98829f111e623ec2",
+ "reference": "68e9ea0553ef6e2ee8db5c1d98829f111e623ec2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1.0"
+ },
+ "require-dev": {
+ "symfony/phpunit-bridge": "^5.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Negotiation\\": "src/Negotiation"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "William Durand",
+ "email": "will+git@drnd.me"
+ }
+ ],
+ "description": "Content Negotiation tools for PHP provided as a standalone library.",
+ "homepage": "http://williamdurand.fr/Negotiation/",
+ "keywords": [
+ "accept",
+ "content",
+ "format",
+ "header",
+ "negotiation"
+ ],
+ "support": {
+ "issues": "https://github.com/willdurand/Negotiation/issues",
+ "source": "https://github.com/willdurand/Negotiation/tree/3.1.0"
+ },
+ "time": "2022-01-30T20:08:53+00:00"
+ }
+ ],
+ "packages-dev": [
+ {
+ "name": "amphp/amp",
+ "version": "v3.1.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/amphp/amp.git",
+ "reference": "73c38b323ff8d790abf0f76c56fcc892bda4111a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/amphp/amp/zipball/73c38b323ff8d790abf0f76c56fcc892bda4111a",
+ "reference": "73c38b323ff8d790abf0f76c56fcc892bda4111a",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "revolt/event-loop": "^1 || ^0.2"
+ },
+ "require-dev": {
+ "amphp/php-cs-fixer-config": "^2",
+ "phpunit/phpunit": "^9",
+ "psalm/phar": "6.16.1"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/functions.php",
+ "src/Future/functions.php",
+ "src/Internal/functions.php"
+ ],
+ "psr-4": {
+ "Amp\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Aaron Piotrowski",
+ "email": "aaron@trowski.com"
+ },
+ {
+ "name": "Bob Weinand",
+ "email": "bobwei9@hotmail.com"
+ },
+ {
+ "name": "Niklas Keller",
+ "email": "me@kelunik.com"
+ },
+ {
+ "name": "Daniel Lowrey",
+ "email": "rdlowrey@php.net"
+ }
+ ],
+ "description": "A non-blocking concurrency framework for PHP applications.",
+ "homepage": "https://amphp.org/amp",
+ "keywords": [
+ "async",
+ "asynchronous",
+ "awaitable",
+ "concurrency",
+ "event",
+ "event-loop",
+ "future",
+ "non-blocking",
+ "promise"
+ ],
+ "support": {
+ "issues": "https://github.com/amphp/amp/issues",
+ "source": "https://github.com/amphp/amp/tree/v3.1.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/amphp",
+ "type": "github"
+ }
+ ],
+ "time": "2026-07-19T17:59:20+00:00"
+ },
+ {
+ "name": "amphp/byte-stream",
+ "version": "v2.1.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/amphp/byte-stream.git",
+ "reference": "55a6bd071aec26fa2a3e002618c20c35e3df1b46"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/amphp/byte-stream/zipball/55a6bd071aec26fa2a3e002618c20c35e3df1b46",
+ "reference": "55a6bd071aec26fa2a3e002618c20c35e3df1b46",
+ "shasum": ""
+ },
+ "require": {
+ "amphp/amp": "^3",
+ "amphp/parser": "^1.1",
+ "amphp/pipeline": "^1",
+ "amphp/serialization": "^1",
+ "amphp/sync": "^2",
+ "php": ">=8.1",
+ "revolt/event-loop": "^1 || ^0.2.3"
+ },
+ "require-dev": {
+ "amphp/php-cs-fixer-config": "^2",
+ "amphp/phpunit-util": "^3",
+ "phpunit/phpunit": "^9",
+ "psalm/phar": "5.22.1"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/functions.php",
+ "src/Internal/functions.php"
+ ],
+ "psr-4": {
+ "Amp\\ByteStream\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Aaron Piotrowski",
+ "email": "aaron@trowski.com"
+ },
+ {
+ "name": "Niklas Keller",
+ "email": "me@kelunik.com"
+ }
+ ],
+ "description": "A stream abstraction to make working with non-blocking I/O simple.",
+ "homepage": "https://amphp.org/byte-stream",
+ "keywords": [
+ "amp",
+ "amphp",
+ "async",
+ "io",
+ "non-blocking",
+ "stream"
+ ],
+ "support": {
+ "issues": "https://github.com/amphp/byte-stream/issues",
+ "source": "https://github.com/amphp/byte-stream/tree/v2.1.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/amphp",
+ "type": "github"
+ }
+ ],
+ "time": "2025-03-16T17:10:27+00:00"
+ },
+ {
+ "name": "amphp/cache",
+ "version": "v2.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/amphp/cache.git",
+ "reference": "46912e387e6aa94933b61ea1ead9cf7540b7797c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/amphp/cache/zipball/46912e387e6aa94933b61ea1ead9cf7540b7797c",
+ "reference": "46912e387e6aa94933b61ea1ead9cf7540b7797c",
+ "shasum": ""
+ },
+ "require": {
+ "amphp/amp": "^3",
+ "amphp/serialization": "^1",
+ "amphp/sync": "^2",
+ "php": ">=8.1",
+ "revolt/event-loop": "^1 || ^0.2"
+ },
+ "require-dev": {
+ "amphp/php-cs-fixer-config": "^2",
+ "amphp/phpunit-util": "^3",
+ "phpunit/phpunit": "^9",
+ "psalm/phar": "^5.4"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Amp\\Cache\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Niklas Keller",
+ "email": "me@kelunik.com"
+ },
+ {
+ "name": "Aaron Piotrowski",
+ "email": "aaron@trowski.com"
+ },
+ {
+ "name": "Daniel Lowrey",
+ "email": "rdlowrey@php.net"
+ }
+ ],
+ "description": "A fiber-aware cache API based on Amp and Revolt.",
+ "homepage": "https://amphp.org/cache",
+ "support": {
+ "issues": "https://github.com/amphp/cache/issues",
+ "source": "https://github.com/amphp/cache/tree/v2.0.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/amphp",
+ "type": "github"
+ }
+ ],
+ "time": "2024-04-19T03:38:06+00:00"
+ },
+ {
+ "name": "amphp/dns",
+ "version": "v2.4.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/amphp/dns.git",
+ "reference": "ec5948bfafac808f410406e18bc7b52a2d4629b7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/amphp/dns/zipball/ec5948bfafac808f410406e18bc7b52a2d4629b7",
+ "reference": "ec5948bfafac808f410406e18bc7b52a2d4629b7",
+ "shasum": ""
+ },
+ "require": {
+ "amphp/amp": "^3",
+ "amphp/byte-stream": "^2",
+ "amphp/cache": "^2",
+ "amphp/parser": "^1",
+ "amphp/process": "^2",
+ "daverandom/libdns": "^2.0.2",
+ "ext-filter": "*",
+ "ext-json": "*",
+ "php": ">=8.1",
+ "revolt/event-loop": "^1 || ^0.2"
+ },
+ "require-dev": {
+ "amphp/php-cs-fixer-config": "^2",
+ "amphp/phpunit-util": "^3",
+ "phpunit/phpunit": "^9",
+ "psalm/phar": "6.16.1"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/functions.php"
+ ],
+ "psr-4": {
+ "Amp\\Dns\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Chris Wright",
+ "email": "addr@daverandom.com"
+ },
+ {
+ "name": "Daniel Lowrey",
+ "email": "rdlowrey@php.net"
+ },
+ {
+ "name": "Bob Weinand",
+ "email": "bobwei9@hotmail.com"
+ },
+ {
+ "name": "Niklas Keller",
+ "email": "me@kelunik.com"
+ },
+ {
+ "name": "Aaron Piotrowski",
+ "email": "aaron@trowski.com"
+ }
+ ],
+ "description": "Async DNS resolution for Amp.",
+ "homepage": "https://github.com/amphp/dns",
+ "keywords": [
+ "amp",
+ "amphp",
+ "async",
+ "client",
+ "dns",
+ "resolve"
+ ],
+ "support": {
+ "issues": "https://github.com/amphp/dns/issues",
+ "source": "https://github.com/amphp/dns/tree/v2.4.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/amphp",
+ "type": "github"
+ }
+ ],
+ "time": "2026-07-26T17:53:54+00:00"
+ },
+ {
+ "name": "amphp/parallel",
+ "version": "v2.4.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/amphp/parallel.git",
+ "reference": "37f5b2754fadc229c00f9416bd68fb8d04529a81"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/amphp/parallel/zipball/37f5b2754fadc229c00f9416bd68fb8d04529a81",
+ "reference": "37f5b2754fadc229c00f9416bd68fb8d04529a81",
+ "shasum": ""
+ },
+ "require": {
+ "amphp/amp": "^3",
+ "amphp/byte-stream": "^2",
+ "amphp/cache": "^2",
+ "amphp/parser": "^1",
+ "amphp/pipeline": "^1",
+ "amphp/process": "^2",
+ "amphp/serialization": "^1",
+ "amphp/socket": "^2",
+ "amphp/sync": "^2",
+ "php": ">=8.1",
+ "revolt/event-loop": "^1"
+ },
+ "require-dev": {
+ "amphp/php-cs-fixer-config": "^2",
+ "amphp/phpunit-util": "^3",
+ "phpunit/phpunit": "^9",
+ "psalm/phar": "6.16.1"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/Context/functions.php",
+ "src/Context/Internal/functions.php",
+ "src/Ipc/functions.php",
+ "src/Worker/functions.php"
+ ],
+ "psr-4": {
+ "Amp\\Parallel\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Aaron Piotrowski",
+ "email": "aaron@trowski.com"
+ },
+ {
+ "name": "Niklas Keller",
+ "email": "me@kelunik.com"
+ },
+ {
+ "name": "Stephen Coakley",
+ "email": "me@stephencoakley.com"
+ }
+ ],
+ "description": "Parallel processing component for Amp.",
+ "homepage": "https://github.com/amphp/parallel",
+ "keywords": [
+ "async",
+ "asynchronous",
+ "concurrent",
+ "multi-processing",
+ "multi-threading"
+ ],
+ "support": {
+ "issues": "https://github.com/amphp/parallel/issues",
+ "source": "https://github.com/amphp/parallel/tree/v2.4.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/amphp",
+ "type": "github"
+ }
+ ],
+ "time": "2026-05-16T16:54:01+00:00"
+ },
+ {
+ "name": "amphp/parser",
+ "version": "v1.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/amphp/parser.git",
+ "reference": "3cf1f8b32a0171d4b1bed93d25617637a77cded7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/amphp/parser/zipball/3cf1f8b32a0171d4b1bed93d25617637a77cded7",
+ "reference": "3cf1f8b32a0171d4b1bed93d25617637a77cded7",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.4"
+ },
+ "require-dev": {
+ "amphp/php-cs-fixer-config": "^2",
+ "phpunit/phpunit": "^9",
+ "psalm/phar": "^5.4"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Amp\\Parser\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Aaron Piotrowski",
+ "email": "aaron@trowski.com"
+ },
+ {
+ "name": "Niklas Keller",
+ "email": "me@kelunik.com"
+ }
+ ],
+ "description": "A generator parser to make streaming parsers simple.",
+ "homepage": "https://github.com/amphp/parser",
+ "keywords": [
+ "async",
+ "non-blocking",
+ "parser",
+ "stream"
+ ],
+ "support": {
+ "issues": "https://github.com/amphp/parser/issues",
+ "source": "https://github.com/amphp/parser/tree/v1.1.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/amphp",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-21T19:16:53+00:00"
+ },
+ {
+ "name": "amphp/pipeline",
+ "version": "v1.2.7",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/amphp/pipeline.git",
+ "reference": "cf2d67696c2015ea7c7fd8f6ec5f8ed7c2d17c17"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/amphp/pipeline/zipball/cf2d67696c2015ea7c7fd8f6ec5f8ed7c2d17c17",
+ "reference": "cf2d67696c2015ea7c7fd8f6ec5f8ed7c2d17c17",
+ "shasum": ""
+ },
+ "require": {
+ "amphp/amp": "^3",
+ "php": ">=8.1",
+ "revolt/event-loop": "^1"
+ },
+ "require-dev": {
+ "amphp/php-cs-fixer-config": "^2",
+ "amphp/phpunit-util": "^3",
+ "phpunit/phpunit": "^9",
+ "psalm/phar": "6.16.1"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Amp\\Pipeline\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Aaron Piotrowski",
+ "email": "aaron@trowski.com"
+ },
+ {
+ "name": "Niklas Keller",
+ "email": "me@kelunik.com"
+ }
+ ],
+ "description": "Asynchronous iterators and operators.",
+ "homepage": "https://amphp.org/pipeline",
+ "keywords": [
+ "amp",
+ "amphp",
+ "async",
+ "io",
+ "iterator",
+ "non-blocking"
+ ],
+ "support": {
+ "issues": "https://github.com/amphp/pipeline/issues",
+ "source": "https://github.com/amphp/pipeline/tree/v1.2.7"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/amphp",
+ "type": "github"
+ }
+ ],
+ "time": "2026-07-26T14:50:43+00:00"
+ },
+ {
+ "name": "amphp/process",
+ "version": "v2.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/amphp/process.git",
+ "reference": "583959df17d00304ad7b0b32285373f985935643"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/amphp/process/zipball/583959df17d00304ad7b0b32285373f985935643",
+ "reference": "583959df17d00304ad7b0b32285373f985935643",
+ "shasum": ""
+ },
+ "require": {
+ "amphp/amp": "^3",
+ "amphp/byte-stream": "^2",
+ "amphp/sync": "^2",
+ "php": ">=8.1",
+ "revolt/event-loop": "^1 || ^0.2"
+ },
+ "require-dev": {
+ "amphp/php-cs-fixer-config": "^2",
+ "amphp/phpunit-util": "^3",
+ "phpunit/phpunit": "^9",
+ "psalm/phar": "6.16.1"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/functions.php"
+ ],
+ "psr-4": {
+ "Amp\\Process\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Bob Weinand",
+ "email": "bobwei9@hotmail.com"
+ },
+ {
+ "name": "Aaron Piotrowski",
+ "email": "aaron@trowski.com"
+ },
+ {
+ "name": "Niklas Keller",
+ "email": "me@kelunik.com"
+ }
+ ],
+ "description": "A fiber-aware process manager based on Amp and Revolt.",
+ "homepage": "https://amphp.org/process",
+ "support": {
+ "issues": "https://github.com/amphp/process/issues",
+ "source": "https://github.com/amphp/process/tree/v2.1.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/amphp",
+ "type": "github"
+ }
+ ],
+ "time": "2026-05-31T15:11:55+00:00"
+ },
+ {
+ "name": "amphp/serialization",
+ "version": "v1.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/amphp/serialization.git",
+ "reference": "fdf2834d78cebb0205fb2672676c1b1eb84371f0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/amphp/serialization/zipball/fdf2834d78cebb0205fb2672676c1b1eb84371f0",
+ "reference": "fdf2834d78cebb0205fb2672676c1b1eb84371f0",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.4"
+ },
+ "require-dev": {
+ "amphp/php-cs-fixer-config": "^2",
+ "ext-json": "*",
+ "ext-zlib": "*",
+ "phpunit/phpunit": "^9",
+ "psalm/phar": "6.16.1"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/functions.php"
+ ],
+ "psr-4": {
+ "Amp\\Serialization\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Aaron Piotrowski",
+ "email": "aaron@trowski.com"
+ },
+ {
+ "name": "Niklas Keller",
+ "email": "me@kelunik.com"
+ }
+ ],
+ "description": "Serialization tools for IPC and data storage in PHP.",
+ "homepage": "https://github.com/amphp/serialization",
+ "keywords": [
+ "async",
+ "asynchronous",
+ "serialization",
+ "serialize"
+ ],
+ "support": {
+ "issues": "https://github.com/amphp/serialization/issues",
+ "source": "https://github.com/amphp/serialization/tree/v1.1.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/amphp",
+ "type": "github"
+ }
+ ],
+ "time": "2026-04-05T15:59:53+00:00"
+ },
+ {
+ "name": "amphp/socket",
+ "version": "v2.4.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/amphp/socket.git",
+ "reference": "b347be5aff6b2cc025208bb4d896607eb470c018"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/amphp/socket/zipball/b347be5aff6b2cc025208bb4d896607eb470c018",
+ "reference": "b347be5aff6b2cc025208bb4d896607eb470c018",
+ "shasum": ""
+ },
+ "require": {
+ "amphp/amp": "^3",
+ "amphp/byte-stream": "^2",
+ "amphp/dns": "^2",
+ "ext-openssl": "*",
+ "kelunik/certificate": "^1.1",
+ "league/uri": "^7",
+ "league/uri-interfaces": "^7",
+ "php": ">=8.1",
+ "revolt/event-loop": "^1"
+ },
+ "require-dev": {
+ "amphp/php-cs-fixer-config": "^2",
+ "amphp/phpunit-util": "^3",
+ "amphp/process": "^2",
+ "phpunit/phpunit": "^9",
+ "psalm/phar": "6.16.1"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/functions.php",
+ "src/Internal/functions.php",
+ "src/SocketAddress/functions.php"
+ ],
+ "psr-4": {
+ "Amp\\Socket\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Daniel Lowrey",
+ "email": "rdlowrey@gmail.com"
+ },
+ {
+ "name": "Aaron Piotrowski",
+ "email": "aaron@trowski.com"
+ },
+ {
+ "name": "Niklas Keller",
+ "email": "me@kelunik.com"
+ }
+ ],
+ "description": "Non-blocking socket connection / server implementations based on Amp and Revolt.",
+ "homepage": "https://github.com/amphp/socket",
+ "keywords": [
+ "amp",
+ "async",
+ "encryption",
+ "non-blocking",
+ "sockets",
+ "tcp",
+ "tls"
+ ],
+ "support": {
+ "issues": "https://github.com/amphp/socket/issues",
+ "source": "https://github.com/amphp/socket/tree/v2.4.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/amphp",
+ "type": "github"
+ }
+ ],
+ "time": "2026-08-22T18:13:20+00:00"
+ },
+ {
+ "name": "amphp/sync",
+ "version": "v2.3.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/amphp/sync.git",
+ "reference": "217097b785130d77cfcc58ff583cf26cd1770bf1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/amphp/sync/zipball/217097b785130d77cfcc58ff583cf26cd1770bf1",
+ "reference": "217097b785130d77cfcc58ff583cf26cd1770bf1",
+ "shasum": ""
+ },
+ "require": {
+ "amphp/amp": "^3",
+ "amphp/pipeline": "^1",
+ "amphp/serialization": "^1",
+ "php": ">=8.1",
+ "revolt/event-loop": "^1 || ^0.2"
+ },
+ "require-dev": {
+ "amphp/php-cs-fixer-config": "^2",
+ "amphp/phpunit-util": "^3",
+ "phpunit/phpunit": "^9",
+ "psalm/phar": "5.23"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/functions.php"
+ ],
+ "psr-4": {
+ "Amp\\Sync\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Aaron Piotrowski",
+ "email": "aaron@trowski.com"
+ },
+ {
+ "name": "Niklas Keller",
+ "email": "me@kelunik.com"
+ },
+ {
+ "name": "Stephen Coakley",
+ "email": "me@stephencoakley.com"
+ }
+ ],
+ "description": "Non-blocking synchronization primitives for PHP based on Amp and Revolt.",
+ "homepage": "https://github.com/amphp/sync",
+ "keywords": [
+ "async",
+ "asynchronous",
+ "mutex",
+ "semaphore",
+ "synchronization"
+ ],
+ "support": {
+ "issues": "https://github.com/amphp/sync/issues",
+ "source": "https://github.com/amphp/sync/tree/v2.3.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/amphp",
+ "type": "github"
+ }
+ ],
+ "time": "2024-08-03T19:31:26+00:00"
+ },
+ {
+ "name": "behat/behat",
+ "version": "v3.20.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Behat/Behat.git",
+ "reference": "edb265a32329d514e3a5ec44924646e0e02d95ee"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Behat/Behat/zipball/edb265a32329d514e3a5ec44924646e0e02d95ee",
+ "reference": "edb265a32329d514e3a5ec44924646e0e02d95ee",
+ "shasum": ""
+ },
+ "require": {
+ "behat/gherkin": "^4.12.0",
+ "behat/transliterator": "^1.5",
+ "composer-runtime-api": "^2.2",
+ "composer/xdebug-handler": "^3.0",
+ "ext-mbstring": "*",
+ "nikic/php-parser": "^5.0",
+ "php": "8.1.* || 8.2.* || 8.3.* || 8.4.* ",
+ "psr/container": "^1.0 || ^2.0",
+ "symfony/config": "^5.4 || ^6.4 || ^7.0",
+ "symfony/console": "^5.4 || ^6.4 || ^7.0",
+ "symfony/dependency-injection": "^5.4 || ^6.4 || ^7.0",
+ "symfony/event-dispatcher": "^5.4 || ^6.4 || ^7.0",
+ "symfony/translation": "^5.4 || ^6.4 || ^7.0",
+ "symfony/yaml": "^5.4 || ^6.4 || ^7.0"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "^3.68",
+ "phpstan/phpstan": "^2.0",
+ "phpunit/phpunit": "^9.6",
+ "sebastian/diff": "^4.0",
+ "symfony/polyfill-php84": "^1.31",
+ "symfony/process": "^5.4 || ^6.4 || ^7.0"
+ },
+ "suggest": {
+ "ext-dom": "Needed to output test results in JUnit format."
+ },
+ "bin": [
+ "bin/behat"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Behat\\Hook\\": "src/Behat/Hook/",
+ "Behat\\Step\\": "src/Behat/Step/",
+ "Behat\\Behat\\": "src/Behat/Behat/",
+ "Behat\\Config\\": "src/Behat/Config/",
+ "Behat\\Testwork\\": "src/Behat/Testwork/",
+ "Behat\\Transformation\\": "src/Behat/Transformation/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Konstantin Kudryashov",
+ "email": "ever.zet@gmail.com",
+ "homepage": "http://everzet.com"
+ }
+ ],
+ "description": "Scenario-oriented BDD framework for PHP",
+ "homepage": "https://behat.org/",
+ "keywords": [
+ "Agile",
+ "BDD",
+ "ScenarioBDD",
+ "Scrum",
+ "StoryBDD",
+ "User story",
+ "business",
+ "development",
+ "documentation",
+ "examples",
+ "symfony",
+ "testing"
+ ],
+ "support": {
+ "issues": "https://github.com/Behat/Behat/issues",
+ "source": "https://github.com/Behat/Behat/tree/v3.20.0"
+ },
+ "time": "2025-04-02T14:51:45+00:00"
+ },
+ {
+ "name": "behat/gherkin",
+ "version": "v4.18.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Behat/Gherkin.git",
+ "reference": "496248b29d9d7848311a263c2d17c1860e4c7b6c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Behat/Gherkin/zipball/496248b29d9d7848311a263c2d17c1860e4c7b6c",
+ "reference": "496248b29d9d7848311a263c2d17c1860e4c7b6c",
+ "shasum": ""
+ },
+ "require": {
+ "composer-runtime-api": "^2.2",
+ "php": ">=8.1 <8.6"
+ },
+ "require-dev": {
+ "cucumber/gherkin-monorepo": "dev-gherkin-v42.0.1",
+ "friendsofphp/php-cs-fixer": "^3.77",
+ "mikey179/vfsstream": "^1.6",
+ "phpstan/extension-installer": "^1",
+ "phpstan/phpstan": "2.2.14",
+ "phpstan/phpstan-deprecation-rules": "^2.0",
+ "phpstan/phpstan-phpunit": "^2",
+ "phpunit/phpunit": "^10.5",
+ "symfony/yaml": "^5.4 || ^6.4 || ^7.0"
+ },
+ "suggest": {
+ "symfony/yaml": "If you want to parse features, represented in YAML files"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Behat\\Gherkin\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Konstantin Kudryashov",
+ "email": "ever.zet@gmail.com",
+ "homepage": "https://everzet.com"
+ }
+ ],
+ "description": "Gherkin DSL parser for PHP",
+ "homepage": "https://behat.org/",
+ "keywords": [
+ "BDD",
+ "Behat",
+ "Cucumber",
+ "DSL",
+ "gherkin",
+ "parser"
+ ],
+ "support": {
+ "issues": "https://github.com/Behat/Gherkin/issues",
+ "source": "https://github.com/Behat/Gherkin/tree/v4.18.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/acoulton",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/carlos-granados",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/stof",
+ "type": "github"
+ }
+ ],
+ "time": "2026-09-25T21:26:10+00:00"
+ },
+ {
+ "name": "behat/mink-selenium2-driver",
+ "version": "v1.7.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/minkphp/MinkSelenium2Driver.git",
+ "reference": "4ca4083f305de7dff4434ac402dc4e3f39c0866a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/minkphp/MinkSelenium2Driver/zipball/4ca4083f305de7dff4434ac402dc4e3f39c0866a",
+ "reference": "4ca4083f305de7dff4434ac402dc4e3f39c0866a",
+ "shasum": ""
+ },
+ "require": {
+ "behat/mink": "^1.11@dev",
+ "ext-json": "*",
+ "instaclick/php-webdriver": "^1.4.14",
+ "php": ">=7.2"
+ },
+ "require-dev": {
+ "mink/driver-testsuite": "dev-master",
+ "phpstan/phpstan": "^1.10",
+ "phpstan/phpstan-phpunit": "^1.3",
+ "phpunit/phpunit": "^8.5.22 || ^9.5.11",
+ "symfony/error-handler": "^4.4 || ^5.0 || ^6.0 || ^7.0"
+ },
+ "type": "mink-driver",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Behat\\Mink\\Driver\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Pete Otaqui",
+ "email": "pete@otaqui.com",
+ "homepage": "https://github.com/pete-otaqui"
+ },
+ {
+ "name": "Konstantin Kudryashov",
+ "email": "ever.zet@gmail.com",
+ "homepage": "http://everzet.com"
+ }
+ ],
+ "description": "Selenium2 (WebDriver) driver for Mink framework",
+ "homepage": "https://mink.behat.org/",
+ "keywords": [
+ "ajax",
+ "browser",
+ "javascript",
+ "selenium",
+ "testing",
+ "webdriver"
+ ],
+ "support": {
+ "issues": "https://github.com/minkphp/MinkSelenium2Driver/issues",
+ "source": "https://github.com/minkphp/MinkSelenium2Driver/tree/v1.7.0"
+ },
+ "time": "2023-12-09T11:58:45+00:00"
+ },
+ {
+ "name": "composer/pcre",
+ "version": "3.3.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/pcre.git",
+ "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
+ "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4 || ^8.0"
+ },
+ "conflict": {
+ "phpstan/phpstan": "<1.11.10"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^1.12 || ^2",
+ "phpstan/phpstan-strict-rules": "^1 || ^2",
+ "phpunit/phpunit": "^8 || ^9"
+ },
+ "type": "library",
+ "extra": {
+ "phpstan": {
+ "includes": [
+ "extension.neon"
+ ]
+ },
+ "branch-alias": {
+ "dev-main": "3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Composer\\Pcre\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jordi Boggiano",
+ "email": "j.boggiano@seld.be",
+ "homepage": "http://seld.be"
+ }
+ ],
+ "description": "PCRE wrapping library that offers type-safe preg_* replacements.",
+ "keywords": [
+ "PCRE",
+ "preg",
+ "regex",
+ "regular expression"
+ ],
+ "support": {
+ "issues": "https://github.com/composer/pcre/issues",
+ "source": "https://github.com/composer/pcre/tree/3.3.2"
+ },
+ "funding": [
+ {
+ "url": "https://packagist.com",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/composer",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/composer/composer",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-11-12T16:29:46+00:00"
+ },
+ {
+ "name": "composer/xdebug-handler",
+ "version": "3.0.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/xdebug-handler.git",
+ "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef",
+ "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef",
+ "shasum": ""
+ },
+ "require": {
+ "composer/pcre": "^1 || ^2 || ^3",
+ "php": "^7.2.5 || ^8.0",
+ "psr/log": "^1 || ^2 || ^3"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^1.0",
+ "phpstan/phpstan-strict-rules": "^1.1",
+ "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Composer\\XdebugHandler\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "John Stevenson",
+ "email": "john-stevenson@blueyonder.co.uk"
+ }
+ ],
+ "description": "Restarts a process without Xdebug.",
+ "keywords": [
+ "Xdebug",
+ "performance"
+ ],
+ "support": {
+ "irc": "ircs://irc.libera.chat:6697/composer",
+ "issues": "https://github.com/composer/xdebug-handler/issues",
+ "source": "https://github.com/composer/xdebug-handler/tree/3.0.5"
+ },
+ "funding": [
+ {
+ "url": "https://packagist.com",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/composer",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/composer/composer",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-05-06T16:37:16+00:00"
+ },
+ {
+ "name": "daverandom/libdns",
+ "version": "v2.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/DaveRandom/LibDNS.git",
+ "reference": "b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/DaveRandom/LibDNS/zipball/b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a",
+ "reference": "b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a",
+ "shasum": ""
+ },
+ "require": {
+ "ext-ctype": "*",
+ "php": ">=7.1"
+ },
+ "suggest": {
+ "ext-intl": "Required for IDN support"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/functions.php"
+ ],
+ "psr-4": {
+ "LibDNS\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "DNS protocol implementation written in pure PHP",
+ "keywords": [
+ "dns"
+ ],
+ "support": {
+ "issues": "https://github.com/DaveRandom/LibDNS/issues",
+ "source": "https://github.com/DaveRandom/LibDNS/tree/v2.1.0"
+ },
+ "time": "2024-04-12T12:12:48+00:00"
+ },
+ {
+ "name": "dealerdirect/phpcodesniffer-composer-installer",
+ "version": "v1.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/PHPCSStandards/composer-installer.git",
+ "reference": "963f0c67bffde0eac41b56be71ac0e8ba132f0bd"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/963f0c67bffde0eac41b56be71ac0e8ba132f0bd",
+ "reference": "963f0c67bffde0eac41b56be71ac0e8ba132f0bd",
+ "shasum": ""
+ },
+ "require": {
+ "composer-plugin-api": "^2.2",
+ "php": ">=5.4",
+ "squizlabs/php_codesniffer": "^3.1.0 || ^4.0"
+ },
+ "require-dev": {
+ "composer/composer": "^2.2",
+ "ext-json": "*",
+ "ext-zip": "*",
+ "php-parallel-lint/php-parallel-lint": "^1.4.0",
+ "phpcompatibility/php-compatibility": "^9.0 || ^10.0.0@dev",
+ "yoast/phpunit-polyfills": "^1.0"
+ },
+ "type": "composer-plugin",
+ "extra": {
+ "class": "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin"
+ },
+ "autoload": {
+ "psr-4": {
+ "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Franck Nijhof",
+ "email": "opensource@frenck.dev",
+ "homepage": "https://frenck.dev",
+ "role": "Open source developer"
+ },
+ {
+ "name": "Contributors",
+ "homepage": "https://github.com/PHPCSStandards/composer-installer/graphs/contributors"
+ }
+ ],
+ "description": "PHP_CodeSniffer Standards Composer Installer Plugin",
+ "keywords": [
+ "PHPCodeSniffer",
+ "PHP_CodeSniffer",
+ "code quality",
+ "codesniffer",
+ "composer",
+ "installer",
+ "phpcbf",
+ "phpcs",
+ "plugin",
+ "qa",
+ "quality",
+ "standard",
+ "standards",
+ "style guide",
+ "stylecheck",
+ "tests"
+ ],
+ "support": {
+ "issues": "https://github.com/PHPCSStandards/composer-installer/issues",
+ "security": "https://github.com/PHPCSStandards/composer-installer/security/policy",
+ "source": "https://github.com/PHPCSStandards/composer-installer"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/PHPCSStandards",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/jrfnl",
+ "type": "github"
+ },
+ {
+ "url": "https://opencollective.com/php_codesniffer",
+ "type": "open_collective"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/phpcsstandards",
+ "type": "thanks_dev"
+ }
+ ],
+ "time": "2026-05-06T08:26:05+00:00"
+ },
+ {
+ "name": "dmore/behat-chrome-extension",
+ "version": "1.4.0",
+ "source": {
+ "type": "git",
+ "url": "https://gitlab.com/behat-chrome/behat-chrome-extension.git",
+ "reference": "888e91f52b3ffd19afe61cea3d5edebb0a4d43a7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://gitlab.com/api/v4/projects/behat-chrome%2Fbehat-chrome-extension/repository/archive.zip?sha=888e91f52b3ffd19afe61cea3d5edebb0a4d43a7",
+ "reference": "888e91f52b3ffd19afe61cea3d5edebb0a4d43a7",
+ "shasum": ""
+ },
+ "require": {
+ "behat/behat": "^3.0.4",
+ "dmore/chrome-mink-driver": "^2.4.1",
+ "friends-of-behat/mink-extension": "^2.0",
+ "php": ">=7.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^8.5 || ^9.5"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "DMore\\ChromeExtension\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Dorian More",
+ "email": "doriancmore@gmail.com"
+ }
+ ],
+ "description": "Behat extension for controlling Chrome without Selenium",
+ "homepage": "https://gitlab.com/behat-chrome/chrome-mink-driver",
+ "keywords": [
+ "Behat",
+ "chrome",
+ "driver",
+ "headless"
+ ],
+ "support": {
+ "issues": "https://gitlab.com/behat-chrome/behat-chrome-extension/-/issues",
+ "source": "https://gitlab.com/behat-chrome/behat-chrome-extension/-/tree/1.4.0"
+ },
+ "time": "2022-04-10T04:25:11+00:00"
+ },
+ {
+ "name": "dmore/chrome-mink-driver",
+ "version": "2.9.3",
+ "source": {
+ "type": "git",
+ "url": "https://gitlab.com/behat-chrome/chrome-mink-driver.git",
+ "reference": "4dc18d3b4668e749ab7bef5a6796c13711c93e61"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://gitlab.com/api/v4/projects/behat-chrome%2Fchrome-mink-driver/repository/archive.zip?sha=4dc18d3b4668e749ab7bef5a6796c13711c93e61",
+ "reference": "4dc18d3b4668e749ab7bef5a6796c13711c93e61",
+ "shasum": ""
+ },
+ "require": {
+ "behat/mink": "^1.9",
+ "ext-curl": "*",
+ "ext-json": "*",
+ "ext-mbstring": "*",
+ "phrity/websocket": "^1.7.0"
+ },
+ "require-dev": {
+ "mink/driver-testsuite": "dev-master",
+ "phpunit/phpunit": "^8.5.22 || ^9.5.11",
+ "squizlabs/php_codesniffer": "^3.5"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "DMore\\ChromeDriver\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Dorian More",
+ "email": "doriancmore@gmail.com"
+ }
+ ],
+ "description": "Mink driver for controlling chrome without selenium",
+ "homepage": "https://gitlab.com/behat-chrome/chrome-mink-driver",
+ "support": {
+ "issues": "https://gitlab.com/behat-chrome/chrome-mink-driver/-/issues",
+ "source": "https://gitlab.com/behat-chrome/chrome-mink-driver/-/tree/2.9.3"
+ },
+ "time": "2024-05-17T12:26:55+00:00"
+ },
+ {
+ "name": "friends-of-behat/mink",
+ "version": "v1.11.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/FriendsOfBehat/Mink.git",
+ "reference": "8aa0dc57999cb12736b80b379e22187d7f18e8a9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/FriendsOfBehat/Mink/zipball/8aa0dc57999cb12736b80b379e22187d7f18e8a9",
+ "reference": "8aa0dc57999cb12736b80b379e22187d7f18e8a9",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4|^8.0",
+ "symfony/css-selector": "^4.4|^5.0|^6.0|^7.0"
+ },
+ "replace": {
+ "behat/mink": "self.version"
+ },
+ "require-dev": {
+ "symfony/phpunit-bridge": "^5.2|^6.0|^7.0"
+ },
+ "suggest": {
+ "behat/mink-browserkit-driver": "extremely fast headless driver for Symfony\\Kernel-based apps (Sf2, Silex)",
+ "behat/mink-goutte-driver": "fast headless driver for any app without JS emulation",
+ "behat/mink-selenium2-driver": "slow, but JS-enabled driver for any app (requires Selenium2)",
+ "behat/mink-zombie-driver": "fast and JS-enabled headless driver for any app (requires node.js)",
+ "dmore/chrome-mink-driver": "fast and JS-enabled driver for any app (requires chromium or google chrome)"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.8.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Behat\\Mink\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Konstantin Kudryashov",
+ "email": "ever.zet@gmail.com",
+ "homepage": "http://everzet.com"
+ }
+ ],
+ "description": "Browser controller/emulator abstraction for PHP",
+ "homepage": "http://mink.behat.org/",
+ "keywords": [
+ "browser",
+ "testing",
+ "web"
+ ],
+ "support": {
+ "source": "https://github.com/FriendsOfBehat/Mink/tree/v1.11.0"
+ },
+ "abandoned": "behat/mink",
+ "time": "2024-02-06T13:17:10+00:00"
+ },
+ {
+ "name": "friends-of-behat/mink-browserkit-driver",
+ "version": "v1.6.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/FriendsOfBehat/MinkBrowserKitDriver.git",
+ "reference": "4f7d58037f8aa5f3aa17308cb6341b029859ea65"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/FriendsOfBehat/MinkBrowserKitDriver/zipball/4f7d58037f8aa5f3aa17308cb6341b029859ea65",
+ "reference": "4f7d58037f8aa5f3aa17308cb6341b029859ea65",
+ "shasum": ""
+ },
+ "require": {
+ "behat/mink": "^1.7",
+ "php": "^7.4|^8.0",
+ "symfony/browser-kit": "^4.4|^5.0|^6.0|^7.0",
+ "symfony/dom-crawler": "^4.4|^5.0|^6.0|^7.0"
+ },
+ "replace": {
+ "behat/mink-browserkit-driver": "self.version"
+ },
+ "require-dev": {
+ "friends-of-behat/mink-driver-testsuite": "dev-master",
+ "symfony/http-kernel": "^4.4|^5.0|^6.0|^7.0"
+ },
+ "type": "mink-driver",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Behat\\Mink\\Driver\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Konstantin Kudryashov",
+ "email": "ever.zet@gmail.com",
+ "homepage": "http://everzet.com"
+ }
+ ],
+ "description": "Symfony2 BrowserKit driver for Mink framework",
+ "homepage": "http://mink.behat.org/",
+ "keywords": [
+ "Mink",
+ "Symfony2",
+ "browser",
+ "testing"
+ ],
+ "support": {
+ "source": "https://github.com/FriendsOfBehat/MinkBrowserKitDriver/tree/v1.6.2"
+ },
+ "abandoned": "behat/mink-browserkit-driver",
+ "time": "2024-02-06T13:25:07+00:00"
+ },
+ {
+ "name": "friends-of-behat/mink-debug-extension",
+ "version": "v2.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/FriendsOfBehat/MinkDebugExtension.git",
+ "reference": "270e5aa5aef5358d81569a9a16eb2b3258314f9a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/FriendsOfBehat/MinkDebugExtension/zipball/270e5aa5aef5358d81569a9a16eb2b3258314f9a",
+ "reference": "270e5aa5aef5358d81569a9a16eb2b3258314f9a",
+ "shasum": ""
+ },
+ "require": {
+ "behat/behat": "^3.5",
+ "behat/mink-extension": "^2.3",
+ "php": ">=7.4"
+ },
+ "require-dev": {
+ "behat/mink-goutte-driver": "^1.2",
+ "behat/mink-selenium2-driver": "^1.4",
+ "dmore/behat-chrome-extension": "^1.3",
+ "dmore/chrome-mink-driver": "^2.7",
+ "symfony/process": "^4.4 || ^5.2"
+ },
+ "type": "behat-extension",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.1-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "FriendsOfBehat\\MinkDebugExtension\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Kamil Kokot",
+ "email": "kamil@kokot.me",
+ "homepage": "https://kamilkokot.com"
+ }
+ ],
+ "description": "Debug extension for Behat",
+ "homepage": "https://github.com/FriendsOfBehat/MinkDebugExtension",
+ "keywords": [
+ "Behat",
+ "Mink",
+ "debug",
+ "logging"
+ ],
+ "support": {
+ "issues": "https://github.com/FriendsOfBehat/MinkDebugExtension/issues",
+ "source": "https://github.com/FriendsOfBehat/MinkDebugExtension/tree/v2.1.0"
+ },
+ "time": "2021-12-13T08:52:43+00:00"
+ },
+ {
+ "name": "friends-of-behat/mink-extension",
+ "version": "v2.7.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/FriendsOfBehat/MinkExtension.git",
+ "reference": "854336030e11983f580f49faad1b49a1238f9846"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/FriendsOfBehat/MinkExtension/zipball/854336030e11983f580f49faad1b49a1238f9846",
+ "reference": "854336030e11983f580f49faad1b49a1238f9846",
+ "shasum": ""
+ },
+ "require": {
+ "behat/behat": "^3.0.5",
+ "behat/mink": "^1.5",
+ "php": ">=7.4",
+ "symfony/config": "^4.4 || ^5.0 || ^6.0 || ^7.0"
+ },
+ "replace": {
+ "behat/mink-extension": "self.version"
+ },
+ "require-dev": {
+ "behat/mink-goutte-driver": "^1.1 || ^2.0",
+ "phpspec/phpspec": "^6.0 || ^7.0 || 7.1.x-dev"
+ },
+ "type": "behat-extension",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-0": {
+ "Behat\\MinkExtension": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Konstantin Kudryashov",
+ "email": "ever.zet@gmail.com"
+ },
+ {
+ "name": "Christophe Coevoet",
+ "email": "stof@notk.org"
+ }
+ ],
+ "description": "Mink extension for Behat",
+ "homepage": "http://extensions.behat.org/mink",
+ "keywords": [
+ "browser",
+ "gui",
+ "test",
+ "web"
+ ],
+ "support": {
+ "issues": "https://github.com/FriendsOfBehat/MinkExtension/issues",
+ "source": "https://github.com/FriendsOfBehat/MinkExtension/tree/v2.7.5"
+ },
+ "time": "2024-01-11T09:12:02+00:00"
+ },
+ {
+ "name": "friends-of-behat/page-object-extension",
+ "version": "v0.3.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/FriendsOfBehat/PageObjectExtension.git",
+ "reference": "2e65b0bd7cca6ff2085b0fa885dc13dc57b18d4b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/FriendsOfBehat/PageObjectExtension/zipball/2e65b0bd7cca6ff2085b0fa885dc13dc57b18d4b",
+ "reference": "2e65b0bd7cca6ff2085b0fa885dc13dc57b18d4b",
+ "shasum": ""
+ },
+ "require": {
+ "behat/mink": "^1.7",
+ "php": "^7.1 || ^8.0"
+ },
+ "require-dev": {
+ "symfony/routing": "^3.4 || ^4.4 || ^5.1"
+ },
+ "suggest": {
+ "symfony/routing": "Allow better support for PageObject pattern in Symfony applications"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "0.3-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "FriendsOfBehat\\PageObjectExtension\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Łukasz Chruściel",
+ "email": "lchrusciel@gmail.com"
+ },
+ {
+ "name": "Kamil Kokot",
+ "email": "kamil@kokot.me",
+ "homepage": "http://kamil.kokot.me"
+ },
+ {
+ "name": "Mateusz Zalewski",
+ "email": "mateusz.p.zalewski@gmail.com",
+ "homepage": "http://mpzalewski.com.pl"
+ }
+ ],
+ "description": "Provides default classes for Page object pattern in Behat",
+ "support": {
+ "issues": "https://github.com/FriendsOfBehat/PageObjectExtension/issues",
+ "source": "https://github.com/FriendsOfBehat/PageObjectExtension/tree/v0.3.2"
+ },
+ "time": "2020-11-05T20:37:07+00:00"
+ },
+ {
+ "name": "friends-of-behat/suite-settings-extension",
+ "version": "v1.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/FriendsOfBehat/SuiteSettingsExtension.git",
+ "reference": "7a4e44e232622d20680582f305b33846f0617823"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/FriendsOfBehat/SuiteSettingsExtension/zipball/7a4e44e232622d20680582f305b33846f0617823",
+ "reference": "7a4e44e232622d20680582f305b33846f0617823",
+ "shasum": ""
+ },
+ "require": {
+ "behat/behat": "^3.8",
+ "php": "^7.4 || ^8.0"
+ },
+ "require-dev": {
+ "friends-of-behat/test-context": "^1.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "FriendsOfBehat\\SuiteSettingsExtension\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Kamil Kokot",
+ "email": "kamil@kokot.me",
+ "homepage": "https://kamilkokot.com"
+ }
+ ],
+ "description": "Allows to overwrite suites' default settings.",
+ "support": {
+ "issues": "https://github.com/FriendsOfBehat/SuiteSettingsExtension/issues",
+ "source": "https://github.com/FriendsOfBehat/SuiteSettingsExtension/tree/v1.1.0"
+ },
+ "time": "2021-02-05T14:35:02+00:00"
+ },
+ {
+ "name": "friends-of-behat/symfony-extension",
+ "version": "v2.6.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/FriendsOfBehat/SymfonyExtension.git",
+ "reference": "dfb1c9c96cc0fb7c8e1caa060695426a12e1efbd"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/FriendsOfBehat/SymfonyExtension/zipball/dfb1c9c96cc0fb7c8e1caa060695426a12e1efbd",
+ "reference": "dfb1c9c96cc0fb7c8e1caa060695426a12e1efbd",
+ "shasum": ""
+ },
+ "require": {
+ "behat/behat": "^3.6.1",
+ "php": "^8.1",
+ "symfony/dependency-injection": "^6.2 || ^7.0",
+ "symfony/http-kernel": "^6.2 || ^7.0"
+ },
+ "require-dev": {
+ "behat/mink": "^1.9",
+ "behat/mink-browserkit-driver": "^2.0",
+ "behat/mink-selenium2-driver": "^1.3",
+ "friends-of-behat/mink-extension": "^2.5",
+ "friends-of-behat/page-object-extension": "^0.3.2",
+ "friends-of-behat/service-container-extension": "^1.1",
+ "sylius-labs/coding-standard": ">=4.1.1, <=4.2.1",
+ "symfony/browser-kit": "^6.2 || ^7.0",
+ "symfony/framework-bundle": "^6.2 || ^7.0",
+ "symfony/process": "^6.2 || ^7.0",
+ "symfony/yaml": "^6.2 || ^7.0",
+ "vimeo/psalm": "4.30.0"
+ },
+ "suggest": {
+ "behat/mink": "^1.9",
+ "behat/mink-browserkit-driver": "^2.0",
+ "friends-of-behat/mink-extension": "^2.5"
+ },
+ "type": "symfony-bundle",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.2-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "FriendsOfBehat\\SymfonyExtension\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Kamil Kokot",
+ "email": "kamil@kokot.me",
+ "homepage": "https://kamilkokot.com"
+ }
+ ],
+ "description": "Integrates Behat with Symfony.",
+ "support": {
+ "issues": "https://github.com/FriendsOfBehat/SymfonyExtension/issues",
+ "source": "https://github.com/FriendsOfBehat/SymfonyExtension/tree/v2.6.0"
+ },
+ "time": "2024-07-03T15:49:43+00:00"
+ },
+ {
+ "name": "friends-of-behat/variadic-extension",
+ "version": "v1.6.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/FriendsOfBehat/VariadicExtension.git",
+ "reference": "892929189fd4deac9a920e64bb124231be624cdc"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/FriendsOfBehat/VariadicExtension/zipball/892929189fd4deac9a920e64bb124231be624cdc",
+ "reference": "892929189fd4deac9a920e64bb124231be624cdc",
+ "shasum": ""
+ },
+ "require": {
+ "behat/behat": "^3.8",
+ "php": "^8.1",
+ "symfony/dependency-injection": "^5.4 || ^6.4 || ^7.0"
+ },
+ "require-dev": {
+ "friends-of-behat/test-context": "^1.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.5-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "FriendsOfBehat\\VariadicExtension\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Łukasz Chruściel",
+ "email": "lchrusciel@gmail.com"
+ }
+ ],
+ "description": "Variadic support for behat context arguments",
+ "support": {
+ "issues": "https://github.com/FriendsOfBehat/VariadicExtension/issues",
+ "source": "https://github.com/FriendsOfBehat/VariadicExtension/tree/v1.6.0"
+ },
+ "time": "2024-01-30T11:13:11+00:00"
+ },
+ {
+ "name": "friendsoftwig/twigcs",
+ "version": "v6.5.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/friendsoftwig/twigcs.git",
+ "reference": "540fd6b80a4bcd1eb906b49464ce4513ee24fd5b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/friendsoftwig/twigcs/zipball/540fd6b80a4bcd1eb906b49464ce4513ee24fd5b",
+ "reference": "540fd6b80a4bcd1eb906b49464ce4513ee24fd5b",
+ "shasum": ""
+ },
+ "require": {
+ "ext-ctype": "*",
+ "ext-hash": "*",
+ "ext-json": "*",
+ "ext-mbstring": "*",
+ "ext-simplexml": "*",
+ "php": "~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
+ "symfony/console": "^4.4 || ^5.3 || ^6.0 || ^7.0",
+ "symfony/filesystem": "^4.4 || ^5.3 || ^6.0 || ^7.0",
+ "symfony/finder": "^4.4 || ^5.3 || ^6.0 || ^7.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.6.19",
+ "symfony/phpunit-bridge": "^7.2.0"
+ },
+ "bin": [
+ "bin/twigcs"
+ ],
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "FriendsOfTwig\\Twigcs\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Tristan Maindron",
+ "email": "tmaindron@gmail.com"
+ }
+ ],
+ "description": "Checkstyle automation for Twig",
+ "support": {
+ "issues": "https://github.com/friendsoftwig/twigcs/issues",
+ "source": "https://github.com/friendsoftwig/twigcs/tree/v6.5.0"
+ },
+ "time": "2025-11-28T15:45:09+00:00"
+ },
+ {
+ "name": "gitonomy/gitlib",
+ "version": "v1.6.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/gitonomy/gitlib.git",
+ "reference": "c1476cb0fd317fc512971e926b40ebcdb666e263"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/gitonomy/gitlib/zipball/c1476cb0fd317fc512971e926b40ebcdb666e263",
+ "reference": "c1476cb0fd317fc512971e926b40ebcdb666e263",
+ "shasum": ""
+ },
+ "require": {
+ "ext-pcre": "*",
+ "php": "^8.0",
+ "symfony/polyfill-mbstring": "^1.7",
+ "symfony/process": "^5.4 || ^6.0 || ^7.0 || ^8.0"
+ },
+ "require-dev": {
+ "ext-fileinfo": "*",
+ "phpspec/prophecy-phpunit": "^2.0",
+ "phpunit/phpunit": "^7.5.20 || ^8.5.20 || ^9.5.9",
+ "psr/log": "^1.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Gitonomy\\Git\\": "src/Gitonomy/Git/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Graham Campbell",
+ "email": "hello@gjcampbell.co.uk",
+ "homepage": "https://github.com/GrahamCampbell"
+ },
+ {
+ "name": "Julien Didier",
+ "email": "genzo.wm@gmail.com",
+ "homepage": "https://github.com/juliendidier"
+ },
+ {
+ "name": "Grégoire Pineau",
+ "email": "lyrixx@lyrixx.info",
+ "homepage": "https://github.com/lyrixx"
+ },
+ {
+ "name": "Alexandre Salomé",
+ "email": "alexandre.salome@gmail.com",
+ "homepage": "https://github.com/alexandresalome"
+ }
+ ],
+ "description": "Library for accessing git",
+ "support": {
+ "issues": "https://github.com/gitonomy/gitlib/issues",
+ "source": "https://github.com/gitonomy/gitlib/tree/v1.6.0"
+ },
+ "funding": [
+ {
+ "url": "https://tidelift.com/funding/github/packagist/gitonomy/gitlib",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-12-09T20:06:56+00:00"
+ },
+ {
+ "name": "hamcrest/hamcrest-php",
+ "version": "v2.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/hamcrest/hamcrest-php.git",
+ "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487",
+ "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4|^8.0"
+ },
+ "replace": {
+ "cordoval/hamcrest-php": "*",
+ "davedevelopment/hamcrest-php": "*",
+ "kodova/hamcrest-php": "*"
+ },
+ "require-dev": {
+ "phpunit/php-file-iterator": "^1.4 || ^2.0 || ^3.0",
+ "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0 || ^8.0 || ^9.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.1-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "hamcrest"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "description": "This is the PHP port of Hamcrest Matchers",
+ "keywords": [
+ "test"
+ ],
+ "support": {
+ "issues": "https://github.com/hamcrest/hamcrest-php/issues",
+ "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.1.1"
+ },
+ "time": "2025-04-30T06:54:44+00:00"
+ },
+ {
+ "name": "instaclick/php-webdriver",
+ "version": "1.4.20",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/instaclick/php-webdriver.git",
+ "reference": "981db0846ff4ac5be0301d609e36e2f023e74301"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/instaclick/php-webdriver/zipball/981db0846ff4ac5be0301d609e36e2f023e74301",
+ "reference": "981db0846ff4ac5be0301d609e36e2f023e74301",
+ "shasum": ""
+ },
+ "require": {
+ "ext-curl": "*",
+ "php": ">=5.3.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^8.5 || ^9.5",
+ "satooshi/php-coveralls": "^1.0 || ^2.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.4.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-0": {
+ "WebDriver": "lib/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "Apache-2.0"
+ ],
+ "authors": [
+ {
+ "name": "Justin Bishop",
+ "email": "jubishop@gmail.com",
+ "role": "Developer"
+ },
+ {
+ "name": "Anthon Pang",
+ "email": "apang@softwaredevelopment.ca",
+ "role": "Fork Maintainer"
+ }
+ ],
+ "description": "PHP WebDriver for Selenium 2",
+ "homepage": "http://instaclick.com/",
+ "keywords": [
+ "browser",
+ "selenium",
+ "webdriver",
+ "webtest"
+ ],
+ "support": {
+ "issues": "https://github.com/instaclick/php-webdriver/issues",
+ "source": "https://github.com/instaclick/php-webdriver/tree/1.4.20"
+ },
+ "time": "2025-12-04T11:20:11+00:00"
+ },
+ {
+ "name": "kelunik/certificate",
+ "version": "v1.1.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/kelunik/certificate.git",
+ "reference": "7e00d498c264d5eb4f78c69f41c8bd6719c0199e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/kelunik/certificate/zipball/7e00d498c264d5eb4f78c69f41c8bd6719c0199e",
+ "reference": "7e00d498c264d5eb4f78c69f41c8bd6719c0199e",
+ "shasum": ""
+ },
+ "require": {
+ "ext-openssl": "*",
+ "php": ">=7.0"
+ },
+ "require-dev": {
+ "amphp/php-cs-fixer-config": "^2",
+ "phpunit/phpunit": "^6 | 7 | ^8 | ^9"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Kelunik\\Certificate\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Niklas Keller",
+ "email": "me@kelunik.com"
+ }
+ ],
+ "description": "Access certificate details and transform between different formats.",
+ "keywords": [
+ "DER",
+ "certificate",
+ "certificates",
+ "openssl",
+ "pem",
+ "x509"
+ ],
+ "support": {
+ "issues": "https://github.com/kelunik/certificate/issues",
+ "source": "https://github.com/kelunik/certificate/tree/v1.1.3"
+ },
+ "time": "2023-02-03T21:26:53+00:00"
+ },
+ {
+ "name": "lakion/mink-debug-extension",
+ "version": "v2.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/FriendsOfBehat/MinkDebugExtension.git",
+ "reference": "46daa9bc10ff52ad3d875712cdd5bd047025b787"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/FriendsOfBehat/MinkDebugExtension/zipball/46daa9bc10ff52ad3d875712cdd5bd047025b787",
+ "reference": "46daa9bc10ff52ad3d875712cdd5bd047025b787",
+ "shasum": ""
+ },
+ "require": {
+ "behat/behat": "^3.5",
+ "behat/mink-extension": "^2.3",
+ "php": "^7.2 || ^8.0"
+ },
+ "require-dev": {
+ "behat/mink-goutte-driver": "^1.2",
+ "behat/mink-selenium2-driver": "^1.4",
+ "dmore/behat-chrome-extension": "^1.3",
+ "dmore/chrome-mink-driver": "^2.7",
+ "symfony/process": "^4.4 || ^5.2"
+ },
+ "type": "behat-extension",
+ "autoload": {
+ "psr-4": {
+ "FriendsOfBehat\\MinkDebugExtension\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Kamil Kokot",
+ "email": "kamil@kokot.me",
+ "homepage": "https://kamilkokot.com"
+ }
+ ],
+ "description": "Debug extension for Behat",
+ "homepage": "https://github.com/FriendsOfBehat/MinkDebugExtension",
+ "keywords": [
+ "Behat",
+ "Mink",
+ "debug",
+ "logging"
+ ],
+ "support": {
+ "issues": "https://github.com/FriendsOfBehat/MinkDebugExtension/issues",
+ "source": "https://github.com/FriendsOfBehat/MinkDebugExtension/tree/v2.0.0"
+ },
+ "abandoned": "friends-of-behat/mink-debug-extension",
+ "time": "2020-12-02T11:45:44+00:00"
+ },
+ {
+ "name": "laravel/serializable-closure",
+ "version": "v2.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/laravel/serializable-closure.git",
+ "reference": "2d5869a838bbcf37e0d8b0568fc41914e81374b5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/2d5869a838bbcf37e0d8b0568fc41914e81374b5",
+ "reference": "2d5869a838bbcf37e0d8b0568fc41914e81374b5",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.1"
+ },
+ "require-dev": {
+ "illuminate/support": "^10.0|^11.0|^12.0|^13.0",
+ "nesbot/carbon": "^2.67|^3.0",
+ "pestphp/pest": "^2.36|^3.0|^4.0",
+ "phpstan/phpstan": "^2.0",
+ "symfony/var-dumper": "^6.2.0|^7.0.0|^8.0.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Laravel\\SerializableClosure\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Taylor Otwell",
+ "email": "taylor@laravel.com"
+ },
+ {
+ "name": "Nuno Maduro",
+ "email": "nuno@laravel.com"
+ }
+ ],
+ "description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.",
+ "keywords": [
+ "closure",
+ "laravel",
+ "serializable"
+ ],
+ "support": {
+ "issues": "https://github.com/laravel/serializable-closure/issues",
+ "source": "https://github.com/laravel/serializable-closure"
+ },
+ "time": "2026-09-22T14:32:34+00:00"
+ },
+ {
+ "name": "masterminds/html5",
+ "version": "2.11.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Masterminds/html5-php.git",
+ "reference": "a1e7a2f88ee13635d86fc61cfbdf2306a76ddfc7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/a1e7a2f88ee13635d86fc61cfbdf2306a76ddfc7",
+ "reference": "a1e7a2f88ee13635d86fc61cfbdf2306a76ddfc7",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "php": ">=7.4"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^6 || ^7 || ^8 || ^9 || ^10"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.7-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Masterminds\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Matt Butcher",
+ "email": "technosophos@gmail.com"
+ },
+ {
+ "name": "Matt Farina",
+ "email": "matt@mattfarina.com"
+ },
+ {
+ "name": "Asmir Mustafic",
+ "email": "goetas@gmail.com"
+ }
+ ],
+ "description": "An HTML5 parser and serializer.",
+ "homepage": "http://masterminds.github.io/html5-php",
+ "keywords": [
+ "HTML5",
+ "dom",
+ "html",
+ "parser",
+ "querypath",
+ "serializer",
+ "xml"
+ ],
+ "support": {
+ "issues": "https://github.com/Masterminds/html5-php/issues",
+ "source": "https://github.com/Masterminds/html5-php/tree/2.11.0"
+ },
+ "time": "2026-08-18T06:18:41+00:00"
+ },
+ {
+ "name": "mockery/mockery",
+ "version": "1.6.12",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/mockery/mockery.git",
+ "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/mockery/mockery/zipball/1f4efdd7d3beafe9807b08156dfcb176d18f1699",
+ "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699",
+ "shasum": ""
+ },
+ "require": {
+ "hamcrest/hamcrest-php": "^2.0.1",
+ "lib-pcre": ">=7.0",
+ "php": ">=7.3"
+ },
+ "conflict": {
+ "phpunit/phpunit": "<8.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^8.5 || ^9.6.17",
+ "symplify/easy-coding-standard": "^12.1.14"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "library/helpers.php",
+ "library/Mockery.php"
+ ],
+ "psr-4": {
+ "Mockery\\": "library/Mockery"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Pádraic Brady",
+ "email": "padraic.brady@gmail.com",
+ "homepage": "https://github.com/padraic",
+ "role": "Author"
+ },
+ {
+ "name": "Dave Marshall",
+ "email": "dave.marshall@atstsolutions.co.uk",
+ "homepage": "https://davedevelopment.co.uk",
+ "role": "Developer"
+ },
+ {
+ "name": "Nathanael Esayeas",
+ "email": "nathanael.esayeas@protonmail.com",
+ "homepage": "https://github.com/ghostwriter",
+ "role": "Lead Developer"
+ }
+ ],
+ "description": "Mockery is a simple yet flexible PHP mock object framework",
+ "homepage": "https://github.com/mockery/mockery",
+ "keywords": [
+ "BDD",
+ "TDD",
+ "library",
+ "mock",
+ "mock objects",
+ "mockery",
+ "stub",
+ "test",
+ "test double",
+ "testing"
+ ],
+ "support": {
+ "docs": "https://docs.mockery.io/",
+ "issues": "https://github.com/mockery/mockery/issues",
+ "rss": "https://github.com/mockery/mockery/releases.atom",
+ "security": "https://github.com/mockery/mockery/security/advisories",
+ "source": "https://github.com/mockery/mockery"
+ },
+ "time": "2024-05-16T03:13:13+00:00"
+ },
+ {
+ "name": "myclabs/deep-copy",
+ "version": "1.14.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/myclabs/DeepCopy.git",
+ "reference": "8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae",
+ "reference": "8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.0"
+ },
+ "conflict": {
+ "doctrine/collections": "<1.6.8",
+ "doctrine/common": "<2.13.3 || >=3 <3.2.2"
+ },
+ "require-dev": {
+ "doctrine/collections": "^1.6.8",
+ "doctrine/common": "^2.13.3 || ^3.2.2",
+ "phpspec/prophecy": "^1.10",
+ "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/DeepCopy/deep_copy.php"
+ ],
+ "psr-4": {
+ "DeepCopy\\": "src/DeepCopy/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Create deep copies (clones) of your objects",
+ "keywords": [
+ "clone",
+ "copy",
+ "duplicate",
+ "object",
+ "object graph"
+ ],
+ "support": {
+ "issues": "https://github.com/myclabs/DeepCopy/issues",
+ "source": "https://github.com/myclabs/DeepCopy/tree/1.14.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/mnapoli",
+ "type": "github"
+ }
+ ],
+ "time": "2026-08-11T10:17:44+00:00"
+ },
+ {
+ "name": "nelmio/alice",
+ "version": "3.15.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/nelmio/alice.git",
+ "reference": "9cd00ce94b91be0f99fd969b7e8b591e5780fe53"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/nelmio/alice/zipball/9cd00ce94b91be0f99fd969b7e8b591e5780fe53",
+ "reference": "9cd00ce94b91be0f99fd969b7e8b591e5780fe53",
+ "shasum": ""
+ },
+ "require": {
+ "fakerphp/faker": "^1.10",
+ "myclabs/deep-copy": "^1.10",
+ "php": "^8.2",
+ "sebastian/comparator": "^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0",
+ "symfony/polyfill-php84": "^1.31",
+ "symfony/property-access": "^6.4 || ^7.4",
+ "symfony/yaml": "^6.0 || ^7.4"
+ },
+ "conflict": {
+ "symfony/framework-bundle": "<6.4.0 || >=7.0 <7.4.0"
+ },
+ "require-dev": {
+ "bamarni/composer-bin-plugin": "^1.8.1",
+ "phpspec/prophecy": "^1.6",
+ "phpspec/prophecy-phpunit": "^2.0",
+ "phpunit/phpunit": "^11",
+ "symfony/config": "^6.4 || ^7.4",
+ "symfony/dependency-injection": "^6.4 || ^7.4",
+ "symfony/finder": "^6.4 || ^7.4",
+ "symfony/http-kernel": "^6.4 || ^7.4",
+ "symfony/var-dumper": "^6.4 || ^7.4"
+ },
+ "suggest": {
+ "theofidry/alice-data-fixtures": "Wrapper for Alice to provide a persistence layer."
+ },
+ "type": "library",
+ "extra": {
+ "bamarni-bin": {
+ "bin-links": false,
+ "forward-command": false
+ },
+ "branch-alias": {
+ "dev-master": "3.x-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/deep_clone.php"
+ ],
+ "psr-4": {
+ "Nelmio\\Alice\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jordi Boggiano",
+ "email": "j.boggiano@seld.be"
+ },
+ {
+ "name": "Tim Shelburne",
+ "email": "shelburt02@gmail.com"
+ },
+ {
+ "name": "Théo FIDRY",
+ "email": "theo.fidry@gmail.com"
+ }
+ ],
+ "description": "Expressive fixtures generator",
+ "keywords": [
+ "Fixture",
+ "data",
+ "faker",
+ "test"
+ ],
+ "support": {
+ "issues": "https://github.com/nelmio/alice/issues",
+ "source": "https://github.com/nelmio/alice/tree/3.15.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/theofidry",
+ "type": "github"
+ }
+ ],
+ "time": "2025-12-09T11:29:11+00:00"
+ },
+ {
+ "name": "nikic/php-parser",
+ "version": "v5.9.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/nikic/PHP-Parser.git",
+ "reference": "9e33da9553fe7786f0962b35f4e4ecf01be89def"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/9e33da9553fe7786f0962b35f4e4ecf01be89def",
+ "reference": "9e33da9553fe7786f0962b35f4e4ecf01be89def",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "ext-tokenizer": "*",
+ "php": ">=7.4"
+ },
+ "require-dev": {
+ "ircmaxell/php-yacc": "^0.0.7",
+ "phpunit/phpunit": "^9.0"
+ },
+ "bin": [
+ "bin/php-parse"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "PhpParser\\": "lib/PhpParser"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Nikita Popov"
+ }
+ ],
+ "description": "A PHP parser written in PHP",
+ "keywords": [
+ "parser",
+ "php"
+ ],
+ "support": {
+ "issues": "https://github.com/nikic/PHP-Parser/issues",
+ "source": "https://github.com/nikic/PHP-Parser/tree/v5.9.0"
+ },
+ "time": "2026-09-13T18:51:52+00:00"
+ },
+ {
+ "name": "ondram/ci-detector",
+ "version": "4.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/OndraM/ci-detector.git",
+ "reference": "8b0223b5ed235fd377c75fdd1bfcad05c0f168b8"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/OndraM/ci-detector/zipball/8b0223b5ed235fd377c75fdd1bfcad05c0f168b8",
+ "reference": "8b0223b5ed235fd377c75fdd1bfcad05c0f168b8",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4 || ^8.0"
+ },
+ "require-dev": {
+ "ergebnis/composer-normalize": "^2.13.2",
+ "lmc/coding-standard": "^3.0.0",
+ "php-parallel-lint/php-parallel-lint": "^1.2",
+ "phpstan/extension-installer": "^1.1.0",
+ "phpstan/phpstan": "^1.2.0",
+ "phpstan/phpstan-phpunit": "^1.0.0",
+ "phpunit/phpunit": "^9.6.13"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "OndraM\\CiDetector\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Ondřej Machulda",
+ "email": "ondrej.machulda@gmail.com"
+ }
+ ],
+ "description": "Detect continuous integration environment and provide unified access to properties of current build",
+ "keywords": [
+ "CircleCI",
+ "Codeship",
+ "Wercker",
+ "adapter",
+ "appveyor",
+ "aws",
+ "aws codebuild",
+ "azure",
+ "azure devops",
+ "azure pipelines",
+ "bamboo",
+ "bitbucket",
+ "buddy",
+ "ci-info",
+ "codebuild",
+ "continuous integration",
+ "continuousphp",
+ "devops",
+ "drone",
+ "github",
+ "gitlab",
+ "interface",
+ "jenkins",
+ "pipelines",
+ "sourcehut",
+ "teamcity",
+ "travis"
+ ],
+ "support": {
+ "issues": "https://github.com/OndraM/ci-detector/issues",
+ "source": "https://github.com/OndraM/ci-detector/tree/4.2.0"
+ },
+ "time": "2024-03-12T13:22:30+00:00"
+ },
+ {
+ "name": "pdepend/pdepend",
+ "version": "2.16.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/pdepend/pdepend.git",
+ "reference": "f942b208dc2a0868454d01b29f0c75bbcfc6ed58"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/pdepend/pdepend/zipball/f942b208dc2a0868454d01b29f0c75bbcfc6ed58",
+ "reference": "f942b208dc2a0868454d01b29f0c75bbcfc6ed58",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.7",
+ "symfony/config": "^2.3.0|^3|^4|^5|^6.0|^7.0",
+ "symfony/dependency-injection": "^2.3.0|^3|^4|^5|^6.0|^7.0",
+ "symfony/filesystem": "^2.3.0|^3|^4|^5|^6.0|^7.0",
+ "symfony/polyfill-mbstring": "^1.19"
+ },
+ "require-dev": {
+ "easy-doc/easy-doc": "0.0.0|^1.2.3",
+ "gregwar/rst": "^1.0",
+ "squizlabs/php_codesniffer": "^2.0.0"
+ },
+ "bin": [
+ "src/bin/pdepend"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "PDepend\\": "src/main/php/PDepend"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "description": "Official version of pdepend to be handled with Composer",
+ "keywords": [
+ "PHP Depend",
+ "PHP_Depend",
+ "dev",
+ "pdepend"
+ ],
+ "support": {
+ "issues": "https://github.com/pdepend/pdepend/issues",
+ "source": "https://github.com/pdepend/pdepend/tree/2.16.2"
+ },
+ "funding": [
+ {
+ "url": "https://tidelift.com/funding/github/packagist/pdepend/pdepend",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2023-12-17T18:09:59+00:00"
+ },
+ {
+ "name": "phar-io/manifest",
+ "version": "2.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phar-io/manifest.git",
+ "reference": "54750ef60c58e43759730615a392c31c80e23176"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176",
+ "reference": "54750ef60c58e43759730615a392c31c80e23176",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-libxml": "*",
+ "ext-phar": "*",
+ "ext-xmlwriter": "*",
+ "phar-io/version": "^3.0.1",
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Heuer",
+ "email": "sebastian@phpeople.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)",
+ "support": {
+ "issues": "https://github.com/phar-io/manifest/issues",
+ "source": "https://github.com/phar-io/manifest/tree/2.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/theseer",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-03T12:33:53+00:00"
+ },
+ {
+ "name": "phar-io/version",
+ "version": "3.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phar-io/version.git",
+ "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74",
+ "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Heuer",
+ "email": "sebastian@phpeople.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "Library for handling version information and constraints",
+ "support": {
+ "issues": "https://github.com/phar-io/version/issues",
+ "source": "https://github.com/phar-io/version/tree/3.2.1"
+ },
+ "time": "2022-02-21T01:04:05+00:00"
+ },
+ {
+ "name": "php-parallel-lint/php-parallel-lint",
+ "version": "v1.4.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-parallel-lint/PHP-Parallel-Lint.git",
+ "reference": "6db563514f27e19595a19f45a4bf757b6401194e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-parallel-lint/PHP-Parallel-Lint/zipball/6db563514f27e19595a19f45a4bf757b6401194e",
+ "reference": "6db563514f27e19595a19f45a4bf757b6401194e",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "php": ">=5.3.0"
+ },
+ "replace": {
+ "grogy/php-parallel-lint": "*",
+ "jakub-onderka/php-parallel-lint": "*"
+ },
+ "require-dev": {
+ "nette/tester": "^1.3 || ^2.0",
+ "php-parallel-lint/php-console-highlighter": "0.* || ^1.0",
+ "squizlabs/php_codesniffer": "^3.6"
+ },
+ "suggest": {
+ "php-parallel-lint/php-console-highlighter": "Highlight syntax in code snippet"
+ },
+ "bin": [
+ "parallel-lint"
+ ],
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "./src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-2-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Jakub Onderka",
+ "email": "ahoj@jakubonderka.cz"
+ }
+ ],
+ "description": "This tool checks the syntax of PHP files about 20x faster than serial check.",
+ "homepage": "https://github.com/php-parallel-lint/PHP-Parallel-Lint",
+ "keywords": [
+ "lint",
+ "static analysis"
+ ],
+ "support": {
+ "issues": "https://github.com/php-parallel-lint/PHP-Parallel-Lint/issues",
+ "source": "https://github.com/php-parallel-lint/PHP-Parallel-Lint/tree/v1.4.0"
+ },
+ "time": "2024-03-27T12:14:49+00:00"
+ },
+ {
+ "name": "phpmd/phpmd",
+ "version": "2.15.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpmd/phpmd.git",
+ "reference": "74a1f56e33afad4128b886e334093e98e1b5e7c0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpmd/phpmd/zipball/74a1f56e33afad4128b886e334093e98e1b5e7c0",
+ "reference": "74a1f56e33afad4128b886e334093e98e1b5e7c0",
+ "shasum": ""
+ },
+ "require": {
+ "composer/xdebug-handler": "^1.0 || ^2.0 || ^3.0",
+ "ext-xml": "*",
+ "pdepend/pdepend": "^2.16.1",
+ "php": ">=5.3.9"
+ },
+ "require-dev": {
+ "easy-doc/easy-doc": "0.0.0 || ^1.3.2",
+ "ext-json": "*",
+ "ext-simplexml": "*",
+ "gregwar/rst": "^1.0",
+ "mikey179/vfsstream": "^1.6.8",
+ "squizlabs/php_codesniffer": "^2.9.2 || ^3.7.2"
+ },
+ "bin": [
+ "src/bin/phpmd"
+ ],
+ "type": "library",
+ "autoload": {
+ "psr-0": {
+ "PHPMD\\": "src/main/php"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Manuel Pichler",
+ "email": "github@manuel-pichler.de",
+ "homepage": "https://github.com/manuelpichler",
+ "role": "Project Founder"
+ },
+ {
+ "name": "Marc Würth",
+ "email": "ravage@bluewin.ch",
+ "homepage": "https://github.com/ravage84",
+ "role": "Project Maintainer"
+ },
+ {
+ "name": "Other contributors",
+ "homepage": "https://github.com/phpmd/phpmd/graphs/contributors",
+ "role": "Contributors"
+ }
+ ],
+ "description": "PHPMD is a spin-off project of PHP Depend and aims to be a PHP equivalent of the well known Java tool PMD.",
+ "homepage": "https://phpmd.org/",
+ "keywords": [
+ "dev",
+ "mess detection",
+ "mess detector",
+ "pdepend",
+ "phpmd",
+ "pmd"
+ ],
+ "support": {
+ "irc": "irc://irc.freenode.org/phpmd",
+ "issues": "https://github.com/phpmd/phpmd/issues",
+ "source": "https://github.com/phpmd/phpmd/tree/2.15.0"
+ },
+ "funding": [
+ {
+ "url": "https://tidelift.com/funding/github/packagist/phpmd/phpmd",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2023-12-11T08:22:20+00:00"
+ },
+ {
+ "name": "phpro/grumphp",
+ "version": "v2.25.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpro/grumphp.git",
+ "reference": "8af668884c838382e5114404204e62385ce3cdba"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpro/grumphp/zipball/8af668884c838382e5114404204e62385ce3cdba",
+ "reference": "8af668884c838382e5114404204e62385ce3cdba",
+ "shasum": ""
+ },
+ "require": {
+ "amphp/amp": "^3.0",
+ "amphp/parallel": "^2.1",
+ "composer-plugin-api": "^2.0",
+ "doctrine/collections": "^1.6.8 || ^2.0 || ^3.0",
+ "ext-json": "*",
+ "gitonomy/gitlib": "^1.6",
+ "laravel/serializable-closure": "^2.0",
+ "monolog/monolog": "^2.0 || ^3.0",
+ "ondram/ci-detector": "^4.0",
+ "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
+ "psr/container": "^1.1 || ^2.0",
+ "seld/jsonlint": "^1.8",
+ "symfony/config": "^6.4 || ^7.0 || ^8.0",
+ "symfony/console": "^6.4 || ^7.0 || ^8.0",
+ "symfony/dependency-injection": "^6.4 || ^7.0 || ^8.0",
+ "symfony/dotenv": "^6.4 || ^7.0 || ^8.0",
+ "symfony/event-dispatcher": "^6.4 || ^7.0 || ^8.0",
+ "symfony/expression-language": "^6.4 || ^7.0 || ^8.0",
+ "symfony/filesystem": "^6.4 || ^7.0 || ^8.0",
+ "symfony/finder": "^6.4 || ^7.0 || ^8.0",
+ "symfony/options-resolver": "^6.4 || ^7.0 || ^8.0",
+ "symfony/process": "^6.4 || ^7.0 || ^8.0",
+ "symfony/yaml": "^6.4 || ^7.0 || ^8.0"
+ },
+ "require-dev": {
+ "brianium/paratest": "^7.8.5",
+ "composer/composer": "^2.9.6",
+ "nikic/php-parser": "^5.7",
+ "php-cs-fixer/shim": "^3.93",
+ "php-parallel-lint/php-parallel-lint": "^1.4",
+ "phpspec/phpspec": "^8.2",
+ "phpspec/prophecy": "^1.24",
+ "phpspec/prophecy-phpunit": "^2.4",
+ "phpunit/phpunit": "^11.5.50"
+ },
+ "suggest": {
+ "atoum/atoum": "Lets GrumPHP run your unit tests.",
+ "behat/behat": "Lets GrumPHP validate your project features.",
+ "brianium/paratest": "Lets GrumPHP run PHPUnit in parallel.",
+ "carthage-software/mago": "Lets GrumPHP help you write better PHP code.",
+ "codeception/codeception": "Lets GrumPHP run your project's full stack tests",
+ "consolidation/robo": "Lets GrumPHP run your automated PHP tasks.",
+ "designsecurity/progpilot": "Lets GrumPHP be sure that there are no vulnerabilities in your code.",
+ "doctrine/orm": "Lets GrumPHP validate your Doctrine mapping files.",
+ "enlightn/security-checker": "Lets GrumPHP be sure that there are no known security issues.",
+ "ergebnis/composer-normalize": "Lets GrumPHP tidy and normalize your composer.json file.",
+ "friendsofphp/php-cs-fixer": "Lets GrumPHP automatically fix your codestyle.",
+ "friendsoftwig/twigcs": "Lets GrumPHP check Twig coding standard.",
+ "infection/infection": "Lets GrumPHP evaluate the quality your unit tests",
+ "maglnet/composer-require-checker": "Lets GrumPHP analyze composer dependencies.",
+ "malukenho/kawaii-gherkin": "Lets GrumPHP lint your Gherkin files.",
+ "nette/tester": "Lets GrumPHP run your unit tests with nette tester.",
+ "nikic/php-parser": "Lets GrumPHP run static analyses through your PHP files.",
+ "pestphp/pest": "Lets GrumPHP run your unit test with Pest PHP",
+ "phan/phan": "Lets GrumPHP unleash a static analyzer on your code",
+ "phing/phing": "Lets GrumPHP run your automated PHP tasks.",
+ "php-parallel-lint/php-parallel-lint": "Lets GrumPHP quickly lint your entire code base.",
+ "phparkitect/phparkitect": "Let GrumPHP keep your codebase coherent and solid, by permitting to add some architectural constraint check to your workflow.",
+ "phpmd/phpmd": "Lets GrumPHP sort out the mess in your code",
+ "phpspec/phpspec": "Lets GrumPHP spec your code.",
+ "phpstan/phpstan": "Lets GrumPHP discover bugs in your code without running it.",
+ "phpunit/phpunit": "Lets GrumPHP run your unit tests.",
+ "povils/phpmnd": "Lets GrumPHP help you detect magic numbers in PHP code.",
+ "rector/rector ": "Lets GrumPHP instantly upgrade and automatically refactor your PHP code.",
+ "roave/security-advisories": "Lets GrumPHP be sure that there are no known security issues.",
+ "sebastian/phpcpd": "Lets GrumPHP find duplicated code.",
+ "squizlabs/php_codesniffer": "Lets GrumPHP sniff on your code.",
+ "sstalle/php7cc": "Lets GrumPHP check PHP 5.3 - 5.6 code compatibility with PHP 7.",
+ "symfony/phpunit-bridge": "Lets GrumPHP run your unit tests with the phpunit-bridge of Symfony.",
+ "symplify/easy-coding-standard": "Lets GrumPHP check coding standard.",
+ "vimeo/psalm": "Lets GrumPHP discover errors in your code without running it.",
+ "vincentlanglet/twig-cs-fixer": "Lets GrumPHP check and fix twig coding standard."
+ },
+ "bin": [
+ "bin/grumphp"
+ ],
+ "type": "composer-plugin",
+ "extra": {
+ "class": "GrumPHP\\Composer\\GrumPHPPlugin"
+ },
+ "autoload": {
+ "psr-4": {
+ "GrumPHP\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Toon Verwerft",
+ "email": "toon.verwerft@phpro.be"
+ },
+ {
+ "name": "Community",
+ "homepage": "https://github.com/phpro/grumphp/graphs/contributors"
+ }
+ ],
+ "description": "A composer plugin that enables source code quality checks.",
+ "support": {
+ "issues": "https://github.com/phpro/grumphp/issues",
+ "source": "https://github.com/phpro/grumphp/tree/v2.25.0"
+ },
+ "time": "2026-10-03T08:58:23+00:00"
+ },
+ {
+ "name": "phpstan/extension-installer",
+ "version": "1.4.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpstan/extension-installer.git",
+ "reference": "85e90b3942d06b2326fba0403ec24fe912372936"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpstan/extension-installer/zipball/85e90b3942d06b2326fba0403ec24fe912372936",
+ "reference": "85e90b3942d06b2326fba0403ec24fe912372936",
+ "shasum": ""
+ },
+ "require": {
+ "composer-plugin-api": "^2.0",
+ "php": "^7.2 || ^8.0",
+ "phpstan/phpstan": "^1.9.0 || ^2.0"
+ },
+ "require-dev": {
+ "composer/composer": "^2.0",
+ "php-parallel-lint/php-parallel-lint": "^1.2.0",
+ "phpstan/phpstan-strict-rules": "^0.11 || ^0.12 || ^1.0"
+ },
+ "type": "composer-plugin",
+ "extra": {
+ "class": "PHPStan\\ExtensionInstaller\\Plugin"
+ },
+ "autoload": {
+ "psr-4": {
+ "PHPStan\\ExtensionInstaller\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Composer plugin for automatic installation of PHPStan extensions",
+ "keywords": [
+ "dev",
+ "static analysis"
+ ],
+ "support": {
+ "issues": "https://github.com/phpstan/extension-installer/issues",
+ "source": "https://github.com/phpstan/extension-installer/tree/1.4.3"
+ },
+ "time": "2024-09-04T20:21:43+00:00"
+ },
+ {
+ "name": "phpstan/phpdoc-parser",
+ "version": "2.3.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpstan/phpdoc-parser.git",
+ "reference": "1427af4647235b4a73b13940b1f1258d3584f36e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/1427af4647235b4a73b13940b1f1258d3584f36e",
+ "reference": "1427af4647235b4a73b13940b1f1258d3584f36e",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4 || ^8.0"
+ },
+ "require-dev": {
+ "doctrine/annotations": "^2.0",
+ "nikic/php-parser": "^5.3.0",
+ "php-parallel-lint/php-parallel-lint": "^1.2",
+ "phpstan/extension-installer": "^1.0",
+ "phpstan/phpstan": "^2.0",
+ "phpstan/phpstan-phpunit": "^2.0",
+ "phpstan/phpstan-strict-rules": "^2.0",
+ "phpunit/phpunit": "^9.6",
+ "symfony/process": "^5.2"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "PHPStan\\PhpDocParser\\": [
+ "src/"
+ ]
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "PHPDoc parser with support for nullable, intersection and generic types",
+ "support": {
+ "issues": "https://github.com/phpstan/phpdoc-parser/issues",
+ "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.6"
+ },
+ "time": "2026-09-27T20:07:45+00:00"
+ },
+ {
+ "name": "phpstan/phpstan",
+ "version": "2.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpstan/phpstan.git",
+ "reference": "50d276fc3bf1430ec315f2f109bbde2769821524"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpstan/phpstan/zipball/50d276fc3bf1430ec315f2f109bbde2769821524",
+ "reference": "50d276fc3bf1430ec315f2f109bbde2769821524",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4|^8.0"
+ },
+ "conflict": {
+ "phpstan/phpstan-shim": "*"
+ },
+ "bin": [
+ "phpstan",
+ "phpstan.phar"
+ ],
+ "type": "library",
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "PHPStan - PHP Static Analysis Tool",
+ "keywords": [
+ "dev",
+ "static analysis"
+ ],
+ "support": {
+ "docs": "https://phpstan.org/user-guide/getting-started",
+ "forum": "https://github.com/phpstan/phpstan/discussions",
+ "issues": "https://github.com/phpstan/phpstan/issues",
+ "security": "https://github.com/phpstan/phpstan/security/policy",
+ "source": "https://github.com/phpstan/phpstan-src"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/ondrejmirtes",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/phpstan",
+ "type": "github"
+ }
+ ],
+ "time": "2024-12-17T17:14:01+00:00"
+ },
+ {
+ "name": "phpstan/phpstan-doctrine",
+ "version": "2.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpstan/phpstan-doctrine.git",
+ "reference": "bdb6a835c5aa9725979694ae9b70591e180f4853"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/bdb6a835c5aa9725979694ae9b70591e180f4853",
+ "reference": "bdb6a835c5aa9725979694ae9b70591e180f4853",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4 || ^8.0",
+ "phpstan/phpstan": "^2.0.3"
+ },
+ "conflict": {
+ "doctrine/collections": "<1.0",
+ "doctrine/common": "<2.7",
+ "doctrine/mongodb-odm": "<1.2",
+ "doctrine/orm": "<2.5",
+ "doctrine/persistence": "<1.3"
+ },
+ "require-dev": {
+ "cache/array-adapter": "^1.1",
+ "composer/semver": "^3.3.2",
+ "cweagans/composer-patches": "^1.7.3",
+ "doctrine/annotations": "^2.0",
+ "doctrine/collections": "^1.6 || ^2.1",
+ "doctrine/common": "^2.7 || ^3.0",
+ "doctrine/dbal": "^3.3.8",
+ "doctrine/lexer": "^2.0 || ^3.0",
+ "doctrine/mongodb-odm": "^2.4.3",
+ "doctrine/orm": "^2.16.0",
+ "doctrine/persistence": "^2.2.1 || ^3.2",
+ "gedmo/doctrine-extensions": "^3.8",
+ "nesbot/carbon": "^2.49",
+ "php-parallel-lint/php-parallel-lint": "^1.2",
+ "phpstan/phpstan-phpunit": "^2.0",
+ "phpstan/phpstan-strict-rules": "^2.0",
+ "phpunit/phpunit": "^9.6.20",
+ "ramsey/uuid": "^4.2",
+ "symfony/cache": "^5.4"
+ },
+ "type": "phpstan-extension",
+ "extra": {
+ "phpstan": {
+ "includes": [
+ "extension.neon",
+ "rules.neon"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "PHPStan\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Doctrine extensions for PHPStan",
+ "support": {
+ "issues": "https://github.com/phpstan/phpstan-doctrine/issues",
+ "source": "https://github.com/phpstan/phpstan-doctrine/tree/2.0.1"
+ },
+ "time": "2024-12-02T16:48:00+00:00"
+ },
+ {
+ "name": "phpstan/phpstan-strict-rules",
+ "version": "2.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpstan/phpstan-strict-rules.git",
+ "reference": "ed6fea0ad4ad9c7e25f3ad2e7c4d420cf1e67fe3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/ed6fea0ad4ad9c7e25f3ad2e7c4d420cf1e67fe3",
+ "reference": "ed6fea0ad4ad9c7e25f3ad2e7c4d420cf1e67fe3",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4 || ^8.0",
+ "phpstan/phpstan": "^2.0.4"
+ },
+ "require-dev": {
+ "php-parallel-lint/php-parallel-lint": "^1.2",
+ "phpstan/phpstan-deprecation-rules": "^2.0",
+ "phpstan/phpstan-phpunit": "^2.0",
+ "phpunit/phpunit": "^9.6"
+ },
+ "type": "phpstan-extension",
+ "extra": {
+ "phpstan": {
+ "includes": [
+ "rules.neon"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "PHPStan\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Extra strict and opinionated rules for PHPStan",
+ "support": {
+ "issues": "https://github.com/phpstan/phpstan-strict-rules/issues",
+ "source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.1"
+ },
+ "time": "2024-12-12T20:21:10+00:00"
+ },
+ {
+ "name": "phpstan/phpstan-webmozart-assert",
+ "version": "2.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpstan/phpstan-webmozart-assert.git",
+ "reference": "0c641817d2a8f05c7157f92d91986e74d3c8ab0c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpstan/phpstan-webmozart-assert/zipball/0c641817d2a8f05c7157f92d91986e74d3c8ab0c",
+ "reference": "0c641817d2a8f05c7157f92d91986e74d3c8ab0c",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4 || ^8.0",
+ "phpstan/phpstan": "^2.0"
+ },
+ "require-dev": {
+ "nikic/php-parser": "^5.1",
+ "php-parallel-lint/php-parallel-lint": "^1.2",
+ "phpstan/phpstan-deprecation-rules": "^2.0",
+ "phpstan/phpstan-phpunit": "^2.0",
+ "phpstan/phpstan-strict-rules": "^2.0",
+ "phpunit/phpunit": "^9.6",
+ "webmozart/assert": "^1.11.0"
+ },
+ "type": "phpstan-extension",
+ "extra": {
+ "phpstan": {
+ "includes": [
+ "extension.neon"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "PHPStan\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "PHPStan webmozart/assert extension",
+ "support": {
+ "issues": "https://github.com/phpstan/phpstan-webmozart-assert/issues",
+ "source": "https://github.com/phpstan/phpstan-webmozart-assert/tree/2.0.0"
+ },
+ "time": "2024-10-14T03:45:26+00:00"
+ },
+ {
+ "name": "phpunit/php-code-coverage",
+ "version": "9.2.32",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
+ "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/85402a822d1ecf1db1096959413d35e1c37cf1a5",
+ "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-libxml": "*",
+ "ext-xmlwriter": "*",
+ "nikic/php-parser": "^4.19.1 || ^5.1.0",
+ "php": ">=7.3",
+ "phpunit/php-file-iterator": "^3.0.6",
+ "phpunit/php-text-template": "^2.0.4",
+ "sebastian/code-unit-reverse-lookup": "^2.0.3",
+ "sebastian/complexity": "^2.0.3",
+ "sebastian/environment": "^5.1.5",
+ "sebastian/lines-of-code": "^1.0.4",
+ "sebastian/version": "^3.0.2",
+ "theseer/tokenizer": "^1.2.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.6"
+ },
+ "suggest": {
+ "ext-pcov": "PHP extension that provides line coverage",
+ "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "9.2.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.",
+ "homepage": "https://github.com/sebastianbergmann/php-code-coverage",
+ "keywords": [
+ "coverage",
+ "testing",
+ "xunit"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
+ "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
+ "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.32"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-08-22T04:23:01+00:00"
+ },
+ {
+ "name": "phpunit/php-file-iterator",
+ "version": "3.0.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-file-iterator.git",
+ "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf",
+ "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "FilterIterator implementation that filters files based on a list of suffixes.",
+ "homepage": "https://github.com/sebastianbergmann/php-file-iterator/",
+ "keywords": [
+ "filesystem",
+ "iterator"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues",
+ "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2021-12-02T12:48:52+00:00"
+ },
+ {
+ "name": "phpunit/php-invoker",
+ "version": "3.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-invoker.git",
+ "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67",
+ "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "ext-pcntl": "*",
+ "phpunit/phpunit": "^9.3"
+ },
+ "suggest": {
+ "ext-pcntl": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.1-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Invoke callables with a timeout",
+ "homepage": "https://github.com/sebastianbergmann/php-invoker/",
+ "keywords": [
+ "process"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-invoker/issues",
+ "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T05:58:55+00:00"
+ },
+ {
+ "name": "phpunit/php-text-template",
+ "version": "2.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-text-template.git",
+ "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28",
+ "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Simple template engine.",
+ "homepage": "https://github.com/sebastianbergmann/php-text-template/",
+ "keywords": [
+ "template"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-text-template/issues",
+ "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T05:33:50+00:00"
+ },
+ {
+ "name": "phpunit/php-timer",
+ "version": "5.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-timer.git",
+ "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2",
+ "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Utility class for timing",
+ "homepage": "https://github.com/sebastianbergmann/php-timer/",
+ "keywords": [
+ "timer"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-timer/issues",
+ "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:16:10+00:00"
+ },
+ {
+ "name": "phpunit/phpunit",
+ "version": "9.6.38",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/phpunit.git",
+ "reference": "8b5f757e8e92071bfdbfa93943f94f84c4e31007"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/8b5f757e8e92071bfdbfa93943f94f84c4e31007",
+ "reference": "8b5f757e8e92071bfdbfa93943f94f84c4e31007",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/instantiator": "^1.5.0 || ^2",
+ "ext-dom": "*",
+ "ext-filter": "*",
+ "ext-json": "*",
+ "ext-libxml": "*",
+ "ext-mbstring": "*",
+ "ext-xmlwriter": "*",
+ "myclabs/deep-copy": "^1.13.4",
+ "phar-io/manifest": "^2.0.4",
+ "phar-io/version": "^3.2.1",
+ "php": ">=7.3",
+ "phpunit/php-code-coverage": "^9.2.32",
+ "phpunit/php-file-iterator": "^3.0.6",
+ "phpunit/php-invoker": "^3.1.1",
+ "phpunit/php-text-template": "^2.0.4",
+ "phpunit/php-timer": "^5.0.3",
+ "sebastian/cli-parser": "^1.0.2",
+ "sebastian/code-unit": "^1.0.8",
+ "sebastian/comparator": "^4.0.10",
+ "sebastian/diff": "^4.0.6",
+ "sebastian/environment": "^5.1.5",
+ "sebastian/exporter": "^4.0.9",
+ "sebastian/global-state": "^5.0.8",
+ "sebastian/object-enumerator": "^4.0.4",
+ "sebastian/resource-operations": "^3.0.4",
+ "sebastian/type": "^3.2.1",
+ "sebastian/version": "^3.0.2"
+ },
+ "suggest": {
+ "ext-soap": "To be able to generate mocks based on WSDL files",
+ "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage"
+ },
+ "bin": [
+ "phpunit"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "9.6-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/Framework/Assert/Functions.php"
+ ],
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "The PHP Unit Testing framework.",
+ "homepage": "https://phpunit.de/",
+ "keywords": [
+ "phpunit",
+ "testing",
+ "xunit"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/phpunit/issues",
+ "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
+ "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.38"
+ },
+ "funding": [
+ {
+ "url": "https://phpunit.de/sponsoring.html",
+ "type": "other"
+ }
+ ],
+ "time": "2026-10-05T05:44:26+00:00"
+ },
+ {
+ "name": "phrity/net-stream",
+ "version": "1.3.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sirn-se/phrity-net-stream.git",
+ "reference": "9105931b65ad90c75f4885a40b268b0f65802e3e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sirn-se/phrity-net-stream/zipball/9105931b65ad90c75f4885a40b268b0f65802e3e",
+ "reference": "9105931b65ad90c75f4885a40b268b0f65802e3e",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4 | ^8.0",
+ "phrity/util-errorhandler": "^1.0",
+ "psr/http-factory": "^1.0",
+ "psr/http-message": "^1.0 | ^2.0"
+ },
+ "require-dev": {
+ "php-coveralls/php-coveralls": "^2.0",
+ "phpunit/phpunit": "^9.0 | ^10.0",
+ "phrity/net-uri": "^1.1",
+ "squizlabs/php_codesniffer": "^3.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Phrity\\Net\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Sören Jensen",
+ "email": "sirn@sirn.se",
+ "homepage": "https://phrity.sirn.se"
+ }
+ ],
+ "description": "Socket stream classes implementing PSR-7 Stream and PSR-17 StreamFactory",
+ "homepage": "https://phrity.sirn.se/net-stream",
+ "keywords": [
+ "Socket",
+ "client",
+ "psr-17",
+ "psr-7",
+ "server",
+ "stream",
+ "stream factory"
+ ],
+ "support": {
+ "issues": "https://github.com/sirn-se/phrity-net-stream/issues",
+ "source": "https://github.com/sirn-se/phrity-net-stream/tree/1.3.0"
+ },
+ "time": "2023-10-22T10:47:03+00:00"
+ },
+ {
+ "name": "phrity/net-uri",
+ "version": "1.3.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sirn-se/phrity-net-uri.git",
+ "reference": "3f458e0c4d1ddc0e218d7a5b9420127c63925f43"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sirn-se/phrity-net-uri/zipball/3f458e0c4d1ddc0e218d7a5b9420127c63925f43",
+ "reference": "3f458e0c4d1ddc0e218d7a5b9420127c63925f43",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4 | ^8.0",
+ "psr/http-factory": "^1.0",
+ "psr/http-message": "^1.0 | ^2.0"
+ },
+ "require-dev": {
+ "php-coveralls/php-coveralls": "^2.0",
+ "phpunit/phpunit": "^9.0 | ^10.0",
+ "squizlabs/php_codesniffer": "^3.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Phrity\\Net\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Sören Jensen",
+ "email": "sirn@sirn.se",
+ "homepage": "https://phrity.sirn.se"
+ }
+ ],
+ "description": "PSR-7 Uri and PSR-17 UriFactory implementation",
+ "homepage": "https://phrity.sirn.se/net-uri",
+ "keywords": [
+ "psr-17",
+ "psr-7",
+ "uri",
+ "uri factory"
+ ],
+ "support": {
+ "issues": "https://github.com/sirn-se/phrity-net-uri/issues",
+ "source": "https://github.com/sirn-se/phrity-net-uri/tree/1.3.0"
+ },
+ "time": "2023-08-21T10:33:06+00:00"
+ },
+ {
+ "name": "phrity/util-errorhandler",
+ "version": "1.2.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sirn-se/phrity-util-errorhandler.git",
+ "reference": "70a669cc22db2eed6a109ec66fd95168a4332c9b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sirn-se/phrity-util-errorhandler/zipball/70a669cc22db2eed6a109ec66fd95168a4332c9b",
+ "reference": "70a669cc22db2eed6a109ec66fd95168a4332c9b",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.1"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^2.0",
+ "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0",
+ "robiningelbrecht/phpunit-coverage-tools": "^1.9",
+ "squizlabs/php_codesniffer": "^3.5 || ^4.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Phrity\\Util\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Sören Jensen",
+ "email": "sirn@sirn.se",
+ "homepage": "https://phrity.sirn.se"
+ }
+ ],
+ "description": "Inline error handler; catch and resolve errors for code block.",
+ "homepage": "https://phrity.sirn.se/util-errorhandler",
+ "keywords": [
+ "error",
+ "warning"
+ ],
+ "support": {
+ "issues": "https://github.com/sirn-se/phrity-util-errorhandler/issues",
+ "source": "https://github.com/sirn-se/phrity-util-errorhandler/tree/1.2.2"
+ },
+ "time": "2025-12-05T21:25:36+00:00"
+ },
+ {
+ "name": "phrity/websocket",
+ "version": "1.7.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sirn-se/websocket-php.git",
+ "reference": "8a525da4457b599ab1960f24183f25626c96ce3c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sirn-se/websocket-php/zipball/8a525da4457b599ab1960f24183f25626c96ce3c",
+ "reference": "8a525da4457b599ab1960f24183f25626c96ce3c",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4 | ^8.0",
+ "phrity/net-stream": "^1.2",
+ "phrity/net-uri": "^1.2",
+ "phrity/util-errorhandler": "^1.0",
+ "psr/http-message": "^1.1 | ^2.0",
+ "psr/log": "^1.0 | ^2.0 | ^3.0"
+ },
+ "require-dev": {
+ "php-coveralls/php-coveralls": "^2.0",
+ "phpunit/phpunit": "^9.0 | ^10.0",
+ "phrity/net-mock": "^1.3",
+ "squizlabs/php_codesniffer": "^3.5"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "WebSocket\\": "lib/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "ISC"
+ ],
+ "authors": [
+ {
+ "name": "Fredrik Liljegren"
+ },
+ {
+ "name": "Sören Jensen",
+ "email": "sirn@sirn.se",
+ "homepage": "https://phrity.sirn.se"
+ }
+ ],
+ "description": "WebSocket client and server",
+ "homepage": "https://phrity.sirn.se/websocket",
+ "keywords": [
+ "client",
+ "server",
+ "websocket"
+ ],
+ "support": {
+ "issues": "https://github.com/sirn-se/websocket-php/issues",
+ "source": "https://github.com/sirn-se/websocket-php/tree/1.7.3"
+ },
+ "time": "2024-05-31T13:43:32+00:00"
+ },
+ {
+ "name": "rector/rector",
+ "version": "2.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/rectorphp/rector.git",
+ "reference": "df5de7b80deced1ea7f719a0b4d02e4aee87dd21"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/rectorphp/rector/zipball/df5de7b80deced1ea7f719a0b4d02e4aee87dd21",
+ "reference": "df5de7b80deced1ea7f719a0b4d02e4aee87dd21",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4|^8.0",
+ "phpstan/phpstan": "^2.0.4"
+ },
+ "conflict": {
+ "rector/rector-doctrine": "*",
+ "rector/rector-downgrade-php": "*",
+ "rector/rector-phpunit": "*",
+ "rector/rector-symfony": "*"
+ },
+ "suggest": {
+ "ext-dom": "To manipulate phpunit.xml via the custom-rule command"
+ },
+ "bin": [
+ "bin/rector"
+ ],
+ "type": "library",
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Instant Upgrade and Automated Refactoring of any PHP code",
+ "keywords": [
+ "automation",
+ "dev",
+ "migration",
+ "refactoring"
+ ],
+ "support": {
+ "issues": "https://github.com/rectorphp/rector/issues",
+ "source": "https://github.com/rectorphp/rector/tree/2.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/tomasvotruba",
+ "type": "github"
+ }
+ ],
+ "time": "2024-12-26T23:06:19+00:00"
+ },
+ {
+ "name": "revolt/event-loop",
+ "version": "v1.0.9",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/revoltphp/event-loop.git",
+ "reference": "44061cf513e53c6200372fc935ac42271566295d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/revoltphp/event-loop/zipball/44061cf513e53c6200372fc935ac42271566295d",
+ "reference": "44061cf513e53c6200372fc935ac42271566295d",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "require-dev": {
+ "ext-json": "*",
+ "jetbrains/phpstorm-stubs": "^2019.3",
+ "phpunit/phpunit": "^9",
+ "psalm/phar": "6.16.*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Revolt\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Aaron Piotrowski",
+ "email": "aaron@trowski.com"
+ },
+ {
+ "name": "Cees-Jan Kiewiet",
+ "email": "ceesjank@gmail.com"
+ },
+ {
+ "name": "Christian Lück",
+ "email": "christian@clue.engineering"
+ },
+ {
+ "name": "Niklas Keller",
+ "email": "me@kelunik.com"
+ }
+ ],
+ "description": "Rock-solid event loop for concurrent PHP applications.",
+ "keywords": [
+ "async",
+ "asynchronous",
+ "concurrency",
+ "event",
+ "event-loop",
+ "non-blocking",
+ "scheduler"
+ ],
+ "support": {
+ "issues": "https://github.com/revoltphp/event-loop/issues",
+ "source": "https://github.com/revoltphp/event-loop/tree/v1.0.9"
+ },
+ "time": "2026-05-16T17:55:38+00:00"
+ },
+ {
+ "name": "sebastian/cli-parser",
+ "version": "1.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/cli-parser.git",
+ "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b",
+ "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for parsing CLI options",
+ "homepage": "https://github.com/sebastianbergmann/cli-parser",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/cli-parser/issues",
+ "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-02T06:27:43+00:00"
+ },
+ {
+ "name": "sebastian/code-unit",
+ "version": "1.0.8",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/code-unit.git",
+ "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120",
+ "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Collection of value objects that represent the PHP code units",
+ "homepage": "https://github.com/sebastianbergmann/code-unit",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/code-unit/issues",
+ "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:08:54+00:00"
+ },
+ {
+ "name": "sebastian/code-unit-reverse-lookup",
+ "version": "2.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git",
+ "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5",
+ "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Looks up which function or method a line of code belongs to",
+ "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues",
+ "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T05:30:19+00:00"
+ },
+ {
+ "name": "sebastian/comparator",
+ "version": "4.0.10",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/comparator.git",
+ "reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/e4df00b9b3571187db2831ae9aada2c6efbd715d",
+ "reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3",
+ "sebastian/diff": "^4.0",
+ "sebastian/exporter": "^4.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Volker Dusch",
+ "email": "github@wallbash.com"
+ },
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@2bepublished.at"
+ }
+ ],
+ "description": "Provides the functionality to compare PHP values for equality",
+ "homepage": "https://github.com/sebastianbergmann/comparator",
+ "keywords": [
+ "comparator",
+ "compare",
+ "equality"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/comparator/issues",
+ "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.10"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-24T09:22:56+00:00"
+ },
+ {
+ "name": "sebastian/complexity",
+ "version": "2.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/complexity.git",
+ "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a",
+ "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a",
+ "shasum": ""
+ },
+ "require": {
+ "nikic/php-parser": "^4.18 || ^5.0",
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for calculating the complexity of PHP code units",
+ "homepage": "https://github.com/sebastianbergmann/complexity",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/complexity/issues",
+ "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-12-22T06:19:30+00:00"
+ },
+ {
+ "name": "sebastian/diff",
+ "version": "4.0.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/diff.git",
+ "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc",
+ "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3",
+ "symfony/process": "^4.2 || ^5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Kore Nordmann",
+ "email": "mail@kore-nordmann.de"
+ }
+ ],
+ "description": "Diff implementation",
+ "homepage": "https://github.com/sebastianbergmann/diff",
+ "keywords": [
+ "diff",
+ "udiff",
+ "unidiff",
+ "unified diff"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/diff/issues",
+ "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-02T06:30:58+00:00"
+ },
+ {
+ "name": "sebastian/environment",
+ "version": "5.1.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/environment.git",
+ "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed",
+ "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "suggest": {
+ "ext-posix": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.1-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Provides functionality to handle HHVM/PHP environments",
+ "homepage": "http://www.github.com/sebastianbergmann/environment",
+ "keywords": [
+ "Xdebug",
+ "environment",
+ "hhvm"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/environment/issues",
+ "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-02-03T06:03:51+00:00"
+ },
+ {
+ "name": "sebastian/exporter",
+ "version": "4.0.9",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/exporter.git",
+ "reference": "4352c1a3df741a7ba9e61af6fed51d1fee41cbf7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/4352c1a3df741a7ba9e61af6fed51d1fee41cbf7",
+ "reference": "4352c1a3df741a7ba9e61af6fed51d1fee41cbf7",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3",
+ "sebastian/recursion-context": "^4.0"
+ },
+ "require-dev": {
+ "ext-mbstring": "*",
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Volker Dusch",
+ "email": "github@wallbash.com"
+ },
+ {
+ "name": "Adam Harvey",
+ "email": "aharvey@php.net"
+ },
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@gmail.com"
+ }
+ ],
+ "description": "Provides the functionality to export PHP variables for visualization",
+ "homepage": "https://www.github.com/sebastianbergmann/exporter",
+ "keywords": [
+ "export",
+ "exporter"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/exporter/issues",
+ "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.9"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-08-11T04:55:59+00:00"
+ },
+ {
+ "name": "sebastian/global-state",
+ "version": "5.0.8",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/global-state.git",
+ "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b6781316bdcd28260904e7cc18ec983d0d2ef4f6",
+ "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3",
+ "sebastian/object-reflector": "^2.0",
+ "sebastian/recursion-context": "^4.0"
+ },
+ "require-dev": {
+ "ext-dom": "*",
+ "phpunit/phpunit": "^9.3"
+ },
+ "suggest": {
+ "ext-uopz": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Snapshotting of global state",
+ "homepage": "http://www.github.com/sebastianbergmann/global-state",
+ "keywords": [
+ "global state"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/global-state/issues",
+ "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.8"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-08-10T07:10:35+00:00"
+ },
+ {
+ "name": "sebastian/lines-of-code",
+ "version": "1.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/lines-of-code.git",
+ "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5",
+ "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5",
+ "shasum": ""
+ },
+ "require": {
+ "nikic/php-parser": "^4.18 || ^5.0",
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for counting the lines of code in PHP source code",
+ "homepage": "https://github.com/sebastianbergmann/lines-of-code",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/lines-of-code/issues",
+ "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-12-22T06:20:34+00:00"
+ },
+ {
+ "name": "sebastian/object-enumerator",
+ "version": "4.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/object-enumerator.git",
+ "reference": "5c9eeac41b290a3712d88851518825ad78f45c71"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71",
+ "reference": "5c9eeac41b290a3712d88851518825ad78f45c71",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3",
+ "sebastian/object-reflector": "^2.0",
+ "sebastian/recursion-context": "^4.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Traverses array structures and object graphs to enumerate all referenced objects",
+ "homepage": "https://github.com/sebastianbergmann/object-enumerator/",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/object-enumerator/issues",
+ "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:12:34+00:00"
+ },
+ {
+ "name": "sebastian/object-reflector",
+ "version": "2.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/object-reflector.git",
+ "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7",
+ "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Allows reflection of object attributes, including inherited and non-public ones",
+ "homepage": "https://github.com/sebastianbergmann/object-reflector/",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/object-reflector/issues",
+ "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:14:26+00:00"
+ },
+ {
+ "name": "sebastian/recursion-context",
+ "version": "4.0.7",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/recursion-context.git",
+ "reference": "c85be6922b7fd365942b986b9a50397d65407611"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/c85be6922b7fd365942b986b9a50397d65407611",
+ "reference": "c85be6922b7fd365942b986b9a50397d65407611",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Adam Harvey",
+ "email": "aharvey@php.net"
+ }
+ ],
+ "description": "Provides functionality to recursively process PHP variables",
+ "homepage": "https://github.com/sebastianbergmann/recursion-context",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/recursion-context/issues",
+ "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.7"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-08-11T05:25:24+00:00"
+ },
+ {
+ "name": "sebastian/resource-operations",
+ "version": "3.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/resource-operations.git",
+ "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e",
+ "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Provides a list of PHP built-in functions that operate on resources",
+ "homepage": "https://www.github.com/sebastianbergmann/resource-operations",
+ "support": {
+ "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-14T16:00:52+00:00"
+ },
+ {
+ "name": "sebastian/type",
+ "version": "3.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/type.git",
+ "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7",
+ "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.2-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Collection of value objects that represent the types of the PHP type system",
+ "homepage": "https://github.com/sebastianbergmann/type",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/type/issues",
+ "source": "https://github.com/sebastianbergmann/type/tree/3.2.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-02-03T06:13:03+00:00"
+ },
+ {
+ "name": "sebastian/version",
+ "version": "3.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/version.git",
+ "reference": "c6c1022351a901512170118436c764e473f6de8c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c",
+ "reference": "c6c1022351a901512170118436c764e473f6de8c",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library that helps with managing the version number of Git-hosted PHP projects",
+ "homepage": "https://github.com/sebastianbergmann/version",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/version/issues",
+ "source": "https://github.com/sebastianbergmann/version/tree/3.0.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-09-28T06:39:44+00:00"
+ },
+ {
+ "name": "seld/jsonlint",
+ "version": "1.12.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Seldaek/jsonlint.git",
+ "reference": "9a90eb5d32d5a500296bf43f946d60246444d5f7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/9a90eb5d32d5a500296bf43f946d60246444d5f7",
+ "reference": "9a90eb5d32d5a500296bf43f946d60246444d5f7",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.3 || ^7.0 || ^8.0"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^1.11",
+ "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0 || ^8.5.13"
+ },
+ "bin": [
+ "bin/jsonlint"
+ ],
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Seld\\JsonLint\\": "src/Seld/JsonLint/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jordi Boggiano",
+ "email": "j.boggiano@seld.be",
+ "homepage": "https://seld.be"
+ }
+ ],
+ "description": "JSON Linter",
+ "keywords": [
+ "json",
+ "linter",
+ "parser",
+ "validator"
+ ],
+ "support": {
+ "issues": "https://github.com/Seldaek/jsonlint/issues",
+ "source": "https://github.com/Seldaek/jsonlint/tree/1.12.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/Seldaek",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/seld/jsonlint",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-06-12T11:32:29+00:00"
+ },
+ {
+ "name": "slevomat/coding-standard",
+ "version": "8.22.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/slevomat/coding-standard.git",
+ "reference": "1dd80bf3b93692bedb21a6623c496887fad05fec"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/1dd80bf3b93692bedb21a6623c496887fad05fec",
+ "reference": "1dd80bf3b93692bedb21a6623c496887fad05fec",
+ "shasum": ""
+ },
+ "require": {
+ "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.1.2",
+ "php": "^7.4 || ^8.0",
+ "phpstan/phpdoc-parser": "^2.3.0",
+ "squizlabs/php_codesniffer": "^3.13.4"
+ },
+ "require-dev": {
+ "phing/phing": "3.0.1|3.1.0",
+ "php-parallel-lint/php-parallel-lint": "1.4.0",
+ "phpstan/phpstan": "2.1.24",
+ "phpstan/phpstan-deprecation-rules": "2.0.3",
+ "phpstan/phpstan-phpunit": "2.0.7",
+ "phpstan/phpstan-strict-rules": "2.0.6",
+ "phpunit/phpunit": "9.6.8|10.5.48|11.4.4|11.5.36|12.3.10"
+ },
+ "type": "phpcodesniffer-standard",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "8.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "SlevomatCodingStandard\\": "SlevomatCodingStandard/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Slevomat Coding Standard for PHP_CodeSniffer complements Consistence Coding Standard by providing sniffs with additional checks.",
+ "keywords": [
+ "dev",
+ "phpcs"
+ ],
+ "support": {
+ "issues": "https://github.com/slevomat/coding-standard/issues",
+ "source": "https://github.com/slevomat/coding-standard/tree/8.22.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/kukulich",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/slevomat/coding-standard",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-09-13T08:53:30+00:00"
+ },
+ {
+ "name": "squizlabs/php_codesniffer",
+ "version": "3.13.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git",
+ "reference": "4c378e1a528ea066890fc2397cbdd2f94eb2fc91"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/4c378e1a528ea066890fc2397cbdd2f94eb2fc91",
+ "reference": "4c378e1a528ea066890fc2397cbdd2f94eb2fc91",
+ "shasum": ""
+ },
+ "require": {
+ "ext-simplexml": "*",
+ "ext-tokenizer": "*",
+ "ext-xmlwriter": "*",
+ "php": ">=5.4.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4"
+ },
+ "bin": [
+ "bin/phpcbf",
+ "bin/phpcs"
+ ],
+ "type": "library",
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Greg Sherwood",
+ "role": "Former lead"
+ },
+ {
+ "name": "Juliette Reinders Folmer",
+ "role": "Current lead"
+ },
+ {
+ "name": "Contributors",
+ "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors"
+ }
+ ],
+ "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.",
+ "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer",
+ "keywords": [
+ "phpcs",
+ "standards",
+ "static analysis"
+ ],
+ "support": {
+ "issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues",
+ "security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy",
+ "source": "https://github.com/PHPCSStandards/PHP_CodeSniffer",
+ "wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/PHPCSStandards",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/jrfnl",
+ "type": "github"
+ },
+ {
+ "url": "https://opencollective.com/php_codesniffer",
+ "type": "open_collective"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/phpcsstandards",
+ "type": "thanks_dev"
+ }
+ ],
+ "time": "2026-08-06T00:17:32+00:00"
+ },
+ {
+ "name": "sylius-labs/coding-standard",
+ "version": "v4.5.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/SyliusLabs/CodingStandard.git",
+ "reference": "8543560abff256e185f3ff3a0ff566968d1d9fb7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/SyliusLabs/CodingStandard/zipball/8543560abff256e185f3ff3a0ff566968d1d9fb7",
+ "reference": "8543560abff256e185f3ff3a0ff566968d1d9fb7",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.0",
+ "slevomat/coding-standard": "^8.0",
+ "symplify/easy-coding-standard": "^10.0 || ^11.0 || ^12.0 || ^13.0"
+ },
+ "conflict": {
+ "slevomat/coding-standard": ">=8.23",
+ "symplify/easy-coding-standard": ">=13.1.3",
+ "symplify/package-builder": "^8.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.4-dev"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Kamil Kokot",
+ "email": "kamil@kokot.me",
+ "homepage": "https://kamilkokot.com"
+ }
+ ],
+ "description": "Battle-tested coding standard configuration used in Sylius.",
+ "support": {
+ "issues": "https://github.com/SyliusLabs/CodingStandard/issues",
+ "source": "https://github.com/SyliusLabs/CodingStandard/tree/v4.5.1"
+ },
+ "time": "2026-05-05T13:14:49+00:00"
+ },
+ {
+ "name": "sylius/test-application",
+ "version": "v2.2.0-ALPHA.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Sylius/TestApplication.git",
+ "reference": "a53d99b62a7d48a635690ebf6f55992b896dc8c1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Sylius/TestApplication/zipball/a53d99b62a7d48a635690ebf6f55992b896dc8c1",
+ "reference": "a53d99b62a7d48a635690ebf6f55992b896dc8c1",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.2",
+ "sylius/sylius": "~2.2.0",
+ "symfony/debug-bundle": "*",
+ "symfony/dotenv": "*",
+ "symfony/flex": "*",
+ "symfony/runtime": "*",
+ "symfony/web-profiler-bundle": "*",
+ "theofidry/alice-data-fixtures": "*"
+ },
+ "bin": [
+ "bin/console"
+ ],
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Sylius\\TestApplication\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "support": {
+ "issues": "https://github.com/Sylius/TestApplication/issues",
+ "source": "https://github.com/Sylius/TestApplication/tree/v2.2.0-ALPHA.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sylius",
+ "type": "github"
+ }
+ ],
+ "time": "2025-12-22T09:59:03+00:00"
+ },
+ {
+ "name": "symfony/apache-pack",
+ "version": "v1.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/apache-pack.git",
+ "reference": "3aa5818d73ad2551281fc58a75afd9ca82622e6c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/apache-pack/zipball/3aa5818d73ad2551281fc58a75afd9ca82622e6c",
+ "reference": "3aa5818d73ad2551281fc58a75afd9ca82622e6c",
+ "shasum": ""
+ },
+ "type": "symfony-pack",
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "A pack for Apache support in Symfony",
+ "support": {
+ "issues": "https://github.com/symfony/apache-pack/issues",
+ "source": "https://github.com/symfony/apache-pack/tree/master"
+ },
+ "time": "2017-12-12T01:46:35+00:00"
+ },
+ {
+ "name": "symfony/browser-kit",
+ "version": "v6.4.44",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/browser-kit.git",
+ "reference": "4f631476bb549d882a173c47bf693f3ef84c722d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/browser-kit/zipball/4f631476bb549d882a173c47bf693f3ef84c722d",
+ "reference": "4f631476bb549d882a173c47bf693f3ef84c722d",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/dom-crawler": "^5.4|^6.0|^7.0"
+ },
+ "require-dev": {
+ "symfony/css-selector": "^5.4|^6.0|^7.0",
+ "symfony/http-client": "^5.4|^6.0|^7.0",
+ "symfony/mime": "^5.4|^6.0|^7.0",
+ "symfony/process": "^5.4|^6.0|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\BrowserKit\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Simulates the behavior of a web browser, allowing you to make requests, click on links and submit forms programmatically",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/browser-kit/tree/v6.4.44"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-08-21T15:19:05+00:00"
+ },
+ {
+ "name": "symfony/css-selector",
+ "version": "v6.4.45",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/css-selector.git",
+ "reference": "5484e316cd8125f5215bbf829151e2211aa6d101"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/css-selector/zipball/5484e316cd8125f5215bbf829151e2211aa6d101",
+ "reference": "5484e316cd8125f5215bbf829151e2211aa6d101",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\CssSelector\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Jean-François Simon",
+ "email": "jeanfrancois.simon@sensiolabs.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Converts CSS selectors to XPath expressions",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/css-selector/tree/v6.4.45"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-08-23T07:35:46+00:00"
+ },
+ {
+ "name": "symfony/debug-bundle",
+ "version": "v6.4.35",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/debug-bundle.git",
+ "reference": "eb79084c2c9778559b21f61cb1507cbd580cc6e1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/debug-bundle/zipball/eb79084c2c9778559b21f61cb1507cbd580cc6e1",
+ "reference": "eb79084c2c9778559b21f61cb1507cbd580cc6e1",
+ "shasum": ""
+ },
+ "require": {
+ "ext-xml": "*",
+ "php": ">=8.1",
+ "symfony/dependency-injection": "^5.4|^6.0|^7.0",
+ "symfony/http-kernel": "^5.4|^6.0|^7.0",
+ "symfony/twig-bridge": "^5.4|^6.0|^7.0",
+ "symfony/var-dumper": "^5.4|^6.0|^7.0"
+ },
+ "conflict": {
+ "symfony/config": "<5.4",
+ "symfony/dependency-injection": "<5.4"
+ },
+ "require-dev": {
+ "symfony/config": "^5.4|^6.0|^7.0",
+ "symfony/web-profiler-bundle": "^5.4|^6.0|^7.0"
+ },
+ "type": "symfony-bundle",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Bundle\\DebugBundle\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides a tight integration of the Symfony VarDumper component and the ServerLogCommand from MonologBridge into the Symfony full-stack framework",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/debug-bundle/tree/v6.4.35"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-03-02T09:25:10+00:00"
+ },
+ {
+ "name": "symfony/dom-crawler",
+ "version": "v6.4.44",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/dom-crawler.git",
+ "reference": "998855f6c3699f148b54096d1d7d2b8b0b0fc298"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/998855f6c3699f148b54096d1d7d2b8b0b0fc298",
+ "reference": "998855f6c3699f148b54096d1d7d2b8b0b0fc298",
+ "shasum": ""
+ },
+ "require": {
+ "masterminds/html5": "^2.6",
+ "php": ">=8.1",
+ "symfony/polyfill-ctype": "~1.8",
+ "symfony/polyfill-mbstring": "~1.0"
+ },
+ "require-dev": {
+ "symfony/css-selector": "^5.4|^6.0|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\DomCrawler\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Eases DOM navigation for HTML and XML documents",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/dom-crawler/tree/v6.4.44"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-08-20T17:31:07+00:00"
+ },
+ {
+ "name": "symfony/dotenv",
+ "version": "v6.4.45",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/dotenv.git",
+ "reference": "9b827002b54f89a5ac605c21c349a42161996afb"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/dotenv/zipball/9b827002b54f89a5ac605c21c349a42161996afb",
+ "reference": "9b827002b54f89a5ac605c21c349a42161996afb",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "conflict": {
+ "symfony/console": "<5.4",
+ "symfony/process": "<5.4"
+ },
+ "require-dev": {
+ "symfony/console": "^5.4|^6.0|^7.0",
+ "symfony/process": "^5.4|^6.0|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Dotenv\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Registers environment variables from a .env file",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "dotenv",
+ "env",
+ "environment"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/dotenv/tree/v6.4.45"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-07-26T10:10:07+00:00"
+ },
+ {
+ "name": "symfony/flex",
+ "version": "v2.11.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/flex.git",
+ "reference": "4a6d98eea3ebc7f68d82810cb682eedca2649e99"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/flex/zipball/4a6d98eea3ebc7f68d82810cb682eedca2649e99",
+ "reference": "4a6d98eea3ebc7f68d82810cb682eedca2649e99",
+ "shasum": ""
+ },
+ "require": {
+ "composer-plugin-api": "^2.1",
+ "php": ">=8.1"
+ },
+ "conflict": {
+ "composer/semver": "<1.7.2",
+ "symfony/dotenv": "<5.4"
+ },
+ "require-dev": {
+ "composer/composer": "^2.1",
+ "phpunit/phpunit": "^12.4",
+ "symfony/dotenv": "^6.4.41|^7.4.13|^8.0.13",
+ "symfony/filesystem": "^6.4|^7.4|^8.0",
+ "symfony/process": "^6.4|^7.4|^8.0"
+ },
+ "type": "composer-plugin",
+ "extra": {
+ "class": "Symfony\\Flex\\Flex"
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Flex\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien.potencier@gmail.com"
+ }
+ ],
+ "description": "Composer plugin for Symfony",
+ "support": {
+ "issues": "https://github.com/symfony/flex/issues",
+ "source": "https://github.com/symfony/flex/tree/v2.11.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-05-29T17:25:22+00:00"
+ },
+ {
+ "name": "symfony/runtime",
+ "version": "v6.4.41",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/runtime.git",
+ "reference": "4a354e15532ed4f99c6d0fdc5618bfbd3ea05a8a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/runtime/zipball/4a354e15532ed4f99c6d0fdc5618bfbd3ea05a8a",
+ "reference": "4a354e15532ed4f99c6d0fdc5618bfbd3ea05a8a",
+ "shasum": ""
+ },
+ "require": {
+ "composer-plugin-api": "^1.0|^2.0",
+ "php": ">=8.1"
+ },
+ "conflict": {
+ "symfony/dotenv": "<5.4"
+ },
+ "require-dev": {
+ "composer/composer": "^1.0.2|^2.0",
+ "symfony/console": "^5.4.9|^6.0.9|^7.0",
+ "symfony/dotenv": "^5.4|^6.0|^7.0",
+ "symfony/http-foundation": "^5.4|^6.0|^7.0",
+ "symfony/http-kernel": "^5.4|^6.0|^7.0"
+ },
+ "type": "composer-plugin",
+ "extra": {
+ "class": "Symfony\\Component\\Runtime\\Internal\\ComposerPlugin"
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Runtime\\": "",
+ "Symfony\\Runtime\\Symfony\\Component\\": "Internal/"
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Enables decoupling PHP applications from global state",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "runtime"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/runtime/tree/v6.4.41"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-05-23T14:01:00+00:00"
+ },
+ {
+ "name": "symfony/web-profiler-bundle",
+ "version": "v6.4.47",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/web-profiler-bundle.git",
+ "reference": "df8f3c9f05c08cd7aeab029b4c6af456265c3666"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/web-profiler-bundle/zipball/df8f3c9f05c08cd7aeab029b4c6af456265c3666",
+ "reference": "df8f3c9f05c08cd7aeab029b4c6af456265c3666",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/config": "^5.4|^6.0|^7.0",
+ "symfony/framework-bundle": "^6.4|^7.0",
+ "symfony/http-kernel": "^6.4|^7.0",
+ "symfony/routing": "^5.4|^6.0|^7.0",
+ "symfony/twig-bundle": "^5.4|^6.0",
+ "twig/twig": "^2.13|^3.0.4|^4.0"
+ },
+ "conflict": {
+ "symfony/form": "<5.4",
+ "symfony/mailer": "<5.4",
+ "symfony/messenger": "<5.4",
+ "symfony/twig-bundle": ">=7.0"
+ },
+ "require-dev": {
+ "symfony/browser-kit": "^5.4|^6.0|^7.0",
+ "symfony/console": "^5.4|^6.0|^7.0",
+ "symfony/css-selector": "^5.4|^6.0|^7.0",
+ "symfony/stopwatch": "^5.4|^6.0|^7.0"
+ },
+ "type": "symfony-bundle",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Bundle\\WebProfilerBundle\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides a development tool that gives detailed information about the execution of any request",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "dev"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/web-profiler-bundle/tree/v6.4.47"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-09-28T16:09:37+00:00"
+ },
+ {
+ "name": "symplify/easy-coding-standard",
+ "version": "13.1.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/ecsphp/ecs.git",
+ "reference": "6d22473d1f36945884d8cb291777166020a47770"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/ecsphp/ecs/zipball/6d22473d1f36945884d8cb291777166020a47770",
+ "reference": "6d22473d1f36945884d8cb291777166020a47770",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "conflict": {
+ "friendsofphp/php-cs-fixer": "<3.92.4",
+ "phpcsstandards/php_codesniffer": "<4.0.1",
+ "symplify/coding-standard": "<12.1"
+ },
+ "suggest": {
+ "ext-dom": "Needed to support checkstyle output format in class CheckstyleOutputFormatter"
+ },
+ "bin": [
+ "bin/ecs"
+ ],
+ "type": "library",
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Use Coding Standard with 0-knowledge of PHP-CS-Fixer and PHP_CodeSniffer",
+ "keywords": [
+ "Code style",
+ "automation",
+ "fixer",
+ "static analysis"
+ ],
+ "support": {
+ "issues": "https://github.com/easy-coding-standard/ecs/issues",
+ "source": "https://github.com/easy-coding-standard/ecs/tree/13.1.2"
+ },
+ "funding": [
+ {
+ "url": "https://www.paypal.me/rectorphp",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/tomasvotruba",
+ "type": "github"
+ }
+ ],
+ "time": "2026-05-03T22:05:09+00:00"
+ },
+ {
+ "name": "theofidry/alice-data-fixtures",
+ "version": "1.7.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/theofidry/AliceDataFixtures.git",
+ "reference": "39a2fb2d83d683bec58e9fa0c1e22feceb17056e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/theofidry/AliceDataFixtures/zipball/39a2fb2d83d683bec58e9fa0c1e22feceb17056e",
+ "reference": "39a2fb2d83d683bec58e9fa0c1e22feceb17056e",
+ "shasum": ""
+ },
+ "require": {
+ "nelmio/alice": "^3.10",
+ "php": "^8.2",
+ "psr/log": "^1 || ^2 || ^3",
+ "webmozart/assert": "^1.10"
+ },
+ "conflict": {
+ "doctrine/dbal": "<3.0",
+ "doctrine/orm": "<2.6.3",
+ "doctrine/persistence": "<2.0",
+ "illuminate/database": "<8.12",
+ "ocramius/proxy-manager": "<2.1",
+ "symfony/framework-bundle": "<5.4 || >=6.0 <6.4",
+ "zendframework/zend-code": "<3.3.1"
+ },
+ "require-dev": {
+ "bamarni/composer-bin-plugin": "^1.4.1",
+ "doctrine/annotations": "^1.13",
+ "phpspec/prophecy": "^1.14.0",
+ "phpspec/prophecy-phpunit": "^2.0.1",
+ "phpunit/phpunit": "^9.5.10",
+ "symfony/phpunit-bridge": "^5.3.8 || ^6.4"
+ },
+ "suggest": {
+ "alcaeus/mongo-php-adapter": "To use Doctrine with the MongoDB flavour",
+ "doctrine/data-fixtures": "To use Doctrine",
+ "doctrine/dbal": "To use Doctrine with the PHPCR flavour",
+ "doctrine/mongodb": "To use Doctrine with the MongoDB flavour",
+ "doctrine/mongodb-odm": "To use Doctrine with the MongoDB flavour",
+ "doctrine/orm": "To use Doctrine ORM",
+ "doctrine/phpcr-odm": "To use Doctrine with the PHPCR flavour",
+ "illuminate/database": "To use Eloquent",
+ "jackalope/jackalope-doctrine-dbal": "To use Doctrine with the PHPCR flavour",
+ "ocramius/proxy-manager": "To avoid database connection on kernel boot"
+ },
+ "type": "library",
+ "extra": {
+ "bamarni-bin": {
+ "bin-links": false,
+ "forward-command": false
+ },
+ "branch-alias": {
+ "dev-master": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Fidry\\AliceDataFixtures\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Théo FIDRY",
+ "email": "theo.fidry@gmail.com",
+ "homepage": "https://github.com/theofidry"
+ }
+ ],
+ "description": "Nelmio alice extension to persist the loaded fixtures.",
+ "keywords": [
+ "Fixture",
+ "alice",
+ "data",
+ "faker",
+ "orm",
+ "tests"
+ ],
+ "support": {
+ "issues": "https://github.com/theofidry/AliceDataFixtures/issues",
+ "source": "https://github.com/theofidry/AliceDataFixtures/tree/1.7.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/theofidry",
+ "type": "github"
+ }
+ ],
+ "time": "2024-07-05T21:18:40+00:00"
+ },
+ {
+ "name": "theseer/tokenizer",
+ "version": "1.3.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/theseer/tokenizer.git",
+ "reference": "b7489ce515e168639d17feec34b8847c326b0b3c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c",
+ "reference": "b7489ce515e168639d17feec34b8847c326b0b3c",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-tokenizer": "*",
+ "ext-xmlwriter": "*",
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
+ "support": {
+ "issues": "https://github.com/theseer/tokenizer/issues",
+ "source": "https://github.com/theseer/tokenizer/tree/1.3.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/theseer",
+ "type": "github"
+ }
+ ],
+ "time": "2025-11-17T20:03:58+00:00"
+ }
+ ],
+ "aliases": [],
+ "minimum-stability": "stable",
+ "stability-flags": {
+ "sylius/test-application": 15
+ },
+ "prefer-stable": true,
+ "prefer-lowest": false,
+ "platform": {
+ "php": "^8.2",
+ "ext-json": "*"
+ },
+ "platform-dev": {},
+ "plugin-api-version": "2.9.0"
+}
diff --git a/config/routing/admin.yaml b/config/routing/admin.yaml
index 4800b8e9..f25c4ed1 100644
--- a/config/routing/admin.yaml
+++ b/config/routing/admin.yaml
@@ -2,4 +2,20 @@ controllers:
resource:
path: '../../src/Action/Admin/'
namespace: 'PayPlug\SyliusPayPlugPlugin\Action\Admin'
- type: attribute
\ No newline at end of file
+ type: attribute
+# Declared here rather than with #[Route] attributes — see AuthorizedPaymentController's docblock.
+payplug_sylius_admin_payment_capture:
+ path: /payplug/orders/{orderId}/payments/{paymentId}/capture
+ methods: [POST]
+ controller: PayPlug\SyliusPayPlugPlugin\Action\Admin\AuthorizedPaymentController::capture
+ requirements:
+ orderId: \d+
+ paymentId: \d+
+
+payplug_sylius_admin_payment_cancel:
+ path: /payplug/orders/{orderId}/payments/{paymentId}/cancel
+ methods: [POST]
+ controller: PayPlug\SyliusPayPlugPlugin\Action\Admin\AuthorizedPaymentController::cancel
+ requirements:
+ orderId: \d+
+ paymentId: \d+
diff --git a/config/services.yaml b/config/services.yaml
index 7db6a5fe..328a2c40 100644
--- a/config/services.yaml
+++ b/config/services.yaml
@@ -1,3 +1,24 @@
+parameters:
+ # Overridable via env vars for QA/staging testing; merchants installing the plugin normally
+ # never need to set these — the defaults below are used automatically.
+ payplug.oauth_base_url.default: 'https://api.payplug.com'
+ payplug.oauth_base_url: '%env(default:payplug.oauth_base_url.default:PAYPLUG_OAUTH_BASE_URL)%'
+ payplug.oauth_audience.default: 'https://www.payplug.com'
+ payplug.oauth_audience: '%env(default:payplug.oauth_audience.default:PAYPLUG_OAUTH_AUDIENCE)%'
+ payplug.unified_api_base_url.default: 'https://api.payplug.com'
+ payplug.unified_api_base_url: '%env(default:payplug.unified_api_base_url.default:PAYPLUG_UNIFIED_API_BASE_URL)%'
+ # Escape hatch for QA/staging hosts whose TLS certificate is signed by an internal CA the
+ # merchant's PHP install has no way to obtain or trust. MUST default to true (verification on)
+ # and MUST NOT be disabled in production — this only exists so a QA/staging environment can
+ # explicitly opt out for its own known-internal host, not as a general "skip TLS" switch.
+ payplug.unified_api_verify_tls.default: true
+ payplug.unified_api_verify_tls: '%env(bool:default:payplug.unified_api_verify_tls.default:PAYPLUG_UNIFIED_API_VERIFY_TLS)%'
+ # Hosted Fields JS SDK loaded by the checkout: one full https URL, because the host, the path
+ # and the pinned version all differ per environment. Defaults to the production SDK; QA and
+ # staging setups set PAYPLUG_HOSTED_FIELDS_SDK_URL to their own SDK URL.
+ payplug.hosted_fields_sdk_url.default: 'https://js.dalenys.com/hosted-fields/v2.2.0/hosted-fields.min.js'
+ payplug.hosted_fields_sdk_url: '%env(default:payplug.hosted_fields_sdk_url.default:PAYPLUG_HOSTED_FIELDS_SDK_URL)%'
+
services:
_defaults:
autowire: true
@@ -9,6 +30,22 @@ services:
exclude: '../src/{ApiClient,DependencyInjection,Entity,Exception,Model,Repository,PayPlugSyliusPayPlugPlugin.php}'
bind:
Psr\Log\LoggerInterface: '@monolog.logger.payplug'
+ $payplugOauthBaseUrl: '%payplug.oauth_base_url%'
+ $payplugOauthAudience: '%payplug.oauth_audience%'
+ $unifiedApiBaseUrl: '%payplug.unified_api_base_url%'
+ $unifiedApiVerifyTls: '%payplug.unified_api_verify_tls%'
+ $hostedFieldsSdkUrl: '%payplug.hosted_fields_sdk_url%'
+
+ # `security.csrf.token_manager` is only registered while CSRF protection is enabled, so it is
+ # injected with `@?`: a hard reference would break container compilation for an application
+ # that turns it off, rather than degrading the way Sylius's own admin actions do.
+ PayPlug\SyliusPayPlugPlugin\Action\Admin\Auth\UnifiedLogoutController:
+ arguments:
+ $csrfTokenManager: '@?security.csrf.token_manager'
+
+ PayPlug\SyliusPayPlugPlugin\Action\Admin\AuthorizedPaymentController:
+ arguments:
+ $csrfTokenManager: '@?security.csrf.token_manager'
PayPlug\SyliusPayPlugPlugin\Repository\PaymentRepositoryInterface:
class: PayPlug\SyliusPayPlugPlugin\Repository\PaymentRepository
@@ -25,6 +62,47 @@ services:
PayPlug\SyliusPayPlugPlugin\Provider\OneySimulation\OneySimulationDataProviderInterface:
class: PayPlug\SyliusPayPlugPlugin\Provider\OneySimulation\OneySimulationDataProvider
+ # Alias (not a separate definition) so the interface resolves to the service auto-registered by the
+ # `PayPlug\SyliusPayPlugPlugin\:` prototype above, keeping its `@monolog.logger.payplug` binding.
+ PayPlug\SyliusPayPlugPlugin\PaymentProcessing\HostedFieldsPaymentProcessorInterface:
+ alias: PayPlug\SyliusPayPlugPlugin\PaymentProcessing\NullHostedFieldsPaymentProcessor
+
+ PayplugUnifiedCore\Contracts\ILogger:
+ alias: PayPlug\SyliusPayPlugPlugin\Upc\SyliusUpcLogger
+
+ PayplugUnifiedCore\Contracts\ILock:
+ alias: PayPlug\SyliusPayPlugPlugin\Upc\SyliusUpcLock
+
+ PayplugUnifiedCore\Contracts\IUnifiedApiHttpClient:
+ alias: PayPlug\SyliusPayPlugPlugin\Upc\SyliusUnifiedApiHttpClient
+
+ # Deliberately NOT aliased: PayplugUnifiedCore\Contracts\IConfigurationRepository carries no
+ # account context on any of its methods, and since PRE-3628 a merchant may hold several CB
+ # gateway configs — an unscoped SyliusUpcConfigurationRepository throws on every read. Aliasing
+ # it would let a future UPC consumer autowire a service that blows up at runtime, in whatever
+ # code path UPC happened to choose; without the alias it fails at container build instead.
+ # Consumers take ScopedConfigurationRepositoryInterface and scope it to their payment method.
+ PayPlug\SyliusPayPlugPlugin\Upc\ScopedConfigurationRepositoryInterface:
+ alias: PayPlug\SyliusPayPlugPlugin\Upc\SyliusUpcConfigurationRepository
+
+ PayplugUnifiedCore\Contracts\IPaymentRepository:
+ alias: PayPlug\SyliusPayPlugPlugin\Upc\SyliusPaymentOperationRepository
+
+ PayplugUnifiedCore\Contracts\IOrderStateMutator:
+ alias: PayPlug\SyliusPayPlugPlugin\Upc\SyliusOrderStateMutator
+
+ PayPlug\SyliusPayPlugPlugin\Upc\UnifiedApiPaymentCreatorInterface:
+ alias: PayPlug\SyliusPayPlugPlugin\Upc\UnifiedApiPaymentCreator
+
+ PayPlug\SyliusPayPlugPlugin\Upc\OperationStatusFetcherInterface:
+ alias: PayPlug\SyliusPayPlugPlugin\Upc\UnifiedApiOperationStatusFetcher
+
+ PayPlug\SyliusPayPlugPlugin\Upc\RefundCreatorInterface:
+ alias: PayPlug\SyliusPayPlugPlugin\Upc\UnifiedApiRefundCreator
+
+ PayPlug\SyliusPayPlugPlugin\Upc\AuthorizationOperatorInterface:
+ alias: PayPlug\SyliusPayPlugPlugin\Upc\UnifiedApiAuthorizationOperator
+
payplug_sylius_payplug_plugin.action.capture:
class: PayPlug\SyliusPayPlugPlugin\Action\CaptureAction
diff --git a/config/services/client.xml b/config/services/client.xml
index 3f2d3391..73148c93 100644
--- a/config/services/client.xml
+++ b/config/services/client.xml
@@ -10,6 +10,25 @@
+
+
+
+
+
+
+
+ %payplug.oauth_base_url%
+
+
+ %payplug.oauth_audience%
+
+
+
+
[!IMPORTANT]
> The authorized payment feature is only available for the "PayPlug" payment gateway.
+Two flows exist, depending on the payment method's display mode:
+
+- **Hosted Fields** (Unified API): capture — full, partial, several times — and cancellation are
+ driven from the admin order screen. See [Hosted Fields: capture and cancel from the order screen](#hosted-fields-capture-and-cancel-from-the-order-screen).
+- **Redirected / Integrated Payment** (legacy API): the whole authorized amount is captured at
+ once, by the command or the state machine triggers described in [Trigger the capture](#trigger-the-capture).
+
## Activation
On the payment method configuration, you can enable the deferred capture feature.
@@ -153,3 +160,91 @@ final class CaptureOrderProcessor
}
}
```
+
+## Hosted Fields: capture and cancel from the order screen
+
+With **deferred capture** enabled on a Hosted Fields payment method, each payment is created as an
+authorization only: the customer's funds are held, not debited, and the Sylius payment is
+`authorized` (the order payment state reads *authorized*, not *paid*).
+
+On the order screen (**Sales › Orders › an order**), a **PayPlug — Deferred capture** block is shown
+under *Payments* for each such payment. It displays:
+
+| Field | Meaning |
+|---|---|
+| Authorized amount | What the customer's bank agreed to hold |
+| Already captured | Sum of the captures performed so far |
+| Cancelled | Sum of the cancellations performed so far (shown once there is one) |
+| Remaining capturable | What can still be captured or cancelled |
+| Capture deadline | The date after which the authorization lapses and the funds are released. A warning is shown 48 hours before it; after it, no action is offered any more |
+
+### Capture
+
+Leave the amount blank to capture everything that remains, or type an amount (`12.50` or `12,50`)
+for a partial capture. Captures can be chained as long as their total stays within the authorized
+amount.
+
+| After the capture | Sylius payment state |
+|---|---|
+| Something is still capturable | stays `authorized` |
+| Nothing is left | `completed` (the order becomes *paid*) |
+
+Sylius's own **Complete** button is hidden for these payments. A `complete` transition applied by
+any other means — the `payplug:capture-authorized-payments` command, or your own shipping listener
+as described above — captures the whole remaining amount first; if that capture is refused, the
+transition is aborted and the payment stays `authorized`.
+
+### Cancel
+
+Cancellation is only offered **before any capture**. Leave the amount blank to cancel everything,
+or type an amount for a partial cancellation — which must be enabled on the merchant's PayPlug
+contract; otherwise PayPlug refuses it and the admin explains why.
+
+| After the cancellation | Sylius payment state |
+|---|---|
+| Something is still authorized | stays `authorized` (the rest can still be captured) |
+| Nothing is left | `cancelled` |
+
+Once part of the authorization is captured, use a refund to give money back.
+
+### Refusals
+
+When PayPlug refuses an operation, nothing is recorded and the payment state is left unchanged; the
+admin shows an explicit message, e.g.:
+
+- the authorization has expired;
+- the amount exceeds what remains;
+- partial cancellation is not enabled on the contract;
+- the customer's bank refused the operation;
+- another operation is already in progress on this payment.
+
+A form submitted twice (double click, browser back + resubmit) is refused as *stale*: each form
+carries the state it was built against, and operations on a same payment are serialized. The
+operation is saved before that serialization lock is released, so a second request can never see
+the state from before it.
+
+If PayPlug accepted the operation but Sylius failed to save it (database error), the admin says the
+operation **may have been performed** and asks to check the payment in the PayPlug portal before
+retrying, and a `critical` entry is written to the log. Retrying blindly would capture twice.
+
+### Partial operations and Sylius refunds
+
+Sylius has no notion of a partial capture or a partial cancellation, so the plugin does not try to
+bend it: `Payment::amount` stays at the authorized amount, and Sylius's order payment state and
+RefundPlugin's refundable total are left untouched. What was really captured is tracked in the
+payment details; refunds sent to PayPlug are computed from it. After a partial cancellation followed
+by a capture of the rest, the order therefore reads as paid for the authorized amount, while the
+captured amount is the smaller one shown in the block on the order screen.
+
+### Notifications and operations performed outside Sylius
+
+The webhook confirming each capture or cancellation is matched to the operation Sylius triggered.
+If PayPlug later reports that an operation accepted earlier did **not** go through, it is flagged
+as failed in the payment details (the remaining amount counts it back in) and a `critical` entry is
+written to the `payplug` log channel so that the payment can be reconciled manually.
+
+> [!WARNING]
+> Captures and cancellations performed **outside Sylius** (PayPlug portal, PayPlug support, another
+> integration using the same account) are not reflected in Sylius: they carry operation ids Sylius
+> does not know. Perform them from the Sylius order screen so that the payment state stays in step.
+
diff --git a/features/shop/hosted_fields_payment_and_webhook_flow.feature b/features/shop/hosted_fields_payment_and_webhook_flow.feature
new file mode 100644
index 00000000..5cb81677
--- /dev/null
+++ b/features/shop/hosted_fields_payment_and_webhook_flow.feature
@@ -0,0 +1,80 @@
+# PRE-3551 / Task 14 — end-to-end coverage for the payplug_uhf (Unified Hosted Fields) capture +
+# UPC webhook pipeline built by Tasks 1-13.
+#
+# IMPORTANT — read before relying on this file:
+#
+# This file is WRITTEN BUT NOT EXECUTABLE YET. Every step in the Background and in the opening
+# "Given"/"When"/"Then" lines that also appear in features/shop/hosted_fields_payment_method.feature
+# or features/shop/update_payment_state.feature is backed by a real, already-merged step
+# definition and is safe to trust. Steps marked "# NEW — not yet defined" below do not resolve to
+# any step definition in this codebase today; Behat would report them as "undefined", not pass or
+# fail. See tests-Behat/task-14-report.md (PRE-3551 SDD folder) for why, and for a sketch of the
+# infrastructure (Mocker + a raw-HTTP webhook page object + a PaymentRequest fixture step) that
+# would need to be designed and built — not guessed — before these two scenarios can actually run.
+@paying_with_payplug_for_order
+Feature: Paying with Unified Hosted Fields and receiving the UPC webhook confirmation
+ In order to buy products with a card processed through PayPlug's Unified API
+ As a Customer
+ I want my order to be confirmed once the asynchronous UPC webhook notification arrives
+
+ Background:
+ Given the store operates on a single channel in "United States"
+ And that channel also allows to shop using the "EUR" currency
+ And there is a user "john@bitbag.pl" identified by "password123"
+ And I changed my currency to "EUR"
+ And the store has a payment method "Unified Hosted Fields" with a code "payplug_uhf" and PayPlug Hosted Fields payment gateway
+ And This secret Key is valid
+ And the store ships everywhere for free
+ And the store has "DHL" shipping method with "$0.00" fee
+ And I am logged in as "john@bitbag.pl"
+ And the store has a product "PHP T-Shirt" priced at "€50.00"
+
+ @ui
+ Scenario: Paying with Unified Hosted Fields completes the order without a 3DS redirect
+ Given I added product "PHP T-Shirt" to the cart
+ And I chose "DHL" shipping method
+ Then I should be on the checkout payment step
+ And I should be able to select "Unified Hosted Fields" payment method
+ And I select "Unified Hosted Fields" payment method
+ And I should see the "#card-container" element on the page
+ # NEW — not yet defined. Needs: (1) HostedPaymentCreatorInterface made a public service
+ # (currently private by DI default) so a Behat Mocker can swap it, mirroring
+ # tests/Behat/Mocker/PayPlugApiMocker's pattern for the legacy PayPlugApiClientInterface;
+ # (2) a new UhfApiMocker::mockCreateHostedPaymentWithoutRedirect() stubbing
+ # createHostedPayment() to return a HostedPaymentOutput with redirectUrl = null.
+ And the Unified API is stubbed to accept the payment without requiring 3DS
+ # NEW — not yet defined, and not just missing a step: card entry goes through a real,
+ # cross-origin Dalenys hostedFields iframe SDK (assets/shop/controllers/hosted-fields_controller.js)
+ # loaded from PayPlug's own domain. There is no server-side seam to stub this — Behat would
+ # need either a live sandbox tokenization endpoint or a test-only shim replacing
+ # window.dalenys before the page loads. Deciding whether to build that shim is a product/
+ # architecture call, not a step-definition gap.
+ When I fill in my card details and confirm the payment
+ Then I should see the thank you page
+ # NEW — not yet defined. "Eventually" implies polling/waiting for an async webhook POST
+ # that nothing in this suite currently sends (see Scenario 2's blockers below).
+ And the order's payment should eventually be marked as paid once the webhook is delivered
+
+ @ui
+ Scenario: A retried UPC webhook notification is not processed twice
+ # NEW — not yet defined. Needs a fixture step that creates an Order + Payment already
+ # past the capture step (hosted_fields_created_at set, Payment state "processing") plus a
+ # real Sylius PaymentRequestInterface (action=notify, hash known) so the scenario has a
+ # real notify URL to POST to — sylius/sylius's PaymentRequest entity/repository isn't
+ # available in this checkout's vendor/ to confirm the exact construction API, so writing
+ # this without running it would be guessing at Sylius core internals, not this plugin's.
+ Given an order paid with "Unified Hosted Fields" is awaiting webhook confirmation
+ # NEW — not yet defined. Needs a Page object that performs a raw HTTP POST (via the Mink
+ # BrowserKit driver's underlying client) to the sylius_payment_request_notify route for
+ # that PaymentRequest's hash, with body {"id","execCode","orderId","amount"} and an
+ # Authorization header matching whatever the "payplug_webhook_authorization_header" gateway
+ # config key was seeded to in the Given step above (see
+ # src/Upc/SyliusUpcConfigurationRepository::get() and
+ # src/Command/Handler/NotifyHostedPaymentRequestHandler — confirmed by
+ # tests/PHPUnit/Command/Handler/NotifyHostedPaymentRequestHandlerTest, which is the most
+ # reliable reference for the exact payload/header shape).
+ When the UPC webhook notification is delivered
+ And the same UPC webhook notification is delivered again
+ # NEW — not yet defined. No existing step asserts Payment state (only order state, via
+ # CheckoutContext::theLatestOrderHasState) or asserts a call count on IPaymentRepository.
+ Then the order's payment should be marked as paid exactly once
diff --git a/features/shop/hosted_fields_payment_method.feature b/features/shop/hosted_fields_payment_method.feature
new file mode 100644
index 00000000..7f22142b
--- /dev/null
+++ b/features/shop/hosted_fields_payment_method.feature
@@ -0,0 +1,26 @@
+@paying_with_payplug_for_order
+Feature: Paying with Hosted Fields during checkout
+ In order to buy products
+ As a Customer
+ I want to see Hosted Fields when the merchant enabled it on the PayPlug payment method
+
+ Background:
+ Given the store operates on a single channel in "United States"
+ And that channel also allows to shop using the "EUR" currency
+ And there is a user "john@bitbag.pl" identified by "password123"
+ And I changed my currency to "EUR"
+ And the store has a payment method "PayPlug" with a code "payplug" and PayPlug Hosted Fields payment gateway
+ And This secret Key is valid
+ And the store ships everywhere for free
+ And the store has "DHL" shipping method with "$0.00" fee
+ And I am logged in as "john@bitbag.pl"
+
+ @ui
+ Scenario: I can see and select the PayPlug payment method with Hosted Fields enabled
+ Given the store has a product "PHP T-Shirt" priced at "€50.00"
+ And I added product "PHP T-Shirt" to the cart
+ And I chose "DHL" shipping method
+ Then I should be on the checkout payment step
+ And I should be able to select "PayPlug" payment method
+ And I select "PayPlug" payment method
+ And I should see the "#card-container" element on the page
diff --git a/grumphp.yml b/grumphp.yml
index e8ea3a8c..ae81bef2 100644
--- a/grumphp.yml
+++ b/grumphp.yml
@@ -21,7 +21,10 @@ grumphp:
configuration: 'ruleset/phpstan.neon'
use_grumphp_paths: false
memory_limit: '-1'
- securitychecker_symfony: ~
+ securitychecker_composeraudit:
+ # Abandoned packages are reported, not fatal: the offenders are transitive
+ # Sylius/Behat deps this plugin cannot replace. Advisories still fail the task.
+ abandoned: report
yamllint:
parse_custom_tags: true
ecs:
diff --git a/migrations/Version20260810120000.php b/migrations/Version20260810120000.php
new file mode 100644
index 00000000..3da541cb
--- /dev/null
+++ b/migrations/Version20260810120000.php
@@ -0,0 +1,43 @@
+addSql('CREATE TABLE payplug_upc_operation (
+ id INT AUTO_INCREMENT NOT NULL,
+ order_id VARCHAR(255) NOT NULL,
+ operation_id VARCHAR(255) NOT NULL,
+ exec_code VARCHAR(255) NOT NULL,
+ outcome VARCHAR(255) NOT NULL,
+ amount INT NOT NULL,
+ treated TINYINT(1) NOT NULL,
+ created_at DATETIME NOT NULL,
+ UNIQUE INDEX UNIQ_payplug_upc_operation_operation_id (operation_id),
+ INDEX idx_payplug_upc_operation_order_id (order_id),
+ PRIMARY KEY(id)
+ ) DEFAULT CHARACTER SET UTF8 COLLATE `UTF8_unicode_ci` ENGINE = InnoDB');
+ }
+
+ public function down(Schema $schema): void
+ {
+ // this down() migration is auto-generated, please modify it to your needs
+ $this->addSql('DROP TABLE payplug_upc_operation');
+ }
+}
diff --git a/migrations/Version20260901120000.php b/migrations/Version20260901120000.php
new file mode 100644
index 00000000..65336e3c
--- /dev/null
+++ b/migrations/Version20260901120000.php
@@ -0,0 +1,37 @@
+addSql('DELETE t1 FROM payplug_cards t1 INNER JOIN payplug_cards t2 ON t1.external_id = t2.external_id AND t1.is_live = t2.is_live AND t1.id > t2.id');
+ $this->addSql('CREATE UNIQUE INDEX UNIQ_payplug_cards_external_id_is_live ON payplug_cards (external_id, is_live)');
+ }
+
+ public function down(Schema $schema): void
+ {
+ $this->addSql('DROP INDEX UNIQ_payplug_cards_external_id_is_live ON payplug_cards');
+ }
+}
diff --git a/migrations/Version20260907140000.php b/migrations/Version20260907140000.php
new file mode 100644
index 00000000..f7e37ce8
--- /dev/null
+++ b/migrations/Version20260907140000.php
@@ -0,0 +1,47 @@
+addSql(<<<'SQL'
+ UPDATE sylius_gateway_config
+ SET config = JSON_REMOVE(config, '$.hfSubMerchantId')
+ WHERE factory_name = 'payplug'
+ AND JSON_CONTAINS_PATH(config, 'one', '$.hfSubMerchantId')
+ SQL);
+ }
+
+ public function down(Schema $schema): void
+ {
+ // Deliberately not reversible: the removed value is a credential this migration does not
+ // retain, and no code version reads the key any more. Rolling the plugin back past
+ // PRE-3646 means re-entering it in the admin form, which is also the only way to obtain it.
+ }
+}
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index b01264ac..89387765 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -18,4 +18,10 @@
+
+
+
+ src
+
+
diff --git a/ruleset/phpstan-baseline.neon b/ruleset/phpstan-baseline.neon
index abca980f..76179e7d 100644
--- a/ruleset/phpstan-baseline.neon
+++ b/ruleset/phpstan-baseline.neon
@@ -665,14 +665,8 @@ parameters:
path: ../src/Gateway/AbstractGatewayFactory.php
-
- message: '#^Cannot call method add\(\) on mixed\.$#'
- identifier: method.nonObject
- count: 1
- path: ../src/Gateway/Form/Type/AbstractGatewayConfigurationType.php
-
- -
- message: '#^Cannot call method getId\(\) on mixed\.$#'
- identifier: method.nonObject
+ message: '#^PHPDoc tag @SuppressWarnings has invalid value \(\(PHPMD\.UnusedFormalParameter\)\)\: Unexpected token "\.UnusedFormalParameter\)", expected ''\)'' at offset 554 on line 11$#'
+ identifier: phpDoc.parseError
count: 1
path: ../src/Gateway/Form/Type/AbstractGatewayConfigurationType.php
@@ -1252,18 +1246,6 @@ parameters:
count: 1
path: ../src/Provider/PaymentTokenProvider.php
- -
- message: '#^Cannot access offset ''max_amount'' on mixed\.$#'
- identifier: offsetAccess.nonOffsetAccessible
- count: 1
- path: ../src/Provider/SupportedMethodsProvider.php
-
- -
- message: '#^Cannot access offset ''min_amount'' on mixed\.$#'
- identifier: offsetAccess.nonOffsetAccessible
- count: 1
- path: ../src/Provider/SupportedMethodsProvider.php
-
-
message: '#^PHPDoc tag @var with type Payum\\Core\\Model\\GatewayConfigInterface is not subtype of native type Sylius\\Component\\Payment\\Model\\GatewayConfigInterface\|null\.$#'
identifier: varTag.nativeType
diff --git a/src/Action/Admin/Auth/UnifiedAuthenticationController.php b/src/Action/Admin/Auth/UnifiedAuthenticationController.php
index ef8efcc0..43bd0b2a 100644
--- a/src/Action/Admin/Auth/UnifiedAuthenticationController.php
+++ b/src/Action/Admin/Auth/UnifiedAuthenticationController.php
@@ -7,7 +7,10 @@
use Doctrine\ORM\EntityManagerInterface;
use Payplug\Authentication;
use Payplug\Payplug;
+use PayPlug\SyliusPayPlugPlugin\Auth\IdTokenEmailExtractor;
use PayPlug\SyliusPayPlugPlugin\Validator\PaymentMethodValidator;
+use PayplugUnifiedCore\Auth\OAuth2Client;
+use PayplugUnifiedCore\Contracts\IOAuthHttpClient;
use Psr\Log\LoggerInterface;
use Sylius\Resource\Doctrine\Persistence\RepositoryInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@@ -18,7 +21,6 @@
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\RouterInterface;
-use Symfony\Contracts\Cache\CacheInterface;
/**
* This controller is used to authenticate the user with PayPlug
@@ -30,6 +32,9 @@
#[Route('/payplug/auth')]
final class UnifiedAuthenticationController extends AbstractController
{
+ // Matches the scope legacy Authentication::initiateOAuth() has always requested.
+ private const PKCE_SCOPE = 'openid offline profile email';
+
/**
* @param RepositoryInterface<\Sylius\Component\Core\Model\PaymentMethod> $paymentMethodRepository
*/
@@ -39,36 +44,34 @@ public function __construct(
private EntityManagerInterface $entityManager,
private PaymentMethodValidator $paymentMethodValidator,
private LoggerInterface $logger,
- private CacheInterface $cache,
+ private IOAuthHttpClient $oauthHttpClient,
+ private IdTokenEmailExtractor $idTokenEmailExtractor,
+ private string $payplugOauthBaseUrl,
+ private string $payplugOauthAudience,
) {
}
+ private function buildOAuth2Client(string $redirectUri): OAuth2Client
+ {
+ return new OAuth2Client($this->oauthHttpClient, $this->payplugOauthBaseUrl, $redirectUri, self::PKCE_SCOPE, $this->payplugOauthAudience);
+ }
+
#[Route('/setup-redirection', name: 'payplug_sylius_admin_auth_setup_redirection')]
public function setupRedirection(Request $request): Response
{
try {
- $clientId = $request->query->get('client_id');
- $companyId = $request->query->get('company_id');
+ $clientId = $request->query->getString('client_id');
+ $companyId = $request->query->getString('company_id');
$request->getSession()->set('payplug_client_id', $clientId);
$request->getSession()->set('payplug_company_id', $companyId);
- $challenge = bin2hex(openssl_random_pseudo_bytes(50));
- $request->getSession()->set('payplug_oauth_challenge', $challenge);
-
$callBackUrl = $this->router->generate('payplug_sylius_admin_auth_oauth_callback', [], RouterInterface::ABSOLUTE_URL);
+ $authorizationRequest = $this->buildOAuth2Client($callBackUrl)->buildAuthorizationUrl($clientId);
+ $request->getSession()->set('payplug_oauth_state', $authorizationRequest->state);
+ $request->getSession()->set('payplug_oauth_code_verifier', $authorizationRequest->codeVerifier);
- // This method will redirect the user to PayPlug's oauth page via header('Location')'
- Authentication::initiateOAuth($clientId, $callBackUrl, $challenge);
- // Fetch the header Location the Sdk put and redirect the user to it
- $headers = \headers_list();
- foreach ($headers as $header) {
- if (str_starts_with($header, 'Location:')) {
- return new RedirectResponse(substr($header, 9));
- }
- }
-
- throw new \LogicException('No location header found');
+ return new RedirectResponse($authorizationRequest->url);
} catch (\Throwable $e) {
$this->logger->critical('Error while perform Payplug OAuth Setup redirection', ['message' => $e->getMessage(), 'exception' => $e]);
@@ -81,16 +84,27 @@ public function oauthCallback(Request $request): Response
{
try {
$code = $request->query->getString('code');
- /** @var string $clientId */
+ $state = $request->query->getString('state');
$clientId = $request->getSession()->get('payplug_client_id');
- /** @var string $challenge */
- $challenge = $request->getSession()->get('payplug_oauth_challenge');
- $callback = $this->generateUrl('payplug_sylius_admin_auth_oauth_callback', [], UrlGeneratorInterface::ABSOLUTE_URL);
+ /** @var string $expectedState */
+ $expectedState = $request->getSession()->get('payplug_oauth_state');
+ $codeVerifier = $request->getSession()->get('payplug_oauth_code_verifier');
- $jwt = Authentication::generateJWTOneShot($code, $callback, $clientId, $challenge);
- if ([] === $jwt || $jwt['httpStatus'] !== 200 || !\is_array($jwt['httpResponse'])) {
- throw new BadRequestHttpException('Error while generating JWT');
+ if ('' === $state || $state !== $expectedState) {
+ throw new BadRequestHttpException('OAuth state mismatch');
}
+
+ if (!\is_string($clientId) || '' === $clientId) {
+ throw new BadRequestHttpException('OAuth client id missing from session');
+ }
+
+ if (!\is_string($codeVerifier) || '' === $codeVerifier) {
+ throw new BadRequestHttpException('OAuth code verifier missing from session');
+ }
+
+ $callback = $this->generateUrl('payplug_sylius_admin_auth_oauth_callback', [], UrlGeneratorInterface::ABSOLUTE_URL);
+ $token = $this->buildOAuth2Client($callback)->exchangeAuthorizationCode($clientId, $code, $codeVerifier);
+
$paymentMethodId = $request->getSession()->get('payplug_sylius_oauth_payment_method_id');
if (null === $paymentMethodId) {
throw new BadRequestHttpException('No payment method id found in session');
@@ -105,7 +119,7 @@ public function oauthCallback(Request $request): Response
}
$companyId = $request->getSession()->get('payplug_company_id');
- Payplug::init(['secretKey' => $jwt['httpResponse']['access_token']]);
+ Payplug::init(['secretKey' => $token->accessToken]);
$clientName = 'Sylius - ' . $paymentMethod->getName();
$testClientDataResult = Authentication::createClientIdAndSecret($companyId, $clientName, 'test');
$liveClientDataResult = Authentication::createClientIdAndSecret($companyId, $clientName, 'live');
@@ -113,18 +127,19 @@ public function oauthCallback(Request $request): Response
$config = $gatewayConfig->getConfig();
$config['live_client'] = $liveClientDataResult['httpResponse'] ?? null;
$config['test_client'] = $testClientDataResult['httpResponse'] ?? null;
+ // Who just authorized, for the admin screen to show. The id_token is the only carrier
+ // of that identity — /account has no email field and the client-credentials token used
+ // for every later API call names no user — and it is discarded with $token when this
+ // method returns, so it is captured here or not at all. Overwritten unconditionally,
+ // including with null: after a re-auth against a different PayPlug account, keeping the
+ // previous address would misreport which account is taking the money.
+ $config['account_email'] = $this->idTokenEmailExtractor->extract($token->idToken);
$gatewayConfig->setConfig($config);
$this->entityManager->flush();
$this->cleanSession($request);
$request->getSession()->getFlashBag()->add('success', 'payplug_sylius_payplug_plugin.admin.oauth_callback_success');
- // Clean previous cached client config
- $cacheKeyLive = sprintf('payplug_%s_api_key_live', $gatewayConfig->getFactoryName());
- $cacheKeyTest = sprintf('payplug_%s_api_key_test', $gatewayConfig->getFactoryName());
- $this->cache->delete($cacheKeyLive);
- $this->cache->delete($cacheKeyTest);
-
// Ensure that the payment method is well configured
$this->paymentMethodValidator->process($paymentMethod);
@@ -152,7 +167,8 @@ private function cleanSession(Request $request): void
$session = $request->getSession();
$session->remove('payplug_client_id');
$session->remove('payplug_company_id');
- $session->remove('payplug_oauth_challenge');
+ $session->remove('payplug_oauth_state');
+ $session->remove('payplug_oauth_code_verifier');
$session->remove('payplug_sylius_oauth_payment_method_id');
}
}
diff --git a/src/Action/Admin/Auth/UnifiedLogoutController.php b/src/Action/Admin/Auth/UnifiedLogoutController.php
new file mode 100644
index 00000000..8c66f1cb
--- /dev/null
+++ b/src/Action/Admin/Auth/UnifiedLogoutController.php
@@ -0,0 +1,126 @@
+ would be invalid HTML. The CSRF token is therefore carried in the query string and
+ * checked here — it is the only thing between a crafted link and a merchant losing a connection.
+ */
+#[Route('/payplug/auth')]
+final class UnifiedLogoutController extends AbstractController
+{
+ /**
+ * Per-payment-method so a token minted for one gateway cannot be replayed against another.
+ * Public because the template that renders the button mints the token with it.
+ */
+ public const CSRF_TOKEN_ID_PREFIX = 'payplug_logout_';
+
+ /**
+ * @param RepositoryInterface<\Sylius\Component\Core\Model\PaymentMethod> $paymentMethodRepository
+ */
+ public function __construct(
+ private RouterInterface $router,
+ private RepositoryInterface $paymentMethodRepository,
+ private GatewayConnectionRevoker $connectionRevoker,
+ private ?CsrfTokenManagerInterface $csrfTokenManager,
+ private LoggerInterface $logger,
+ ) {
+ }
+
+ #[Route('/logout/{id}', name: 'payplug_sylius_admin_auth_logout', requirements: ['id' => '\d+'], methods: ['GET'])]
+ public function logout(Request $request, int $id): Response
+ {
+ $this->denyUnlessCsrfTokenIsValid($request, $id);
+ $paymentMethod = $this->findConnectedPayPlugPaymentMethod($id);
+
+ try {
+ $this->connectionRevoker->revoke($paymentMethod);
+ $this->addFlashMessage($request, 'success', 'payplug_sylius_payplug_plugin.admin.logout_success');
+ } catch (\Throwable $e) {
+ $this->logger->critical('Error while logging out the Payplug gateway', ['message' => $e->getMessage(), 'exception' => $e]);
+ $this->addFlashMessage($request, 'error', 'payplug_sylius_payplug_plugin.admin.logout_error');
+ }
+
+ return new RedirectResponse($this->router->generate('sylius_admin_payment_method_update', ['id' => $id]));
+ }
+
+ /**
+ * SessionInterface makes no promise about flashes — only FlashBagAwareSessionInterface does —
+ * and the parent's addFlash() would reach for the container instead of the request in hand.
+ * A non-flash-aware session means a non-web context, where the message has no one to reach
+ * anyway, so it is dropped rather than raised.
+ */
+ private function addFlashMessage(Request $request, string $type, string $message): void
+ {
+ $session = $request->getSession();
+
+ if ($session instanceof FlashBagAwareSessionInterface) {
+ $session->getFlashBag()->add($type, $message);
+ }
+ }
+
+ /**
+ * A null token manager means the application runs with CSRF protection off — the service only
+ * exists while it is on, hence the optional injection in config/services.yaml. The check is
+ * then skipped rather than failed, matching Sylius's own admin actions that carry a token in
+ * the query string and guard it with sylius_csrf_protection_enabled().
+ */
+ private function denyUnlessCsrfTokenIsValid(Request $request, int $id): void
+ {
+ if (null === $this->csrfTokenManager) {
+ return;
+ }
+
+ $token = new CsrfToken(self::CSRF_TOKEN_ID_PREFIX . $id, $request->query->getString('_csrf_token'));
+
+ if (!$this->csrfTokenManager->isTokenValid($token)) {
+ throw new BadRequestHttpException('Invalid CSRF token for the PayPlug logout request.');
+ }
+ }
+
+ /**
+ * 404 rather than a flash for both misses: the only legitimate source of this link is the
+ * connected-account block on a PayPlug gateway's own update screen, so an id that names
+ * nothing — or names another provider's payment method, which logout must never disable —
+ * is a tampered or stale URL, not a merchant mistake worth explaining.
+ */
+ private function findConnectedPayPlugPaymentMethod(int $id): PaymentMethodInterface
+ {
+ $paymentMethod = $this->paymentMethodRepository->find($id);
+
+ if (
+ !$paymentMethod instanceof PaymentMethodInterface ||
+ !str_contains((string) $paymentMethod->getGatewayConfig()?->getFactoryName(), 'payplug')
+ ) {
+ throw new NotFoundHttpException(sprintf('No PayPlug payment method found with id %d.', $id));
+ }
+
+ return $paymentMethod;
+ }
+}
diff --git a/src/Action/Admin/AuthorizedPaymentController.php b/src/Action/Admin/AuthorizedPaymentController.php
new file mode 100644
index 00000000..4714c713
--- /dev/null
+++ b/src/Action/Admin/AuthorizedPaymentController.php
@@ -0,0 +1,222 @@
+handle($request, $orderId, $paymentId, 'capture', function (PaymentInterface $payment, ?int $amount, int $version): void {
+ $this->processor->capture($payment, $amount, $version);
+ });
+ }
+
+ public function cancel(Request $request, int $orderId, int $paymentId): Response
+ {
+ return $this->handle($request, $orderId, $paymentId, 'cancel', function (PaymentInterface $payment, ?int $amount, int $version): void {
+ $this->processor->cancel($payment, $amount, $version);
+ });
+ }
+
+ private function handle(
+ Request $request,
+ int $orderId,
+ int $paymentId,
+ string $action,
+ \Closure $operation,
+ ): Response {
+ if (!$this->authorizationChecker->isGranted(self::ADMIN_ROLE)) {
+ throw new AccessDeniedHttpException('Only an administrator may capture or cancel a payment.');
+ }
+
+ $this->denyUnlessCsrfTokenIsValid($request, $paymentId);
+ $payment = $this->findOrderPayment($orderId, $paymentId);
+ // Outside the try below: a malformed request is a 400, not something to swallow as an
+ // unexpected error.
+ $version = self::parseVersion($request);
+
+ try {
+ $amount = $this->parseAmountFor($payment, $request->request->getString('amount'), $action);
+ // The processor flushes while it still holds its lock, see there.
+ $operation($payment, $amount, $version);
+ $this->addFlashMessage($request, 'success', self::FLASH_PREFIX . $action . '_success');
+ } catch (AuthorizationOperationException $exception) {
+ $this->addFlashMessage($request, 'error', $exception->getTranslationKey(), $exception->getTranslationParameters());
+ } catch (\Throwable $exception) {
+ // Anything but a refusal: typically the flush failing after the Unified API accepted the
+ // operation. The money may have moved without Sylius recording it, so the merchant is
+ // told to check at PayPlug before retrying (a retry would capture a second time) and
+ // a human is alerted.
+ $this->logger->critical('[PayPlug][UPC] Unexpected error during an authorization operation.', [
+ 'sylius_payment_id' => $paymentId,
+ 'action' => $action,
+ 'exception' => $exception,
+ ]);
+ $this->addFlashMessage($request, 'error', self::FLASH_PREFIX . 'error.unexpected_error');
+ }
+
+ return new RedirectResponse($this->router->generate('sylius_admin_order_show', ['id' => $orderId]));
+ }
+
+ /**
+ * Blank means "the whole remaining amount". Accepts a decimal comma as well as a dot, since
+ * that is what a French-speaking merchant types; anything else malformed — or longer than nine
+ * digits before the decimal point, which no real capture reaches and which would overflow —
+ * is sent on as an invalid amount (0) for the processor to refuse with its own explicit
+ * message.
+ *
+ * @throws AuthorizationOperationException when an amount is typed in a currency that
+ * AuthorizedPaymentOperationProcessor::supportsTypedAmounts() refuses
+ */
+ public static function parseAmount(string $raw, string $currencyCode): ?int
+ {
+ $raw = \str_replace([' ', "\u{00A0}", ','], ['', '', '.'], \trim($raw));
+ if ('' === $raw) {
+ return null;
+ }
+
+ if (1 !== \preg_match('/^\d{1,9}(?:\.\d{1,2})?$/', $raw)) {
+ return 0;
+ }
+
+ if (!AuthorizedPaymentOperationProcessor::supportsTypedAmounts($currencyCode)) {
+ throw new AuthorizationOperationException(self::FLASH_PREFIX . 'error.unsupported_currency', ['%currency%' => $currencyCode]);
+ }
+
+ return AmountHelper::toCents((float) $raw, $currencyCode);
+ }
+
+ /**
+ * @throws AuthorizationOperationException
+ */
+ private function parseAmountFor(PaymentInterface $payment, string $raw, string $action): ?int
+ {
+ $currencyCode = (string) $payment->getCurrencyCode();
+
+ try {
+ return self::parseAmount($raw, $currencyCode);
+ } catch (AuthorizationOperationException $exception) {
+ $this->logger->warning('[PayPlug][UPC] Typed amount refused: its currency cannot be converted to the stored amounts.', [
+ 'sylius_payment_id' => $payment->getId(),
+ 'action' => $action,
+ 'currency' => $currencyCode,
+ ]);
+
+ throw $exception;
+ }
+ }
+
+ /**
+ * The AuthorizationDetails::version() the form was built against. Mandatory here: it is the
+ * replay guard, and a hand-crafted POST must not be able to skip it.
+ */
+ private static function parseVersion(Request $request): int
+ {
+ $version = $request->request->getString('version');
+
+ if (1 !== \preg_match('/^\d{1,9}$/', $version)) {
+ throw new BadRequestHttpException('Missing or invalid version for the PayPlug authorization request.');
+ }
+
+ return (int) $version;
+ }
+
+ /**
+ * @param array $parameters
+ */
+ private function addFlashMessage(Request $request, string $type, string $message, array $parameters = []): void
+ {
+ $session = $request->getSession();
+
+ if ($session instanceof FlashBagAwareSessionInterface) {
+ // Sylius's admin flash template translates a ['message', 'parameters'] pair itself.
+ $session->getFlashBag()->add($type, [] === $parameters ? $message : ['message' => $message, 'parameters' => $parameters]);
+ }
+ }
+
+ /**
+ * See UnifiedLogoutController::denyUnlessCsrfTokenIsValid() for why a null token manager
+ * skips the check rather than failing it.
+ */
+ private function denyUnlessCsrfTokenIsValid(Request $request, int $paymentId): void
+ {
+ if (null === $this->csrfTokenManager) {
+ return;
+ }
+
+ $token = new CsrfToken(self::CSRF_TOKEN_ID_PREFIX . $paymentId, $request->request->getString('_csrf_token'));
+
+ if (!$this->csrfTokenManager->isTokenValid($token)) {
+ throw new BadRequestHttpException('Invalid CSRF token for the PayPlug authorization request.');
+ }
+ }
+
+ private function findOrderPayment(int $orderId, int $paymentId): PaymentInterface
+ {
+ $payment = $this->paymentRepository->find($paymentId);
+
+ if (!$payment instanceof PaymentInterface || $payment->getOrder()?->getId() !== $orderId) {
+ throw new NotFoundHttpException(\sprintf('No payment %d found on order %d.', $paymentId, $orderId));
+ }
+
+ return $payment;
+ }
+}
diff --git a/src/Action/Api/ApiAwareTrait.php b/src/Action/Api/ApiAwareTrait.php
index 2bab0e65..5c725017 100644
--- a/src/Action/Api/ApiAwareTrait.php
+++ b/src/Action/Api/ApiAwareTrait.php
@@ -7,11 +7,28 @@
use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientInterface;
use Payum\Core\Exception\UnsupportedApiException;
+/**
+ * NOT deprecated as a whole: the $payPlugApiClient property below is still used by live code.
+ * {@see \PayPlug\SyliusPayPlugPlugin\Controller\OneClickAction}, a routed controller, uses this
+ * trait for that property alone and assigns it itself from PayPlugApiClientFactory. Only setApi()
+ * — the Payum ApiAwareInterface entry point — is dead.
+ */
trait ApiAwareTrait
{
/** @var PayPlugApiClientInterface */
protected $payPlugApiClient;
+ /**
+ * @deprecated Dead Payum wiring — never called at runtime.
+ *
+ * Payum assigns the API through this method when it builds a gateway, but no Payum gateway is
+ * ever registered for the `payplug*` factory names (config/services/gateway.xml, the only
+ * source of `payum.gateway_factory_builder` tags, is not loaded by the bundle extension).
+ * Live code obtains its client from PayPlugApiClientFactoryInterface::createForPaymentMethod()
+ * instead, which is channel-aware.
+ *
+ * Will be removed permanently in early 2027.
+ */
public function setApi($api): void
{
if (!$api instanceof PayPlugApiClientInterface) {
diff --git a/src/Action/CaptureAction.php b/src/Action/CaptureAction.php
index 8030246c..5cbe42a9 100644
--- a/src/Action/CaptureAction.php
+++ b/src/Action/CaptureAction.php
@@ -42,7 +42,18 @@
use Symfony\Contracts\Translation\TranslatorInterface;
use Webmozart\Assert\Assert;
-/** @deprecated */
+/**
+ * @deprecated Dead Payum wiring — this action is never executed.
+ *
+ * The `payum.action` tags declared below do reach the compiled container, but no Payum gateway is
+ * ever registered for the `payplug*` factory names: the only `payum.gateway_factory_builder` tags
+ * live in config/services/gateway.xml, which PayPlugSyliusPayPlugExtension::load() does not load.
+ * With no gateway to attach to, the tagged action is collected and then never run.
+ *
+ * Superseded by {@see \PayPlug\SyliusPayPlugPlugin\Command\Handler\CapturePaymentRequestHandler}.
+ * Kept only as an opt-in escape hatch for host applications importing gateway.xml themselves.
+ * Will be removed permanently in early 2027.
+ */
#[AutoconfigureTag(
name: 'payum.action',
attributes: [
diff --git a/src/Action/ConvertPaymentAction.php b/src/Action/ConvertPaymentAction.php
index 663b9b1a..14394543 100644
--- a/src/Action/ConvertPaymentAction.php
+++ b/src/Action/ConvertPaymentAction.php
@@ -18,7 +18,19 @@
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
-/** @deprecated */
+/**
+ * @deprecated Dead Payum wiring — this action is never executed.
+ *
+ * The `payum.action` tags declared below do reach the compiled container, but no Payum gateway is
+ * ever registered for the `payplug*` factory names: the only `payum.gateway_factory_builder` tags
+ * live in config/services/gateway.xml, which PayPlugSyliusPayPlugExtension::load() does not load.
+ * With no gateway to attach to, the tagged action is collected and then never run.
+ *
+ * The payment payload is now built by {@see \PayPlug\SyliusPayPlugPlugin\Creator\PayPlugPaymentDataCreator},
+ * called directly from {@see \PayPlug\SyliusPayPlugPlugin\Command\Handler\CapturePaymentRequestHandler}.
+ * Kept only as an opt-in escape hatch for host applications importing gateway.xml themselves.
+ * Will be removed permanently in early 2027.
+ */
#[AutoconfigureTag(
name: 'payum.action',
attributes: [
diff --git a/src/Action/NotifyAction.php b/src/Action/NotifyAction.php
index e90dac54..8c4e510f 100644
--- a/src/Action/NotifyAction.php
+++ b/src/Action/NotifyAction.php
@@ -26,7 +26,21 @@
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
-/** @deprecated */
+/**
+ * @deprecated Dead Payum wiring — this action is never executed.
+ *
+ * The `payum.action` tags declared below do reach the compiled container, but no Payum gateway is
+ * ever registered for the `payplug*` factory names: the only `payum.gateway_factory_builder` tags
+ * live in config/services/gateway.xml, which PayPlugSyliusPayPlugExtension::load() does not load.
+ * With no gateway to attach to, the tagged action is collected and then never run.
+ *
+ * The live IPN path is the routed controller {@see \PayPlug\SyliusPayPlugPlugin\Controller\IpnAction},
+ * which dispatches to {@see \PayPlug\SyliusPayPlugPlugin\Command\Handler\NotifyPaymentRequestHandler}.
+ * Consequently the `sleep(10)` in execute() below is dead code and guards nothing in production.
+ *
+ * Kept only as an opt-in escape hatch for host applications importing gateway.xml themselves.
+ * Will be removed permanently in early 2027.
+ */
#[AutoconfigureTag(
name: 'payum.action',
attributes: [
diff --git a/src/Action/StatusAction.php b/src/Action/StatusAction.php
index 135af4a6..9096fc20 100644
--- a/src/Action/StatusAction.php
+++ b/src/Action/StatusAction.php
@@ -28,7 +28,18 @@
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
use Symfony\Component\HttpFoundation\RequestStack;
-/** @deprecated */
+/**
+ * @deprecated Dead Payum wiring — this action is never executed.
+ *
+ * The `payum.action` tags declared below do reach the compiled container, but no Payum gateway is
+ * ever registered for the `payplug*` factory names: the only `payum.gateway_factory_builder` tags
+ * live in config/services/gateway.xml, which PayPlugSyliusPayPlugExtension::load() does not load.
+ * With no gateway to attach to, the tagged action is collected and then never run.
+ *
+ * Superseded by {@see \PayPlug\SyliusPayPlugPlugin\Command\Handler\StatusPaymentRequestHandler}.
+ * Kept only as an opt-in escape hatch for host applications importing gateway.xml themselves.
+ * Will be removed permanently in early 2027.
+ */
#[AutoconfigureTag(
name: 'payum.action',
attributes: [
diff --git a/src/ApiClient/PayPlugApiClientFactory.php b/src/ApiClient/PayPlugApiClientFactory.php
index 7d57cfe9..6e6f641d 100644
--- a/src/ApiClient/PayPlugApiClientFactory.php
+++ b/src/ApiClient/PayPlugApiClientFactory.php
@@ -4,22 +4,35 @@
namespace PayPlug\SyliusPayPlugPlugin\ApiClient;
-use Payplug\Authentication;
use PayPlug\SyliusPayPlugPlugin\Exception\GatewayConfigurationException;
+use PayplugUnifiedCore\Auth\TokenManager;
+use PayplugUnifiedCore\Exceptions\ApiException;
use Sylius\Component\Payment\Model\GatewayConfigInterface;
use Sylius\Component\Payment\Model\PaymentMethodInterface;
use Sylius\Component\Resource\Repository\RepositoryInterface;
use Symfony\Contracts\Cache\CacheInterface;
-use Symfony\Contracts\Cache\ItemInterface;
final class PayPlugApiClientFactory implements PayPlugApiClientFactoryInterface
{
public function __construct(
private RepositoryInterface $gatewayConfigRepository,
private CacheInterface $cache,
+ private TokenManager $tokenManager,
) {
}
+ /**
+ * Channel-ambiguous: since PRE-3628 several enabled gateway configs may share a factory name —
+ * one per channel — and findOneBy() then returns an arbitrary one of them, so the client this
+ * returns may carry another channel's account credentials.
+ *
+ * @internal Kept off {@see PayPlugApiClientFactoryInterface} so no application class can reach
+ * it; the sole remaining callers are the `payplug_sylius_payplug_plugin.api_client.*`
+ * service-factory definitions in config/services/client.xml, which are #[Autowire]d
+ * into seven services that have no payment method in scope. Use
+ * {@see self::createForPaymentMethod()} everywhere else. Removed once those
+ * singletons are made channel-aware — the open half of PRE-3682.
+ */
public function create(string $factoryName): PayPlugApiClientInterface
{
/** @var GatewayConfigInterface|null $gatewayConfig */
@@ -54,26 +67,17 @@ private function getTokenForGatewayConfig(GatewayConfigInterface $gatewayConfig)
}
/** @var array $clientConfig */
$clientConfig = $rawClientConfig;
- $cacheKey = sprintf('payplug_%s_api_key_%s', $gatewayConfig->getFactoryName(), $isLive ? 'live' : 'test');
- return $this->cache->get($cacheKey, function (ItemInterface $item) use ($clientConfig) {
- $response = Authentication::generateJWT($clientConfig['client_id'] ?? '', $clientConfig['client_secret'] ?? '');
- if ([] === $response || !is_array($response['httpResponse'])) {
- throw new GatewayConfigurationException('Unable to connect to PayPlug API. Please check your credentials in the PayPlug plugin configuration.');
- }
-
- $accessToken = $response['httpResponse']['access_token'];
- if (!is_string($accessToken)) {
- throw new GatewayConfigurationException('Unable to connect to PayPlug API. Please check your credentials in the PayPlug plugin configuration.');
- }
- $expiresIn = $response['httpResponse']['expires_in'];
- if (!is_int($expiresIn)) {
- $expiresIn = 200;
- }
-
- $item->expiresAfter($expiresIn);
+ $clientId = $clientConfig['client_id'] ?? '';
+ $clientSecret = $clientConfig['client_secret'] ?? '';
+ if ('' === $clientId || '' === $clientSecret) {
+ throw new GatewayConfigurationException('No client config found for ' . $gatewayConfig->getFactoryName() . '. Please renew your credentials in the PayPlug plugin configuration.');
+ }
- return $accessToken;
- });
+ try {
+ return $this->tokenManager->getValidToken($clientId, $clientSecret);
+ } catch (ApiException $e) {
+ throw new GatewayConfigurationException('Unable to connect to PayPlug API. Please check your credentials in the PayPlug plugin configuration.', 0, $e);
+ }
}
}
diff --git a/src/ApiClient/PayPlugApiClientFactoryInterface.php b/src/ApiClient/PayPlugApiClientFactoryInterface.php
index 12b96f2a..70cb1912 100644
--- a/src/ApiClient/PayPlugApiClientFactoryInterface.php
+++ b/src/ApiClient/PayPlugApiClientFactoryInterface.php
@@ -8,7 +8,12 @@
interface PayPlugApiClientFactoryInterface
{
- public function create(string $factoryName): PayPlugApiClientInterface;
-
+ /**
+ * The only way to obtain a client from application code. Resolving one by factory name is
+ * deliberately absent: since PRE-3628 several enabled gateway configs may share a factory name
+ * — one per channel — so a name-based lookup returns an arbitrary one of them and can sign a
+ * request for channel A with channel B's account credentials. Keeping that signature off this
+ * interface makes the compiler, rather than review, the guard against reintroducing it.
+ */
public function createForPaymentMethod(PaymentMethodInterface $paymentMethod): PayPlugApiClientInterface;
}
diff --git a/src/Auth/GatewayConnectionRevoker.php b/src/Auth/GatewayConnectionRevoker.php
new file mode 100644
index 00000000..3d1fc8bd
--- /dev/null
+++ b/src/Auth/GatewayConnectionRevoker.php
@@ -0,0 +1,102 @@
+getGatewayConfig() ?? throw new \LogicException(
+ 'The payment method has no gateway config, so no PayPlug connection can be revoked for it.',
+ );
+
+ $config = $gatewayConfig->getConfig();
+
+ // Read before the keys are dropped: the client id is the only handle on the cache entry.
+ // The cache is purged after the flush, so a failing flush cannot leave a gateway whose
+ // credentials still persist but whose tokens are gone.
+ $revokedCredentials = \array_map(
+ static fn (string $clientKey): mixed => $config[$clientKey] ?? null,
+ self::CLIENT_CREDENTIAL_KEYS,
+ );
+
+ foreach (self::CONNECTION_KEYS as $key) {
+ unset($config[$key]);
+ }
+
+ // The Hosted Fields account id identifies the account that was just disconnected, so it is
+ // stale the moment the merchant reconnects to a different one. Cleared only where it is
+ // actually in use — on any other gateway, or on a CB gateway in redirected/integrated mode,
+ // it is an inert leftover the merchant may still want on their next login.
+ if (PayPlugGatewayFactory::isHostedFieldsConfig($gatewayConfig)) {
+ unset($config[PayPlugGatewayFactory::HF_IDENTIFIER]);
+ }
+
+ $gatewayConfig->setConfig($config);
+ $paymentMethod->disable();
+
+ $this->entityManager->flush();
+
+ foreach ($revokedCredentials as $clientCredentials) {
+ $this->forgetCachedToken($clientCredentials);
+ }
+ }
+
+ private function forgetCachedToken(mixed $clientCredentials): void
+ {
+ $clientId = \is_array($clientCredentials) ? ($clientCredentials['client_id'] ?? null) : null;
+
+ if (!\is_string($clientId) || '' === $clientId) {
+ return;
+ }
+
+ $this->tokenCache->delete(self::TOKEN_CACHE_KEY_PREFIX . $clientId);
+ }
+}
diff --git a/src/Auth/IdTokenEmailExtractor.php b/src/Auth/IdTokenEmailExtractor.php
new file mode 100644
index 00000000..d52c6b8a
--- /dev/null
+++ b/src/Auth/IdTokenEmailExtractor.php
@@ -0,0 +1,105 @@
+decodeClaims($idToken);
+
+ $email = $claims['email'] ?? null;
+
+ if (!\is_string($email) || false === filter_var($email, \FILTER_VALIDATE_EMAIL)) {
+ return null;
+ }
+
+ return $email;
+ }
+
+ /**
+ * @return array The token's claims, or an empty array for anything unreadable
+ */
+ private function decodeClaims(?string $idToken): array
+ {
+ $payload = $this->decodePayloadSegment($idToken);
+
+ if (null === $payload) {
+ return [];
+ }
+
+ $claims = json_decode($payload, true);
+
+ return \is_array($claims) ? $claims : [];
+ }
+
+ /**
+ * @return string|null The decoded payload segment, or null when the token is absent, empty or
+ * not the three dot-separated segments a JWT is made of
+ */
+ private function decodePayloadSegment(?string $idToken): ?string
+ {
+ if (null === $idToken || '' === $idToken) {
+ return null;
+ }
+
+ $segments = explode('.', $idToken);
+
+ if (self::SEGMENT_COUNT !== \count($segments)) {
+ return null;
+ }
+
+ return $this->base64UrlDecode($segments[self::PAYLOAD_SEGMENT]);
+ }
+
+ /**
+ * JWT segments use base64url (RFC 7515 §2): `-`/`_` in place of `+`/`/`, and no `=` padding.
+ * Feeding one straight to base64_decode() silently corrupts any payload containing those
+ * characters, which real id tokens routinely do.
+ */
+ private function base64UrlDecode(string $segment): ?string
+ {
+ $padded = str_pad(strtr($segment, '-_', '+/'), (int) (ceil(\strlen($segment) / 4) * 4), '=');
+
+ $decoded = base64_decode($padded, true);
+
+ return false === $decoded ? null : $decoded;
+ }
+}
diff --git a/src/Auth/SyliusOAuthHttpClient.php b/src/Auth/SyliusOAuthHttpClient.php
new file mode 100644
index 00000000..36611f3d
--- /dev/null
+++ b/src/Auth/SyliusOAuthHttpClient.php
@@ -0,0 +1,46 @@
+ $formParams
+ * @param array $headers
+ *
+ * @return array{status: int, body: string}
+ */
+ public function post(string $url, array $formParams, array $headers = []): array
+ {
+ try {
+ $response = $this->httpClient->request('POST', $url, [
+ 'body' => http_build_query($formParams),
+ 'headers' => $headers,
+ ]);
+
+ return [
+ 'status' => $response->getStatusCode(),
+ // false = don't throw on non-2xx; OAuth2Client itself checks the status.
+ 'body' => $response->getContent(false),
+ ];
+ } catch (TransportExceptionInterface $e) {
+ // Network-level failure (DNS, timeout, connection reset) — getStatusCode()/getContent()
+ // throw this regardless of the `false` above, since it only suppresses HTTP status
+ // exceptions, not transport ones. Status 0 makes OAuth2Client::requestToken() throw its
+ // own ApiException, which callers (e.g. PayPlugApiClientFactory) already catch and
+ // translate, the same way a non-2xx response from PayPlug itself would be handled.
+ return ['status' => 0, 'body' => $e->getMessage()];
+ }
+ }
+}
diff --git a/src/Auth/SyliusTokenCache.php b/src/Auth/SyliusTokenCache.php
new file mode 100644
index 00000000..1be872a8
--- /dev/null
+++ b/src/Auth/SyliusTokenCache.php
@@ -0,0 +1,48 @@
+cache->getItem($this->sanitizeKey($key));
+
+ if (!$item->isHit()) {
+ return null;
+ }
+
+ $value = $item->get();
+
+ return \is_string($value) ? $value : null;
+ }
+
+ public function set(string $key, string $value, int $ttlSeconds): void
+ {
+ $item = $this->cache->getItem($this->sanitizeKey($key));
+ $item->set($value);
+ $item->expiresAfter($ttlSeconds);
+ $this->cache->save($item);
+ }
+
+ public function delete(string $key): void
+ {
+ $this->cache->deleteItem($this->sanitizeKey($key));
+ }
+
+ // PSR-6 rejects "{}()/\@:" in cache keys; TokenManager's keys contain ":".
+ private function sanitizeKey(string $key): string
+ {
+ return (string) preg_replace('/[{}()\/\\\\@:]/', '_', $key);
+ }
+}
diff --git a/src/Checker/GatewayChannelConflictChecker.php b/src/Checker/GatewayChannelConflictChecker.php
new file mode 100644
index 00000000..57f4be14
--- /dev/null
+++ b/src/Checker/GatewayChannelConflictChecker.php
@@ -0,0 +1,128 @@
+
+ */
+ public function findConflicts(PaymentMethodInterface $paymentMethod, string $factoryName): array
+ {
+ $channelCodes = $this->channelCodes($paymentMethod);
+
+ if (!$paymentMethod->isEnabled() || [] === $channelCodes) {
+ return [];
+ }
+
+ return \array_values(\array_filter(
+ $this->claims($paymentMethod, $factoryName),
+ static fn (string $channelCode): bool => \in_array($channelCode, $channelCodes, true),
+ \ARRAY_FILTER_USE_KEY,
+ ));
+ }
+
+ /**
+ * The channels the gateway channel picker must render as unselectable, keyed by channel code.
+ *
+ * Deliberately unconditional where `findConflicts()` returns early on the subject's `enabled`
+ * flag: the picker is rendered before the admin has decided anything. Building both answers
+ * from the same `claims()` lookup is what keeps the disabled options and the submit-time rule
+ * in step.
+ *
+ * The subject's own channels are excluded, however. A browser does not submit a disabled
+ * checkbox, so disabling one that is *checked* would silently drop that channel the next time
+ * the admin saves — the very removal this picker exists to prevent. An overlap that already
+ * exists in the data therefore stays selectable and is reported by `findConflicts()` on submit
+ * instead. `claims()` alone cannot cover this: it excludes the subject by id, which says
+ * nothing about a *different* enabled payment method holding a channel the subject also holds.
+ *
+ * @return array
+ */
+ public function findClaimedChannels(PaymentMethodInterface $paymentMethod, string $factoryName): array
+ {
+ return \array_diff_key(
+ \array_map(
+ static fn (array $claim): PaymentMethodInterface => $claim['paymentMethod'],
+ $this->claims($paymentMethod, $factoryName),
+ ),
+ \array_flip($this->channelCodes($paymentMethod)),
+ );
+ }
+
+ /**
+ * Channels held by *other* enabled payment methods of the same factory.
+ *
+ * Keying by channel code collapses the case of two enabled rivals holding the same channel down
+ * to a single claim, so neither caller reports it twice. PRE-3628 makes that state hard to
+ * reach, not impossible — see {@see \PayPlug\SyliusPayPlugPlugin\Gateway\Validator\Constraints\HasNoGatewayChannelConflict}
+ * for the write paths the rule does not cover — so the collapsing is load-bearing, not defensive.
+ *
+ * @return array
+ */
+ private function claims(PaymentMethodInterface $paymentMethod, string $factoryName): array
+ {
+ $claims = [];
+ foreach ($this->paymentMethodRepository->findEnabledByGatewayName($factoryName) as $rival) {
+ if ($this->isSamePaymentMethod($paymentMethod, $rival) || !$rival->isEnabled()) {
+ continue;
+ }
+
+ foreach ($rival->getChannels() as $rivalChannel) {
+ if (!$rivalChannel instanceof ChannelInterface) {
+ continue;
+ }
+
+ $channelCode = $rivalChannel->getCode();
+
+ if (null !== $channelCode) {
+ $claims[$channelCode] = ['channel' => $rivalChannel, 'paymentMethod' => $rival];
+ }
+ }
+ }
+
+ return $claims;
+ }
+
+ /**
+ * @return list
+ */
+ private function channelCodes(PaymentMethodInterface $paymentMethod): array
+ {
+ $channelCodes = [];
+ foreach ($paymentMethod->getChannels() as $channel) {
+ $channelCode = $channel->getCode();
+ if (null !== $channelCode) {
+ $channelCodes[] = $channelCode;
+ }
+ }
+
+ return $channelCodes;
+ }
+
+ /**
+ * On creation the subject has no id yet, so it can never match a persisted rival.
+ */
+ private function isSamePaymentMethod(PaymentMethodInterface $subject, PaymentMethodInterface $rival): bool
+ {
+ return null !== $subject->getId() && $subject->getId() === $rival->getId();
+ }
+}
diff --git a/src/Command/CaptureAliasPaymentRequest.php b/src/Command/CaptureAliasPaymentRequest.php
new file mode 100644
index 00000000..665bbabe
--- /dev/null
+++ b/src/Command/CaptureAliasPaymentRequest.php
@@ -0,0 +1,9 @@
+paymentRequestProvider->provide($captureAliasPaymentRequest);
+ /** @var PaymentInterface $payment */
+ $payment = $paymentRequest->getPayment();
+
+ try {
+ $method = $this->contextBuilder->resolvePaymentMethod($payment);
+
+ $card = $this->selectedCardResolver->resolve();
+ if (null === $card) {
+ throw new \LogicException('No saved card alias selected for the payment.');
+ }
+ [$amount, $currencyCode] = $this->contextBuilder->resolveAmountAndCurrency($payment);
+ $accountId = $this->contextBuilder->resolveGatewayCredentials($method);
+
+ $order = $this->assertCardBelongsToOrder($card, $payment->getOrder(), $method);
+ $common = $this->contextBuilder->buildCommonFields($accountId, $amount, $currencyCode, $paymentRequest, $order);
+ $dto = $this->buildPaymentDto($common, $card, $order);
+
+ $output = $this->unifiedApiPaymentCreator->createPayment($dto, $method);
+ } catch (ApiException | InvalidPaymentException | \LogicException $e) {
+ $this->outcomeApplier->failPaymentRequest($paymentRequest, $payment, $e, PaymentCaptureFlow::Alias);
+
+ return;
+ }
+
+ $hostedFieldsIds = $this->contextBuilder->resolveHostedFieldsIds($output->body);
+ $createdDetails = [
+ ...$payment->getDetails(),
+ 'alias_id' => $card->getExternalId(),
+ 'alias_payment_created_at' => (new \DateTimeImmutable())->format(\DateTimeInterface::ATOM),
+ ...$hostedFieldsIds,
+ ];
+ // Before applyOutcome() below — see CaptureHostedPaymentRequestHandler's own comment.
+ $payment->setDetails($common->capture ? $createdDetails : AuthorizationDetails::open($createdDetails, $output, $amount));
+ $this->outcomeApplier->warnWhenPaymentIdMissing($payment, PaymentCaptureFlow::Alias, $hostedFieldsIds);
+
+ $this->outcomeApplier->applyOutcome($paymentRequest, $payment, $output);
+
+ $this->stateMachine->apply($paymentRequest, PaymentRequestTransitions::GRAPH, PaymentRequestTransitions::TRANSITION_COMPLETE);
+ }
+
+ private function assertCardBelongsToOrder(
+ Card $card,
+ ?OrderInterface $order,
+ PaymentMethodInterface $method,
+ ): OrderInterface {
+ if (null === $order || $card->getCustomer() !== $order->getCustomer() || $card->getPaymentMethod() !== $method) {
+ throw new \LogicException('Selected card does not belong to the paying customer or payment method.');
+ }
+
+ return $order;
+ }
+
+ private function buildPaymentDto(CommonFieldsDto $common, Card $card, OrderInterface $order): PaymentDto
+ {
+ $customerDto = $this->contextBuilder->buildCustomerDto($order);
+ $browserDto = $this->contextBuilder->buildBrowserDto();
+
+ $fullName = $this->contextBuilder->resolveFullNameForCardDetails($order);
+ $paymentMethod = null !== $fullName
+ ? ['details' => ['fullName' => $fullName]]
+ : null;
+
+ return new PaymentDto($common, $card->getExternalId(), 'ONE_CLICK', $browserDto, $customerDto, $paymentMethod);
+ }
+}
diff --git a/src/Command/Handler/CaptureHostedPaymentRequestHandler.php b/src/Command/Handler/CaptureHostedPaymentRequestHandler.php
new file mode 100644
index 00000000..d7c6f1d8
--- /dev/null
+++ b/src/Command/Handler/CaptureHostedPaymentRequestHandler.php
@@ -0,0 +1,195 @@
+paymentRequestProvider->provide($captureHostedPaymentRequest);
+ /** @var PaymentInterface $payment */
+ $payment = $paymentRequest->getPayment();
+ $details = $payment->getDetails();
+
+ try {
+ $method = $this->contextBuilder->resolvePaymentMethod($payment);
+ $hfToken = $this->resolveHostedFieldsToken($details);
+ $amountAndCurrency = $this->contextBuilder->resolveAmountAndCurrency($payment);
+ $accountId = $this->contextBuilder->resolveGatewayCredentials($method);
+
+ $dto = $this->buildHostedFieldDto(
+ $paymentRequest,
+ $payment,
+ $details,
+ $hfToken,
+ $amountAndCurrency,
+ $accountId,
+ );
+
+ $this->logger->debug('[PayPlug debug] Unified API hosted payment request payload.', [
+ 'payload' => $dto->createPayloadBody(),
+ ]);
+
+ $output = $this->unifiedApiPaymentCreator->createPayment($dto, $method);
+ } catch (ApiException | InvalidHostedFieldException | \LogicException $e) {
+ $this->outcomeApplier->failPaymentRequest($paymentRequest, $payment, $e, PaymentCaptureFlow::Hosted);
+
+ return;
+ }
+
+ $hostedFieldsIds = $this->contextBuilder->resolveHostedFieldsIds($output->body);
+ $createdDetails = [
+ ...$details,
+ 'hosted_fields_created_at' => (new \DateTimeImmutable())->format(\DateTimeInterface::ATOM),
+ ...$hostedFieldsIds,
+ ];
+ // Before applyOutcome() below, which reads this back to apply "authorize" rather than
+ // "complete" for an authorization-only payment.
+ $payment->setDetails($dto->common->capture ? $createdDetails : AuthorizationDetails::open($createdDetails, $output, $amountAndCurrency[0]));
+ $this->outcomeApplier->warnWhenPaymentIdMissing($payment, PaymentCaptureFlow::Hosted, $hostedFieldsIds);
+
+ $this->outcomeApplier->applyOutcome($paymentRequest, $payment, $output);
+
+ $saveCard = true === ($details['hosted_fields_save_card'] ?? false);
+ if ($saveCard && null !== $output->aliasId) {
+ $unifiedApiOperationId = $hostedFieldsIds['hosted_fields_operation_id'] ?? null;
+ $fetchedCardData = null !== $unifiedApiOperationId ? $this->fetchCardDataFromUnifiedApi($unifiedApiOperationId, $method) : [];
+ $this->cardPersister->persist($output->aliasId, $payment, $method, $details, $fetchedCardData);
+ }
+
+ $this->stateMachine->apply($paymentRequest, PaymentRequestTransitions::GRAPH, PaymentRequestTransitions::TRANSITION_COMPLETE);
+ }
+
+ /** @param mixed[] $details */
+ private function resolveHostedFieldsToken(array $details): string
+ {
+ $hfToken = $details['hosted_fields_token'] ?? null;
+ if (!\is_string($hfToken) || '' === $hfToken) {
+ throw new \LogicException('No hosted fields token found on the payment.');
+ }
+
+ return $hfToken;
+ }
+
+ /**
+ * @param mixed[] $details
+ * @param array{0: int, 1: string} $amountAndCurrency
+ */
+ private function buildHostedFieldDto(
+ PaymentRequestInterface $paymentRequest,
+ PaymentInterface $payment,
+ array $details,
+ string $hfToken,
+ array $amountAndCurrency,
+ string $accountId,
+ ): HostedFieldDto {
+ [$amount, $currencyCode] = $amountAndCurrency;
+
+ $order = $payment->getOrder();
+
+ $common = $this->contextBuilder->buildCommonFields($accountId, $amount, $currencyCode, $paymentRequest, $order);
+
+ $selectedBrand = $details['hosted_fields_selected_brand'] ?? null;
+ $hasSelectedBrand = \is_string($selectedBrand) && '' !== $selectedBrand;
+ $fullName = $this->contextBuilder->resolveFullNameForCardDetails($order);
+ $hasFullName = null !== $fullName;
+
+ $saveCard = true === ($details['hosted_fields_save_card'] ?? false);
+ $recurringMode = $saveCard ? 'ONE_CLICK' : null;
+
+ $cardDetails = [];
+ if ($hasFullName) {
+ $cardDetails['fullName'] = $fullName;
+ }
+ if ($hasSelectedBrand) {
+ $cardDetails['selectedBrand'] = $selectedBrand;
+ }
+
+ // saveFutureUsage is only requested alongside a fullName: the Unified API rejects an
+ // alias-creation request (paymentMethod.saveFutureUsage: true) missing
+ // paymentMethod.details.fullName, so requesting it without one would fail the entire
+ // payment rather than just skip saving the card.
+ /** @var array{details?: array{fullName?: string, selectedBrand?: string}, saveFutureUsage?: bool}|null $paymentMethodDetails */
+ $paymentMethodDetails = match (true) {
+ [] !== $cardDetails && $saveCard && $hasFullName => ['details' => $cardDetails, 'saveFutureUsage' => true],
+ [] !== $cardDetails => ['details' => $cardDetails],
+ default => null,
+ };
+
+ // Named arguments here (rather than positional) because HostedFieldDto's real
+ // constructor interposes $recurringMode between $hfToken and $browser/$customer —
+ // positional args would silently misalign.
+ return new HostedFieldDto(
+ $common,
+ $hfToken,
+ recurringMode: $recurringMode,
+ browser: $this->contextBuilder->buildBrowserDto(),
+ customer: $this->contextBuilder->buildCustomerDto($order),
+ paymentMethod: $paymentMethodDetails,
+ );
+ }
+
+ /**
+ * The Unified API's alias-creation response carries only the alias id, no card metadata — the
+ * card's real brand/last4/expiration only show up on the operation resource, fetched here via
+ * the existing OperationStatusFetcherInterface (already used by StatusHostedPaymentRequestHandler
+ * for its 3DS polling fallback) rather than a second, duplicate client. Best-effort: any
+ * failure (API error, unexpected shape) is logged and swallowed rather than propagated, since
+ * this only enriches the card being persisted — PayplugCardPersister::persist()'s own
+ * $details-based fallback values already cover the case where this fetch fails or a field
+ * turns out to be absent.
+ *
+ * @return array{aliasId?: string, brand?: string, last4?: string, expirationMonth?: int, expirationYear?: int}
+ */
+ private function fetchCardDataFromUnifiedApi(string $operationId, PaymentMethodInterface $method): array
+ {
+ try {
+ $response = $this->operationStatusFetcher->getOperation($operationId, $method);
+ } catch (ApiException $e) {
+ $this->logger->error('[PayPlug][UPC] Failed to fetch operation for card metadata.', [
+ 'unified_api_operation_id' => $operationId,
+ 'error' => $e->getMessage(),
+ ]);
+
+ return [];
+ }
+
+ return CardDataFromPaymentMethodExtractor::extract($response['body']);
+ }
+}
diff --git a/src/Command/Handler/NotifyHostedPaymentRequestHandler.php b/src/Command/Handler/NotifyHostedPaymentRequestHandler.php
new file mode 100644
index 00000000..0b46fbf6
--- /dev/null
+++ b/src/Command/Handler/NotifyHostedPaymentRequestHandler.php
@@ -0,0 +1,147 @@
+paymentRequestProvider->provide($notifyHostedPaymentRequest);
+ $payload = $paymentRequest->getPayload();
+ $rawBody = $payload['http_request']['content'] ?? null; // @phpstan-ignore-line
+ $rawHeaders = $payload['http_request']['headers'] ?? null; // @phpstan-ignore-line
+ if (!\is_string($rawBody) || !\is_array($rawHeaders)) {
+ throw new \LogicException('Invalid UPC notification payload.');
+ }
+ $headers = $this->flattenHeaders($rawHeaders); // @phpstan-ignore-line
+
+ $lockKey = 'payplug_upc_notify_' . \hash('sha256', $rawBody);
+ if (!$this->lock->acquire($lockKey, 30)) {
+ // Another delivery of the same webhook is already being processed; tell PayPlug it
+ // succeeded so it does not keep retrying.
+ $this->stateMachine->apply($paymentRequest, PaymentRequestTransitions::GRAPH, PaymentRequestTransitions::TRANSITION_COMPLETE);
+
+ return;
+ }
+
+ try {
+ // Per-gateway-config shared secret: verify against the account this payment belongs to.
+ $method = $paymentRequest->getPayment()->getMethod();
+ if (!$method instanceof PaymentMethodInterface) {
+ throw new \LogicException('The payment has no payment method, so no webhook secret can be resolved for it.');
+ }
+
+ $expectedHeader = $this->configurationRepository->forPaymentMethod($method)->get(self::CONFIG_KEY_WEBHOOK_AUTHORIZATION_HEADER) ?? '';
+ $operationData = WebhookNotificationHelper::parse($headers, $rawBody, $expectedHeader);
+
+ if (!$this->matchesPaymentRequest($paymentRequest, $operationData)) {
+ return;
+ }
+
+ if ($this->paymentRepository->isTreated($operationData->operationId)) {
+ return;
+ }
+
+ $this->paymentRepository->save($operationData);
+ // This per-request notification only ever confirms the payment's own creation, so an
+ // authorization-only payment's success is "authorized", never "paid".
+ $outcome = AuthorizationDetails::fromDetails($paymentRequest->getPayment()->getDetails())->resolveCreationOutcome($operationData->outcome);
+ $this->orderStateMutator->apply($operationData->orderId, $outcome);
+ $this->paymentRepository->markTreated($operationData->operationId);
+
+ $this->stateMachine->apply($paymentRequest, PaymentRequestTransitions::GRAPH, PaymentRequestTransitions::TRANSITION_COMPLETE);
+ } catch (InvalidNotificationException $e) {
+ $this->logger->error('[PayPlug][UPC] Rejected webhook notification.', ['error' => $e->getMessage()]);
+ $paymentRequest->setResponseData(['error' => $e->getMessage()]);
+ $this->stateMachine->apply($paymentRequest, PaymentRequestTransitions::GRAPH, PaymentRequestTransitions::TRANSITION_FAIL);
+ } finally {
+ $this->lock->release($lockKey);
+ }
+ }
+
+ // Split out of __invoke() to keep its own return count within SonarCloud's limit (php:S1142)
+ // — both branches here mean "nothing to apply," they just differ in whether that's expected
+ // (still-pending) or a problem worth logging and failing the request over (mismatch).
+ private function matchesPaymentRequest(PaymentRequestInterface $paymentRequest, OperationData $operationData): bool
+ {
+ if (PaymentOutcome::THREE_DS_PENDING === $operationData->outcome) {
+ // Not a final outcome — leave the payment request as-is and, crucially, do not
+ // touch isTreated()/markTreated(): a later, final notification for this same
+ // operation must still be free to apply once it arrives. Mirrors the same guard
+ // in HostedFieldsWebhookNotificationHandler::treat().
+ return false;
+ }
+
+ $expectedOrderId = (string) $paymentRequest->getPayment()->getId(); // @phpstan-ignore-line
+ $expectedAmount = $paymentRequest->getPayment()->getAmount();
+ if ($operationData->orderId !== $expectedOrderId || $operationData->amount !== $expectedAmount) {
+ $this->logger->error('[PayPlug][UPC] Webhook notification does not match the payment request it was received for.', [
+ 'expected_order_id' => $expectedOrderId,
+ 'received_order_id' => $operationData->orderId,
+ 'expected_amount' => $expectedAmount,
+ 'received_amount' => $operationData->amount,
+ ]);
+ $paymentRequest->setResponseData(['error' => 'Webhook notification does not match the expected payment.']);
+ $this->stateMachine->apply($paymentRequest, PaymentRequestTransitions::GRAPH, PaymentRequestTransitions::TRANSITION_FAIL);
+
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * @param array> $rawHeaders
+ *
+ * @return array
+ */
+ private function flattenHeaders(array $rawHeaders): array
+ {
+ $headers = [];
+ foreach ($rawHeaders as $name => $values) {
+ $headers[$name] = $values[0] ?? '';
+ }
+
+ return $headers;
+ }
+}
diff --git a/src/Command/Handler/StatusHostedPaymentRequestHandler.php b/src/Command/Handler/StatusHostedPaymentRequestHandler.php
new file mode 100644
index 00000000..4b4e6ea7
--- /dev/null
+++ b/src/Command/Handler/StatusHostedPaymentRequestHandler.php
@@ -0,0 +1,141 @@
+paymentRequestProvider->provide($statusHostedPaymentRequest);
+ /** @var PaymentInterface $payment */
+ $payment = $paymentRequest->getPayment();
+
+ if (self::FORCED_STATUS_CANCELED === $statusHostedPaymentRequest->getForcedStatus()) {
+ if ($this->stateMachine->can($payment, PaymentTransitions::GRAPH, PaymentTransitions::TRANSITION_CANCEL)) {
+ $this->stateMachine->apply($payment, PaymentTransitions::GRAPH, PaymentTransitions::TRANSITION_CANCEL);
+ }
+ } else {
+ $this->pollForOutcomeIfStillPending($payment);
+ }
+
+ $this->stateMachine->apply($paymentRequest, PaymentRequestTransitions::GRAPH, PaymentRequestTransitions::TRANSITION_COMPLETE);
+ }
+
+ /**
+ * Fallback for when the asynchronous webhook (NotifyHostedPaymentRequestHandler /
+ * HostedFieldsWebhookNotificationHandler) hasn't confirmed the outcome by the time the
+ * customer is bounced back from the 3DS challenge page — a GET against the Unified API's
+ * public operation endpoint, per PayPlug's own documented recommendation for a delayed or
+ * lost webhook. Skipped entirely once anything else has already resolved the payment, so
+ * there's nothing here to double-apply.
+ *
+ * The public operation endpoint's response is the same webhook-shaped payload
+ * WebhookNotificationHelper::parse() already knows how to read (id/execCode/orderId/amount
+ * at the top level), so applying it is unconditionally delegated to
+ * HostedFieldsWebhookNotificationHandler::treat() — same cross-check against the payment,
+ * same isTreated()/markTreated() dedupe, same persistence, as the real webhook path,
+ * including its own no-op when the fetched operation is still THREE_DS_PENDING. This handler
+ * no longer needs its own copy of that pending-code check.
+ */
+ private function pollForOutcomeIfStillPending(PaymentInterface $payment): void
+ {
+ $operationId = self::resolveOperationId($payment->getDetails());
+ $method = $payment->getMethod();
+
+ // One guard rather than three: there is nothing to poll unless the payment is still
+ // pending, carries an operation id and names the method whose account owns it.
+ if (
+ \in_array($payment->getState(), self::RESOLVED_STATES, true) ||
+ null === $operationId ||
+ !$method instanceof PaymentMethodInterface
+ ) {
+ return;
+ }
+
+ $body = $this->fetchOperationBody($payment, $operationId, $method);
+
+ if (null === $body) {
+ return;
+ }
+
+ try {
+ $this->webhookNotificationHandler->treat($payment, $body, []);
+ } catch (InvalidNotificationException $e) {
+ $this->logger->error('[PayPlug][UPC] Hosted payment status poll returned a payload that could not be applied.', [
+ 'sylius_payment_id' => $payment->getId(),
+ 'hosted_fields_operation_id' => $operationId,
+ 'error' => $e->getMessage(),
+ ]);
+ }
+ }
+
+ /**
+ * A failed poll is logged and swallowed rather than raised: it is a fallback for a webhook that
+ * has not arrived *yet*, so leaving the payment where it is lets the webhook — or the
+ * UpdatePaymentStateCommand — resolve it later.
+ */
+ private function fetchOperationBody(
+ PaymentInterface $payment,
+ string $operationId,
+ PaymentMethodInterface $method,
+ ): ?string {
+ try {
+ return $this->operationStatusFetcher->getOperation($operationId, $method)['body'];
+ } catch (ApiException $e) {
+ $this->logger->error('[PayPlug][UPC] Hosted payment status poll failed.', [
+ 'sylius_payment_id' => $payment->getId(),
+ 'hosted_fields_operation_id' => $operationId,
+ 'error' => $e->getMessage(),
+ ]);
+
+ return null;
+ }
+ }
+
+ /** @param mixed[] $details */
+ private static function resolveOperationId(array $details): ?string
+ {
+ $operationId = $details['hosted_fields_operation_id'] ?? null;
+
+ return \is_string($operationId) && '' !== $operationId ? $operationId : null;
+ }
+}
diff --git a/src/Command/NotifyHostedPaymentRequest.php b/src/Command/NotifyHostedPaymentRequest.php
new file mode 100644
index 00000000..8a110a78
--- /dev/null
+++ b/src/Command/NotifyHostedPaymentRequest.php
@@ -0,0 +1,9 @@
+getAction() === PaymentRequestInterface::ACTION_CAPTURE;
+ }
+
+ public function provide(PaymentRequestInterface $paymentRequest): object
+ {
+ $details = $paymentRequest->getPayment()->getDetails();
+ $selectedCard = $this->selectedCardResolver->resolve();
+
+ if ($this->isAlreadyInFlight($details, $selectedCard)) {
+ return new OfflineCapturePaymentRequest($paymentRequest->getId());
+ }
+
+ return null !== $selectedCard
+ ? new CaptureAliasPaymentRequest($paymentRequest->getId())
+ : new CaptureHostedPaymentRequest($paymentRequest->getId());
+ }
+
+ /**
+ * True when a capture attempt for this exact payment/card pair was already created — either
+ * via createPayment() (e.g. the shopper returned to /pay after a 3DS redirect) or as an
+ * already-in-flight alias attempt for the same card.
+ *
+ * @param mixed[] $details
+ */
+ private function isAlreadyInFlight(array $details, ?Card $selectedCard): bool
+ {
+ if (\is_string($details['hosted_fields_created_at'] ?? null)) {
+ return true;
+ }
+
+ if (\is_string($details['alias_payment_created_at'] ?? null)) {
+ return $this->isAliasAttemptStillInFlight($details, $selectedCard);
+ }
+
+ return false;
+ }
+
+ // Split out of isAlreadyInFlight() to keep its own return count within SonarCloud's limit
+ // (php:S1142).
+ private function isAliasAttemptStillInFlight(array $details, ?Card $selectedCard): bool
+ {
+ // The customer switching to a *different, currently selected* saved card after an
+ // earlier alias attempt was created for another one is NOT "in flight" — the caller
+ // must fall through and re-evaluate as a fresh attempt, rather than silently
+ // re-polling the stale one. But switching to "other"/no selection at all gives no new
+ // card to justify a fresh attempt, so the still-unresolved earlier alias attempt stays
+ // in flight rather than dispatching a second, independent capture for the same
+ // payment (double-charge risk if the abandoned attempt later resolves too).
+ if (null === $selectedCard) {
+ return true;
+ }
+
+ return $selectedCard->getExternalId() === ($details['alias_id'] ?? null);
+ }
+}
diff --git a/src/Command/Provider/CapturePaymentRequestCommandProvider.php b/src/Command/Provider/CapturePaymentRequestCommandProvider.php
index bb507e7e..61411803 100644
--- a/src/Command/Provider/CapturePaymentRequestCommandProvider.php
+++ b/src/Command/Provider/CapturePaymentRequestCommandProvider.php
@@ -10,6 +10,7 @@
use Sylius\Bundle\PaymentBundle\CommandProvider\PaymentRequestCommandProviderInterface;
use Sylius\Component\Payment\Model\PaymentRequestInterface;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
+use Symfony\Component\DependencyInjection\Attribute\Autowire;
#[AutoconfigureTag(
'payplug_sylius_payplug_plugin.command_provider.payplug',
@@ -41,6 +42,14 @@
)]
final class CapturePaymentRequestCommandProvider implements PaymentRequestCommandProviderInterface
{
+ use DelegatesToHostedFieldsCommandProviderTrait;
+
+ public function __construct(
+ #[Autowire(service: CaptureHostedPaymentRequestCommandProvider::class)]
+ private PaymentRequestCommandProviderInterface $hostedFieldsCommandProvider,
+ ) {
+ }
+
public function supports(PaymentRequestInterface $paymentRequest): bool
{
return $paymentRequest->getAction() === PaymentRequestInterface::ACTION_CAPTURE;
@@ -48,6 +57,11 @@ public function supports(PaymentRequestInterface $paymentRequest): bool
public function provide(PaymentRequestInterface $paymentRequest): object
{
+ $hostedFieldsResult = $this->delegateToHostedFieldsCommandProvider($paymentRequest, $this->hostedFieldsCommandProvider);
+ if (null !== $hostedFieldsResult) {
+ return $hostedFieldsResult;
+ }
+
if ($this->isAlreadyCreated($paymentRequest)) {
// The payment has already been created, let's use the offline capture request to be redirected to the thank-you page
return new OfflineCapturePaymentRequest($paymentRequest->getId());
diff --git a/src/Command/Provider/DelegatesToHostedFieldsCommandProviderTrait.php b/src/Command/Provider/DelegatesToHostedFieldsCommandProviderTrait.php
new file mode 100644
index 00000000..3232d6cc
--- /dev/null
+++ b/src/Command/Provider/DelegatesToHostedFieldsCommandProviderTrait.php
@@ -0,0 +1,29 @@
+getPayment()->getMethod()?->getGatewayConfig())) {
+ return null;
+ }
+
+ return $hostedFieldsCommandProvider->provide($paymentRequest);
+ }
+}
diff --git a/src/Command/Provider/NotifyHostedPaymentRequestCommandProvider.php b/src/Command/Provider/NotifyHostedPaymentRequestCommandProvider.php
new file mode 100644
index 00000000..f16d1340
--- /dev/null
+++ b/src/Command/Provider/NotifyHostedPaymentRequestCommandProvider.php
@@ -0,0 +1,27 @@
+getAction() === PaymentRequestInterface::ACTION_NOTIFY;
+ }
+
+ public function provide(PaymentRequestInterface $paymentRequest): object
+ {
+ return new NotifyHostedPaymentRequest($paymentRequest->getId());
+ }
+}
diff --git a/src/Command/Provider/NotifyPaymentRequestCommandProvider.php b/src/Command/Provider/NotifyPaymentRequestCommandProvider.php
index 4ef91b6a..11deb9df 100644
--- a/src/Command/Provider/NotifyPaymentRequestCommandProvider.php
+++ b/src/Command/Provider/NotifyPaymentRequestCommandProvider.php
@@ -8,6 +8,7 @@
use Sylius\Bundle\PaymentBundle\CommandProvider\PaymentRequestCommandProviderInterface;
use Sylius\Component\Payment\Model\PaymentRequestInterface;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
+use Symfony\Component\DependencyInjection\Attribute\Autowire;
#[AutoconfigureTag(
'payplug_sylius_payplug_plugin.command_provider.payplug',
@@ -39,6 +40,14 @@
)]
final class NotifyPaymentRequestCommandProvider implements PaymentRequestCommandProviderInterface
{
+ use DelegatesToHostedFieldsCommandProviderTrait;
+
+ public function __construct(
+ #[Autowire(service: NotifyHostedPaymentRequestCommandProvider::class)]
+ private PaymentRequestCommandProviderInterface $hostedFieldsCommandProvider,
+ ) {
+ }
+
public function supports(PaymentRequestInterface $paymentRequest): bool
{
return $paymentRequest->getAction() === PaymentRequestInterface::ACTION_NOTIFY;
@@ -46,6 +55,8 @@ public function supports(PaymentRequestInterface $paymentRequest): bool
public function provide(PaymentRequestInterface $paymentRequest): object
{
- return new NotifyPaymentRequest($paymentRequest->getId());
+ $hostedFieldsResult = $this->delegateToHostedFieldsCommandProvider($paymentRequest, $this->hostedFieldsCommandProvider);
+
+ return $hostedFieldsResult ?? new NotifyPaymentRequest($paymentRequest->getId());
}
}
diff --git a/src/Command/Provider/StatusHostedPaymentRequestCommandProvider.php b/src/Command/Provider/StatusHostedPaymentRequestCommandProvider.php
new file mode 100644
index 00000000..9f79b89b
--- /dev/null
+++ b/src/Command/Provider/StatusHostedPaymentRequestCommandProvider.php
@@ -0,0 +1,37 @@
+getAction() === PaymentRequestInterface::ACTION_STATUS;
+ }
+
+ public function provide(PaymentRequestInterface $paymentRequest): object
+ {
+ $request = $this->requestStack->getCurrentRequest();
+ if (null === $request) {
+ return new StatusHostedPaymentRequest($paymentRequest->getId());
+ }
+
+ return new StatusHostedPaymentRequest($paymentRequest->getId(), $request->query->getString('status'));
+ }
+}
diff --git a/src/Command/Provider/StatusPaymentRequestCommandProvider.php b/src/Command/Provider/StatusPaymentRequestCommandProvider.php
index 37676e9b..fd39c7f2 100644
--- a/src/Command/Provider/StatusPaymentRequestCommandProvider.php
+++ b/src/Command/Provider/StatusPaymentRequestCommandProvider.php
@@ -8,6 +8,7 @@
use Sylius\Bundle\PaymentBundle\CommandProvider\PaymentRequestCommandProviderInterface;
use Sylius\Component\Payment\Model\PaymentRequestInterface;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
+use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\RequestStack;
#[AutoconfigureTag(
@@ -40,8 +41,13 @@
)]
final class StatusPaymentRequestCommandProvider implements PaymentRequestCommandProviderInterface
{
- public function __construct(private RequestStack $requestStack)
- {
+ use DelegatesToHostedFieldsCommandProviderTrait;
+
+ public function __construct(
+ private RequestStack $requestStack,
+ #[Autowire(service: StatusHostedPaymentRequestCommandProvider::class)]
+ private PaymentRequestCommandProviderInterface $hostedFieldsCommandProvider,
+ ) {
}
public function supports(PaymentRequestInterface $paymentRequest): bool
@@ -51,6 +57,11 @@ public function supports(PaymentRequestInterface $paymentRequest): bool
public function provide(PaymentRequestInterface $paymentRequest): object
{
+ $hostedFieldsResult = $this->delegateToHostedFieldsCommandProvider($paymentRequest, $this->hostedFieldsCommandProvider);
+ if (null !== $hostedFieldsResult) {
+ return $hostedFieldsResult;
+ }
+
$request = $this->requestStack->getCurrentRequest();
if (null === $request) {
return new StatusPaymentRequest($paymentRequest->getId());
diff --git a/src/Command/StatusHostedPaymentRequest.php b/src/Command/StatusHostedPaymentRequest.php
new file mode 100644
index 00000000..9a33facf
--- /dev/null
+++ b/src/Command/StatusHostedPaymentRequest.php
@@ -0,0 +1,18 @@
+forcedStatus;
+ }
+}
diff --git a/src/Controller/CardController.php b/src/Controller/CardController.php
index d26770e0..5b3167e1 100644
--- a/src/Controller/CardController.php
+++ b/src/Controller/CardController.php
@@ -6,14 +6,13 @@
use Doctrine\Persistence\ManagerRegistry;
use Payplug\Exception\NotFoundException;
-use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientInterface;
+use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientFactoryInterface;
use PayPlug\SyliusPayPlugPlugin\Entity\Card;
use PayPlug\SyliusPayPlugPlugin\Entity\CardsOwnerInterface;
use Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository;
use Sylius\Component\Core\Model\CustomerInterface;
use Sylius\Component\Customer\Context\CustomerContextInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
-use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\AsController;
@@ -27,8 +26,7 @@ public function __construct(
private CustomerContextInterface $customerContext,
private EntityRepository $payplugCardRepository,
private TranslatorInterface $translator,
- #[Autowire('@payplug_sylius_payplug_plugin.api_client.payplug')]
- private PayPlugApiClientInterface $payPlugApiClient,
+ private PayPlugApiClientFactoryInterface $apiClientFactory,
private RequestStack $requestStack,
private ManagerRegistry $managerRegistry,
) {
@@ -73,7 +71,12 @@ public function deleteAction(int $id): Response
$cardToken = $card->getExternalId();
try {
- $this->payPlugApiClient->deleteCard($cardToken);
+ // Signed with the account the card was saved on, not an arbitrary CB config: PayPlug
+ // card tokens are account-scoped, and since PRE-3628 a customer may hold cards from
+ // several channels, each on a different account. Resolving from the ambient channel
+ // context would be wrong for exactly that reason — a Sylius customer spans channels,
+ // so the card's own payment method is the only correct source.
+ $this->apiClientFactory->createForPaymentMethod($card->getPaymentMethod())->deleteCard($cardToken);
} catch (NotFoundException) {
$this->requestStack->getSession()->getFlashBag()->add('error', $this->translator->trans('payplug_sylius_payplug_plugin.ui.account.saved_cards.deleted_error'));
diff --git a/src/Controller/IntegratedPaymentController.php b/src/Controller/IntegratedPaymentController.php
index a5b53925..caaeeb43 100644
--- a/src/Controller/IntegratedPaymentController.php
+++ b/src/Controller/IntegratedPaymentController.php
@@ -5,7 +5,7 @@
namespace PayPlug\SyliusPayPlugPlugin\Controller;
use Doctrine\ORM\EntityManagerInterface;
-use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientFactory;
+use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientFactoryInterface;
use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientInterface;
use PayPlug\SyliusPayPlugPlugin\Creator\PayPlugPaymentDataCreator;
use PayPlug\SyliusPayPlugPlugin\Gateway\PayPlugGatewayFactory;
@@ -35,7 +35,7 @@ public function __construct(
private RepositoryInterface $paymentMethodRepository,
private OrderRepositoryInterface $orderRepository,
private PayPlugPaymentDataCreator $paymentDataCreator,
- private PayPlugApiClientFactory $apiClientFactory,
+ private PayPlugApiClientFactoryInterface $apiClientFactory,
private EntityManagerInterface $entityManager,
private LoggerInterface $logger,
) {
@@ -52,11 +52,6 @@ public function __construct(
#[Route(path: '/{_locale}/payplug/integrated-payment/init/{paymentMethodId}', name: 'payplug_sylius_integrated_payment_init', methods: ['GET', 'POST'])]
public function initPaymentAction(Request $request, int $paymentMethodId): Response
{
- $paymentMethod = $this->paymentMethodRepository->find($paymentMethodId);
- if (!$paymentMethod instanceof PaymentMethodInterface) {
- throw $this->createNotFoundException();
- }
-
$order = null;
if (\is_string($orderToken = $request->query->get('orderToken'))) {
$order = $this->orderRepository->findOneByTokenValue($orderToken);
@@ -73,18 +68,17 @@ public function initPaymentAction(Request $request, int $paymentMethodId): Respo
throw $this->createNotFoundException('No payment available');
}
+ // The id is shopper-supplied, so it is resolved against the order before anything is
+ // done with it — see resolvePaymentMethod().
+ $paymentMethod = $this->resolvePaymentMethod($paymentMethodId, $order);
$payment->setMethod($paymentMethod);
- $factoryName = $paymentMethod->getGatewayConfig()?->getFactoryName();
- if (PayPlugGatewayFactory::FACTORY_NAME !== $factoryName) {
- throw new BadRequestHttpException('Unsupported payment method of Integrated Payment');
- }
$paymentData = $this->paymentDataCreator->create($payment);
// Mandatory
$paymentData['integration'] = PayPlugApiClientInterface::INTEGRATED_PAYMENT_INTEGRATION;
$this->logger->debug('Payplug Payment data for creation', $paymentData->getArrayCopy());
- $apiClient = $this->apiClientFactory->create($factoryName);
+ $apiClient = $this->apiClientFactory->createForPaymentMethod($paymentMethod);
$payplugPayment = $apiClient->createPayment($paymentData->getArrayCopy());
$this->logger->debug('PayPlug payment created', (array) $payplugPayment);
@@ -98,4 +92,60 @@ public function initPaymentAction(Request $request, int $paymentMethodId): Respo
'payment_id' => $payplugPayment->id,
], Response::HTTP_CREATED);
}
+
+ /**
+ * Resolves the shopper-supplied payment method id, refusing anything the order could not
+ * legitimately be paid with.
+ *
+ * The channel check is the load-bearing one. Since PRE-3628 several CB gateway configs may
+ * coexist, one per channel, each connected to a different PayPlug account, and
+ * createForPaymentMethod() faithfully resolves whichever account this id names. Without it a
+ * shopper checking out on channel A could pass channel B's payment method id and have the
+ * payment created on merchant B's account, while the IPN — resolving the same method — verifies
+ * against B's key and marks order A paid.
+ */
+ private function resolvePaymentMethod(int $paymentMethodId, OrderInterface $order): PaymentMethodInterface
+ {
+ $paymentMethod = $this->paymentMethodRepository->find($paymentMethodId);
+
+ if (!$paymentMethod instanceof PaymentMethodInterface) {
+ throw $this->createNotFoundException();
+ }
+
+ if (PayPlugGatewayFactory::FACTORY_NAME !== $paymentMethod->getGatewayConfig()?->getFactoryName()) {
+ throw new BadRequestHttpException('Unsupported payment method of Integrated Payment');
+ }
+
+ if (!$paymentMethod->isEnabled()) {
+ throw new BadRequestHttpException('The payment method is not enabled.');
+ }
+
+ if (!$this->isAvailableOnChannelOf($paymentMethod, $order)) {
+ throw new BadRequestHttpException('The payment method is not available on this channel.');
+ }
+
+ return $paymentMethod;
+ }
+
+ /**
+ * Matched on channel code rather than object identity, the same way
+ * GatewayChannelConflictChecker does: the order and the payment method may carry channel
+ * instances from different unit-of-work states.
+ */
+ private function isAvailableOnChannelOf(PaymentMethodInterface $paymentMethod, OrderInterface $order): bool
+ {
+ $channelCode = $order->getChannel()?->getCode();
+
+ if (null === $channelCode) {
+ return false;
+ }
+
+ foreach ($paymentMethod->getChannels() as $channel) {
+ if ($channelCode === $channel->getCode()) {
+ return true;
+ }
+ }
+
+ return false;
+ }
}
diff --git a/src/Controller/IpnAction.php b/src/Controller/IpnAction.php
index ee7ca7c5..ec4709e0 100644
--- a/src/Controller/IpnAction.php
+++ b/src/Controller/IpnAction.php
@@ -27,7 +27,21 @@
use Symfony\Component\Routing\Attribute\Route;
use Webmozart\Assert\Assert;
-/** @deprecated */
+/**
+ * @deprecated Legacy Payum-era static webhook receiver for the non-Unified-API (SDK-based)
+ * gateways — Oney, Bancontact, Apple Pay, and the legacy card flow. Superseded by
+ * Sylius's native per-payment-method notify mechanism: NotifyPaymentProvider and
+ * NotifyRefundPaymentProvider (both #[AsNotifyPaymentProvider]) already route these
+ * gateways' real notifications through sylius_payment_method_notify
+ * (/payment-methods/{code}) instead, via the notification_url PayPlugPaymentDataCreator
+ * sends at payment-creation time. Unified API traffic (Hosted Fields and any future
+ * UPC-backed method) never used this branch — see UnifiedApiIpnAction instead, which
+ * needs its own fixed, parameter-less URL since PayPlug's Unified API notifier
+ * Receiver is configured once per merchant in Cockpit and cannot target a
+ * per-payment-method route. Kept, rather than deleted outright, until it's confirmed
+ * no already-onboarded merchant's account still has a webhook pointed at this route
+ * for the legacy flow.
+ */
#[AsController]
class IpnAction
{
@@ -76,7 +90,7 @@ public function __invoke(Request $request): JsonResponse
if (
!$paymentMethod->getGatewayConfig() instanceof GatewayConfigInterface ||
- !\in_array($factoryName = $paymentMethod->getGatewayConfig()->getFactoryName(), [
+ !\in_array($paymentMethod->getGatewayConfig()->getFactoryName(), [
PayPlugGatewayFactory::FACTORY_NAME,
OneyGatewayFactory::FACTORY_NAME,
BancontactGatewayFactory::FACTORY_NAME,
@@ -86,7 +100,7 @@ public function __invoke(Request $request): JsonResponse
return new JsonResponse(null, Response::HTTP_UNAUTHORIZED);
}
- $this->payPlugApiClient = $this->apiClientFactory->create($factoryName);
+ $this->payPlugApiClient = $this->apiClientFactory->createForPaymentMethod($paymentMethod);
try {
$resource = $this->payPlugApiClient->treat($input);
diff --git a/src/Controller/OneClickAction.php b/src/Controller/OneClickAction.php
index a1e27ab2..9128e584 100644
--- a/src/Controller/OneClickAction.php
+++ b/src/Controller/OneClickAction.php
@@ -5,7 +5,7 @@
namespace PayPlug\SyliusPayPlugPlugin\Controller;
use PayPlug\SyliusPayPlugPlugin\Action\Api\ApiAwareTrait;
-use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientFactory;
+use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientFactoryInterface;
use Payum\Core\ApiAwareInterface;
use Payum\Core\GatewayAwareInterface;
use Payum\Core\GatewayAwareTrait;
@@ -33,7 +33,7 @@ class OneClickAction extends AbstractController implements GatewayAwareInterface
public function __construct(
private PaymentRepositoryInterface $paymentRepository,
private Payum $payum,
- private PayPlugApiClientFactory $payPlugApiClientFactory,
+ private PayPlugApiClientFactoryInterface $payPlugApiClientFactory,
) {
}
diff --git a/src/Controller/UnifiedApiIpnAction.php b/src/Controller/UnifiedApiIpnAction.php
new file mode 100644
index 00000000..f87b9d8e
--- /dev/null
+++ b/src/Controller/UnifiedApiIpnAction.php
@@ -0,0 +1,137 @@
+getContent();
+ $payment = $this->resolveHostedFieldsPayment($input);
+
+ if (null === $payment) {
+ return new JsonResponse(null, Response::HTTP_UNAUTHORIZED);
+ }
+
+ try {
+ $this->hostedFieldsWebhookNotificationHandler->treat($payment, $input, self::flattenHeaders($request->headers->all()));
+ } catch (InvalidNotificationException $exception) {
+ $this->logger->error('[PayPlug][UPC] Rejected webhook notification.', ['error' => $exception->getMessage()]);
+ }
+
+ return new JsonResponse();
+ }
+
+ // Split out of __invoke() to keep its own return count within SonarCloud's limit (php:S1142)
+ // — collapses the three "reject this notification" conditions (missing id, unknown payment,
+ // wrong gateway) into a single null-or-not result.
+ private function resolveHostedFieldsPayment(string $input): ?PaymentInterface
+ {
+ $content = json_decode($input, true);
+ $id = \is_array($content) ? ($content['id'] ?? null) : null;
+ if (!\is_string($id) || '' === $id) {
+ // if we are too fast canceling a payment before we got an answer from PayPlug gateway
+ return null;
+ }
+
+ $payment = $this->findPaymentWithRetry($id);
+ if (null === $payment) {
+ return null;
+ }
+
+ $paymentMethod = $payment->getMethod();
+ Assert::isInstanceOf($paymentMethod, PaymentMethodInterface::class);
+ $gateway = $paymentMethod->getGatewayConfig();
+ Assert::isInstanceOf($gateway, GatewayConfigInterface::class);
+
+ // Defensive: this route is dedicated to Unified API traffic, so a resolved payment that
+ // isn't actually on a Unified API-backed config (currently, only Hosted Fields) is
+ // rejected rather than guessed at. Update this check if/when a second Unified API-backed
+ // payment method is added.
+ return PayPlugGatewayFactory::isHostedFieldsConfig($gateway) ? $payment : null;
+ }
+
+ private function findPaymentWithRetry(string $id): ?PaymentInterface
+ {
+ for ($attempt = 1; $attempt <= self::PAYMENT_RESOLUTION_MAX_ATTEMPTS; ++$attempt) {
+ $payment = $this->paymentRepository->findOneByPayPlugPaymentId($id);
+ if (null !== $payment) {
+ return $payment;
+ }
+
+ if ($attempt < self::PAYMENT_RESOLUTION_MAX_ATTEMPTS) {
+ usleep(self::PAYMENT_RESOLUTION_RETRY_DELAY_MICROSECONDS);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * @param array> $rawHeaders
+ *
+ * @return array
+ */
+ private static function flattenHeaders(array $rawHeaders): array
+ {
+ $headers = [];
+ foreach ($rawHeaders as $name => $values) {
+ $headers[$name] = $values[0] ?? '';
+ }
+
+ return $headers;
+ }
+}
diff --git a/src/Creator/PayPlugPaymentDataCreator.php b/src/Creator/PayPlugPaymentDataCreator.php
index 9b437e68..7b7000df 100644
--- a/src/Creator/PayPlugPaymentDataCreator.php
+++ b/src/Creator/PayPlugPaymentDataCreator.php
@@ -21,6 +21,7 @@
use PayPlug\SyliusPayPlugPlugin\Gateway\PayPlugGatewayFactory;
use PayPlug\SyliusPayPlugPlugin\Gateway\ScalapayGatewayFactory;
use PayPlug\SyliusPayPlugPlugin\Gateway\WeroGatewayFactory;
+use PayPlug\SyliusPayPlugPlugin\Upc\CustomerTitleResolver;
use Sylius\Component\Core\Model\AddressInterface;
use Sylius\Component\Core\Model\CustomerInterface;
use Sylius\Component\Core\Model\OrderInterface;
@@ -37,8 +38,6 @@ class PayPlugPaymentDataCreator
private const DELIVERY_TYPE_NEW = 'NEW';
- private const PAYPLUG_CARD_ID_OTHER = 'other';
-
public function __construct(
private CanSaveCardCheckerInterface $canSaveCardChecker,
private RepositoryInterface $payplugCardRepository,
@@ -142,9 +141,9 @@ public function formatNumber(string $phoneNumber, ?string $isoCode): array
private function formatTitle(CustomerInterface $customer): ?string
{
- $gender = $customer->getGender();
+ $title = CustomerTitleResolver::resolve($customer->getGender());
- return 'm' === $gender ? 'mr' : ('f' === $gender ? 'mrs' : null);
+ return null !== $title ? strtolower($title) : null;
}
private function formatLanguageCode(?string $languageCode): ?string
@@ -252,7 +251,7 @@ private function alterPayPlugDetailsForOneClick(
$cardId = $this->requestStack->getSession()->get('payplug_payment_method');
if (
- (null === $cardId || self::PAYPLUG_CARD_ID_OTHER === $cardId) && $this->canSaveCardChecker->isAllowed(
+ (null === $cardId || PayPlugGatewayFactory::CARD_CHOICE_OTHER === $cardId) && $this->canSaveCardChecker->isAllowed(
$paymentMethod,
)
) {
diff --git a/src/Entity/Card.php b/src/Entity/Card.php
index 147bdaeb..2f749b3c 100644
--- a/src/Entity/Card.php
+++ b/src/Entity/Card.php
@@ -17,6 +17,7 @@
*/
#[ORM\Entity]
#[ORM\Table(name: 'payplug_cards')]
+#[ORM\UniqueConstraint(name: 'UNIQ_payplug_cards_external_id_is_live', columns: ['external_id', 'is_live'])]
class Card implements ResourceInterface
{
/**
diff --git a/src/Entity/PayPlugOperation.php b/src/Entity/PayPlugOperation.php
new file mode 100644
index 00000000..912f2b34
--- /dev/null
+++ b/src/Entity/PayPlugOperation.php
@@ -0,0 +1,145 @@
+orderId = $orderId;
+ $this->operationId = $operationId;
+ $this->execCode = $execCode;
+ $this->outcome = $outcome;
+ $this->amount = $amount;
+ $this->createdAt = new \DateTimeImmutable();
+ }
+
+ public function getId(): ?int
+ {
+ return $this->id;
+ }
+
+ public function getOrderId(): string
+ {
+ return $this->orderId;
+ }
+
+ public function getOperationId(): string
+ {
+ return $this->operationId;
+ }
+
+ public function getExecCode(): string
+ {
+ return $this->execCode;
+ }
+
+ public function getOutcome(): string
+ {
+ return $this->outcome;
+ }
+
+ public function getAmount(): int
+ {
+ return $this->amount;
+ }
+
+ public function isTreated(): bool
+ {
+ return $this->treated;
+ }
+
+ public function markTreated(): void
+ {
+ $this->treated = true;
+ }
+
+ public function toOperationData(): OperationData
+ {
+ return new OperationData($this->operationId, $this->execCode, $this->outcome, $this->amount, $this->orderId);
+ }
+}
diff --git a/src/EventSubscriber/PostPaymentSelectEventSubscriber.php b/src/EventSubscriber/PostPaymentSelectEventSubscriber.php
index 9991e0b3..d633b4b1 100644
--- a/src/EventSubscriber/PostPaymentSelectEventSubscriber.php
+++ b/src/EventSubscriber/PostPaymentSelectEventSubscriber.php
@@ -5,6 +5,9 @@
namespace PayPlug\SyliusPayPlugPlugin\EventSubscriber;
use Doctrine\ORM\EntityManagerInterface;
+use PayPlug\SyliusPayPlugPlugin\Gateway\PayPlugGatewayFactory;
+use PayPlug\SyliusPayPlugPlugin\PaymentProcessing\HostedFieldsCaptureData;
+use PayPlug\SyliusPayPlugPlugin\PaymentProcessing\HostedFieldsPaymentProcessorInterface;
use Sylius\Abstraction\StateMachine\StateMachineInterface;
use Sylius\Bundle\ResourceBundle\Event\ResourceControllerEvent;
use Sylius\Component\Core\Model\OrderInterface;
@@ -21,30 +24,60 @@ final class PostPaymentSelectEventSubscriber implements EventSubscriberInterface
{
private const CHECKOUT_ROUTE = 'sylius_shop_checkout_select_payment';
- private const UPDATE_ORDER_PAYMENT_ROUTE = 'sylius_shop_order_show';
-
private const TOKEN_FIELD = 'payplug_integrated_payment_token';
+ private const HOSTED_FIELDS_TOKEN_FIELD = 'hostedfields_token';
+
+ private const HOSTED_FIELDS_SELECTED_BRAND_FIELD = 'hostedfields_selected_brand';
+
+ private const HOSTED_FIELDS_SAVE_CARD_FIELD = 'hostedfields_save_card';
+
+ private const HOSTED_FIELDS_LAST4_FIELD = 'hostedfields_last4';
+
+ private const HOSTED_FIELDS_EXP_MONTH_FIELD = 'hostedfields_exp_month';
+
+ private const HOSTED_FIELDS_EXP_YEAR_FIELD = 'hostedfields_exp_year';
+
+ private const HOSTED_FIELDS_COUNTRY_FIELD = 'hostedfields_country';
+
public function __construct(
private RequestStack $requestStack,
private EntityManagerInterface $entityManager,
private StateMachineInterface $stateMachine,
+ private HostedFieldsPaymentProcessorInterface $hostedFieldsPaymentProcessor,
) {
}
public static function getSubscribedEvents(): array
{
return [
- RequestEvent::class => 'alterRequestConfigurationForIntegratedPayment',
+ RequestEvent::class => 'alterRequestConfigurationForInlineCardCapture',
'sylius.order.post_payment' => 'handle',
'sylius.order.post_update' => 'handle',
];
}
- public function alterRequestConfigurationForIntegratedPayment(RequestEvent $event): void
+ /**
+ * Both inline card-capture modes force the checkout to TRANSITION_COMPLETE inside
+ * `sylius.order.post_payment` (see handle()), so a `redirect` entry MUST be injected here:
+ * Sylius's CheckoutRedirectListener listens to that same event and bails out only when
+ * `_sylius['redirect']` is set. Without it, it would resolve a route for the `completed`
+ * checkout state, which has no entry in `sylius_shop.checkout_resolver.route_map`
+ * (RouteNotFoundException).
+ *
+ * Both Integrated Payment and Hosted Fields target `sylius_shop_order_pay` (Payum
+ * capture/status for Integrated Payment; for Hosted Fields, the same `payplug`-tagged
+ * Capture/Notify/StatusPaymentRequestCommandProvider trio delegates to their
+ * Hosted-Fields-specific counterparts — see PayPlugGatewayFactory::isHostedFieldsConfig() —
+ * so the payment is actually created/confirmed through UPC.
+ */
+ public function alterRequestConfigurationForInlineCardCapture(RequestEvent $event): void
{
$request = $event->getRequest();
- if (!$this->hasToken($request) || self::CHECKOUT_ROUTE !== $request->attributes->get('_route')) {
+ if (
+ (!$this->hasToken($request) && !$this->hasHostedFieldsToken($request)) ||
+ self::CHECKOUT_ROUTE !== $request->attributes->get('_route')
+ ) {
return;
}
if (!$request->attributes->has('_sylius')) {
@@ -71,7 +104,7 @@ public function handle(ResourceControllerEvent $resourceControllerEvent): void
return;
}
- if (!\in_array($request->attributes->get('_route'), [self::CHECKOUT_ROUTE, self::UPDATE_ORDER_PAYMENT_ROUTE], true)) {
+ if (self::CHECKOUT_ROUTE !== $request->attributes->get('_route')) {
return;
}
@@ -82,6 +115,12 @@ public function handle(ResourceControllerEvent $resourceControllerEvent): void
return;
}
+ if ($this->hasHostedFieldsToken($request)) {
+ $this->handleHostedFieldsToken($request, $lastPayment);
+
+ return;
+ }
+
if (!$this->hasToken($request)) {
return;
}
@@ -128,6 +167,63 @@ private function getToken(Request $request): string
return $token;
}
+ private function hasHostedFieldsToken(Request $request): bool
+ {
+ if (!$request->request->has(self::HOSTED_FIELDS_TOKEN_FIELD)) {
+ return false;
+ }
+
+ return '' !== $this->getRequestField($request, self::HOSTED_FIELDS_TOKEN_FIELD);
+ }
+
+ private function handleHostedFieldsToken(Request $request, PaymentInterface $lastPayment): void
+ {
+ // Guard against a crafted POST completing checkout through this path for a payment
+ // method that does not actually have Hosted Fields enabled.
+ if (!$this->isHostedFieldsEnabled($lastPayment)) {
+ return;
+ }
+
+ $hfToken = $this->getRequestField($request, self::HOSTED_FIELDS_TOKEN_FIELD);
+ $selectedBrand = $this->getRequestField($request, self::HOSTED_FIELDS_SELECTED_BRAND_FIELD);
+ $saveCard = 'true' === $request->request->get(self::HOSTED_FIELDS_SAVE_CARD_FIELD, 'false');
+ $last4 = $this->getRequestField($request, self::HOSTED_FIELDS_LAST4_FIELD);
+ $expirationMonth = $this->getOptionalIntRequestField($request, self::HOSTED_FIELDS_EXP_MONTH_FIELD);
+ $expirationYear = $this->getOptionalIntRequestField($request, self::HOSTED_FIELDS_EXP_YEAR_FIELD);
+ $countryCode = $this->getRequestField($request, self::HOSTED_FIELDS_COUNTRY_FIELD);
+
+ $this->hostedFieldsPaymentProcessor->process(
+ $lastPayment,
+ new HostedFieldsCaptureData($hfToken, $selectedBrand, $saveCard, $last4, $expirationMonth, $expirationYear, $countryCode),
+ );
+
+ $this->applyToComplete($lastPayment->getOrder() ?? throw new \LogicException('Order not found for payment'));
+ }
+
+ private function isHostedFieldsEnabled(PaymentInterface $payment): bool
+ {
+ return PayPlugGatewayFactory::isHostedFieldsConfig($payment->getMethod()?->getGatewayConfig());
+ }
+
+ private function getRequestField(Request $request, string $field): string
+ {
+ $value = $request->request->get($field, '');
+ Assert::string($value);
+
+ return $value;
+ }
+
+ /**
+ * Distinguishes a genuinely absent field from a legitimately-fetched 0, unlike a plain
+ * (int) cast on the empty-string default, which would collapse both to the same value.
+ */
+ private function getOptionalIntRequestField(Request $request, string $field): ?int
+ {
+ $value = $this->getRequestField($request, $field);
+
+ return '' === $value ? null : (int) $value;
+ }
+
private function applyToComplete(OrderInterface $order): void
{
if ($this->stateMachine->can($order, OrderCheckoutTransitions::GRAPH, OrderCheckoutTransitions::TRANSITION_COMPLETE)) {
diff --git a/src/Exception/Payment/AuthorizationOperationException.php b/src/Exception/Payment/AuthorizationOperationException.php
new file mode 100644
index 00000000..1f7aed60
--- /dev/null
+++ b/src/Exception/Payment/AuthorizationOperationException.php
@@ -0,0 +1,38 @@
+ $translationParameters
+ */
+ public function __construct(
+ private string $translationKey,
+ private array $translationParameters = [],
+ ?\Throwable $previous = null,
+ ) {
+ parent::__construct($translationKey, 0, $previous);
+ }
+
+ public function getTranslationKey(): string
+ {
+ return $this->translationKey;
+ }
+
+ /**
+ * @return array
+ */
+ public function getTranslationParameters(): array
+ {
+ return $this->translationParameters;
+ }
+}
diff --git a/src/Gateway/AbstractGatewayFactory.php b/src/Gateway/AbstractGatewayFactory.php
index 8d7f4d11..e743a202 100644
--- a/src/Gateway/AbstractGatewayFactory.php
+++ b/src/Gateway/AbstractGatewayFactory.php
@@ -17,6 +17,18 @@ abstract class AbstractGatewayFactory extends GatewayFactory
public const BASE_CURRENCY_CODE = 'EUR';
+ /**
+ * @deprecated Never called — this class only extends Payum's GatewayFactory for historical
+ * reasons. populateConfig() runs when Payum builds a gateway, and no Payum gateway
+ * is ever registered for the `payplug*` factory names: the only
+ * `payum.gateway_factory_builder` tags live in config/services/gateway.xml, which
+ * PayPlugSyliusPayPlugExtension::load() does not load. `payum.api` and
+ * `payum.http_client` are therefore never assigned.
+ *
+ * The class itself is NOT deprecated — it remains the home of FACTORY_NAME,
+ * FACTORY_TITLE and BASE_CURRENCY_CODE, referenced throughout the plugin.
+ * Will be removed permanently in early 2027.
+ */
protected function populateConfig(ArrayObject $config): void
{
$config->defaults([
diff --git a/src/Gateway/Form/Extension/PayPlugGatewayConfigurationTypeExtension.php b/src/Gateway/Form/Extension/PayPlugGatewayConfigurationTypeExtension.php
index a6c47a2a..0a981b72 100644
--- a/src/Gateway/Form/Extension/PayPlugGatewayConfigurationTypeExtension.php
+++ b/src/Gateway/Form/Extension/PayPlugGatewayConfigurationTypeExtension.php
@@ -9,12 +9,20 @@
use PayPlug\SyliusPayPlugPlugin\Gateway\PayPlugGatewayFactory;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
+use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
+use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
+use Symfony\Contracts\Translation\TranslatorInterface;
final class PayPlugGatewayConfigurationTypeExtension extends AbstractTypeExtension
{
+ public function __construct(private TranslatorInterface $translator)
+ {
+ }
+
/**
* @inheritdoc
*/
@@ -29,12 +37,6 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
'help_html' => true,
'required' => false,
])
- ->add(PayPlugGatewayFactory::INTEGRATED_PAYMENT, CheckboxType::class, [
- 'block_name' => 'payplug_checkbox',
- 'label' => 'payplug_sylius_payplug_plugin.form.integrated_payment_enable',
- 'validation_groups' => AbstractGatewayConfigurationType::VALIDATION_GROUPS,
- 'required' => false,
- ])
->add(PayPlugGatewayFactory::DEFERRED_CAPTURE, CheckboxType::class, [
'block_name' => 'payplug_checkbox',
'label' => 'payplug_sylius_payplug_plugin.form.deferred_capture_enable',
@@ -43,6 +45,22 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
'help_html' => true,
'required' => false,
])
+ ->add(PayPlugGatewayFactory::DISPLAY_MODE_FIELD, ChoiceType::class, [
+ 'mapped' => false,
+ 'required' => false,
+ 'expanded' => true,
+ 'placeholder' => 'payplug_sylius_payplug_plugin.form.redirected_payment_enable',
+ 'validation_groups' => AbstractGatewayConfigurationType::VALIDATION_GROUPS,
+ 'choices' => [
+ 'payplug_sylius_payplug_plugin.form.integrated_payment_enable' => PayPlugGatewayFactory::DISPLAY_MODE_INTEGRATED_PAYMENT,
+ 'payplug_sylius_payplug_plugin.ui.hosted_fields_option' => PayPlugGatewayFactory::DISPLAY_MODE_HOSTED_FIELDS,
+ ],
+ ])
+ ->add(PayPlugGatewayFactory::HF_IDENTIFIER, TextType::class, [
+ 'label' => 'payplug_sylius_payplug_plugin.ui.hf_identifier_label',
+ 'required' => false,
+ 'validation_groups' => AbstractGatewayConfigurationType::VALIDATION_GROUPS,
+ ])
->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event): void {
$data = $event->getData();
// phpstan check
@@ -52,6 +70,45 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
$data['payum.http_client'] = '@payplug_sylius_payplug_plugin.api_client.payplug';
$event->setData($data);
})
+ // DISPLAY_MODE_FIELD's pre-selection must happen on POST_SET_DATA, not PRE_SET_DATA:
+ // it's `mapped => false`, and Symfony's DataMapper::mapDataToForms() runs right after
+ // PRE_SET_DATA dispatches (as part of the same parent setData() call), resetting every
+ // unmapped child back to its configured (null) default — silently wiping out a
+ // setData() call made from PRE_SET_DATA. POST_SET_DATA fires after that reset, so
+ // nothing overwrites it afterward.
+ ->addEventListener(FormEvents::POST_SET_DATA, function (FormEvent $event): void {
+ $data = $event->getData();
+ if (!is_array($data)) {
+ return;
+ }
+
+ $event->getForm()->get(PayPlugGatewayFactory::DISPLAY_MODE_FIELD)->setData(
+ PayPlugGatewayFactory::resolveDisplayMode($data),
+ );
+ })
+ ->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event): void {
+ $form = $event->getForm();
+ $submittedData = [
+ PayPlugGatewayFactory::DISPLAY_MODE_FIELD => $form->get(PayPlugGatewayFactory::DISPLAY_MODE_FIELD)->getData(),
+ PayPlugGatewayFactory::HF_IDENTIFIER => $form->get(PayPlugGatewayFactory::HF_IDENTIFIER)->getData(),
+ ];
+
+ foreach (PayPlugGatewayFactory::missingHostedFieldsRequirements($submittedData) as $field) {
+ $form->get($field)->addError(new FormError(
+ $this->translator->trans('payplug_sylius_payplug_plugin.form.account_id_required'),
+ ));
+ }
+ })
+ ->addEventListener(FormEvents::SUBMIT, function (FormEvent $event): void {
+ $data = $event->getData();
+ if (!is_array($data)) {
+ return;
+ }
+
+ $displayMode = $event->getForm()->get(PayPlugGatewayFactory::DISPLAY_MODE_FIELD)->getData();
+ $displayMode = is_string($displayMode) ? $displayMode : null;
+ $event->setData(array_merge($data, PayPlugGatewayFactory::resolveDisplayModeFlags($displayMode)));
+ })
;
}
diff --git a/src/Gateway/Form/Extension/PaymentMethodTypeExtension.php b/src/Gateway/Form/Extension/PaymentMethodTypeExtension.php
new file mode 100644
index 00000000..b6e00330
--- /dev/null
+++ b/src/Gateway/Form/Extension/PaymentMethodTypeExtension.php
@@ -0,0 +1,277 @@
+addEventListener(FormEvents::POST_SET_DATA, function (FormEvent $event): void {
+ $form = $event->getForm();
+ $paymentMethod = $this->resolvePayPlugPaymentMethod($event->getData());
+
+ if (null === $paymentMethod || null === $this->resolveConfigurationType($form)) {
+ return;
+ }
+
+ /** @var GatewayConfigInterface $gatewayConfig */
+ $gatewayConfig = $paymentMethod->getGatewayConfig();
+
+ $this->disableClaimedChannelChoices($form, $paymentMethod, (string) $gatewayConfig->getFactoryName());
+ });
+
+ $builder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event): void {
+ $form = $event->getForm();
+ $configurationType = $this->resolveConfigurationType($form);
+ $paymentMethod = $this->resolvePayPlugPaymentMethod($event->getData());
+
+ if (null === $configurationType || null === $paymentMethod) {
+ return;
+ }
+
+ /** @var GatewayConfigInterface $gatewayConfig */
+ $gatewayConfig = $paymentMethod->getGatewayConfig();
+
+ $this->addChannelConflictErrors($form, $paymentMethod, (string) $gatewayConfig->getFactoryName());
+ $this->addBaseCurrencyErrors($form, $paymentMethod, $gatewayConfig, $configurationType);
+ });
+ }
+
+ public static function getExtendedTypes(): iterable
+ {
+ return [PaymentMethodType::class];
+ }
+
+ /**
+ * Returns the submitted payment method only when it carries a gateway config with a factory
+ * name; the listener pairs this with `resolveConfigurationType()` to decide whether the
+ * payment method is one of ours.
+ */
+ private function resolvePayPlugPaymentMethod(mixed $data): ?PaymentMethodInterface
+ {
+ if (!$data instanceof PaymentMethodInterface) {
+ return null;
+ }
+
+ $gatewayConfig = $data->getGatewayConfig();
+
+ $isPayPlugPaymentMethod = $gatewayConfig instanceof GatewayConfigInterface &&
+ null !== $gatewayConfig->getFactoryName();
+
+ return $isPayPlugPaymentMethod ? $data : null;
+ }
+
+ /**
+ * Resolving to non-null is what makes a submitted payment method "one of ours": the decision
+ * rests on the gateway configuration form's own type rather than on a hardcoded factory-name
+ * list, since every PayPlug gateway configuration type extends `AbstractGatewayConfigurationType`
+ * and nothing else does, so the test stays exact when an eighth gateway is added.
+ *
+ * `GatewayConfigType` only adds the `config` child when the factory has a registered
+ * configuration type, hence the `has()` guards.
+ *
+ * The per-gateway currency policy is read back off the configuration type instance rather than
+ * duplicated into a registry here: it already lives one-class-per-gateway, and only the CB type
+ * narrows it (to Integrated Payment). Form types are stateless services, so calling their
+ * public hooks is safe.
+ */
+ private function resolveConfigurationType(FormInterface $form): ?AbstractGatewayConfigurationType
+ {
+ if (!$form->has('gatewayConfig') || !$form->get('gatewayConfig')->has('config')) {
+ return null;
+ }
+
+ $configurationType = $form->get('gatewayConfig')->get('config')->getConfig()->getType()->getInnerType();
+
+ return $configurationType instanceof AbstractGatewayConfigurationType ? $configurationType : null;
+ }
+
+ /**
+ * PRE-3629: render the channels another enabled gateway of the same factory already holds as
+ * unselectable, instead of letting the admin pick one only to be refused on submit.
+ *
+ * The field belongs to Sylius' own `CoreBundle` type extension, so it cannot be configured from
+ * our `buildForm()` — extension ordering between the two is not guaranteed. POST_SET_DATA on the
+ * root form is the first safe point: `GatewayConfigType` adds its `config` child during its own
+ * PRE_SET_DATA, so at root PRE_SET_DATA `resolveConfigurationType()` would still find nothing,
+ * whereas by root POST_SET_DATA every child is built and populated. Replacing a child there is
+ * also still data-mapped (`Form::add()` only skips mapping while `lockSetData` is on, i.e.
+ * during PRE_SET_DATA).
+ *
+ * A resolved form config cannot be mutated, hence the replace-with-copied-options approach.
+ * `data` is `setDefined` rather than defaulted on `FormType`, so it is absent from the copied
+ * options unless someone deliberately set it — in which case carrying it over is the correct
+ * thing to do anyway.
+ */
+ private function disableClaimedChannelChoices(
+ FormInterface $form,
+ PaymentMethodInterface $paymentMethod,
+ string $factoryName,
+ ): void {
+ if (!$form->has('channels')) {
+ return;
+ }
+
+ $claimedChannels = $this->conflictChecker->findClaimedChannels($paymentMethod, $factoryName);
+
+ if ([] === $claimedChannels) {
+ return;
+ }
+
+ $form->add('channels', ChannelChoiceType::class, \array_merge(
+ $form->get('channels')->getConfig()->getOptions(),
+ [
+ 'choice_attr' => fn (ChannelInterface $channel): array => $this->channelChoiceAttributes(
+ $channel,
+ $claimedChannels,
+ ),
+ ],
+ ));
+ }
+
+ /**
+ * A channel the payment method being edited already holds is never reported as claimed (see
+ * `GatewayChannelConflictChecker::findClaimedChannels()`), and that is deliberate: browsers do
+ * not submit disabled checkboxes, so disabling a *checked* one would silently drop the channel
+ * on save. Those are left selectable and reported by the POST_SUBMIT rule instead.
+ *
+ * `disabled => true` is rendered as `disabled="disabled"` by form_div_layout's `attributes`
+ * block, which every Sylius form theme inherits. That block also pipes `title` through `|trans`
+ * against the *field's* translation domain — harmless here, since re-translating an already
+ * translated sentence finds no catalogue entry and Symfony hands the string straight back. The
+ * message can't be left as a bare key for Twig to translate instead: `attr_translation_parameters`
+ * is set per field, not per choice, and each channel has to name a different payment method.
+ *
+ * @param array $claimedChannels channel code => claiming method
+ *
+ * @return array
+ */
+ private function channelChoiceAttributes(ChannelInterface $channel, array $claimedChannels): array
+ {
+ $channelCode = (string) $channel->getCode();
+ $claimedBy = $claimedChannels[$channelCode] ?? null;
+
+ if (null === $claimedBy) {
+ return [];
+ }
+
+ return [
+ 'disabled' => true,
+ 'title' => $this->translator->trans(
+ 'payplug_sylius_payplug_plugin.form.gateway_channel_conflict',
+ [
+ '%channel%' => $channelCode,
+ '%payment_method%' => (string) $claimedBy->getName(),
+ ],
+ ),
+ ];
+ }
+
+ private function addChannelConflictErrors(
+ FormInterface $form,
+ PaymentMethodInterface $paymentMethod,
+ string $factoryName,
+ ): void {
+ if (!$form->has('channels')) {
+ return;
+ }
+
+ $seenMessages = [];
+ foreach ($this->conflictChecker->findConflicts($paymentMethod, $factoryName) as $conflict) {
+ $message = $this->translator->trans(
+ 'payplug_sylius_payplug_plugin.form.gateway_channel_conflict',
+ [
+ '%channel%' => (string) $conflict['channel']->getCode(),
+ '%payment_method%' => (string) $conflict['paymentMethod']->getName(),
+ ],
+ );
+
+ if (\in_array($message, $seenMessages, true)) {
+ continue;
+ }
+
+ $seenMessages[] = $message;
+ $form->get('channels')->addError(new FormError($message));
+ }
+ }
+
+ private function addBaseCurrencyErrors(
+ FormInterface $form,
+ PaymentMethodInterface $paymentMethod,
+ GatewayConfigInterface $gatewayConfig,
+ AbstractGatewayConfigurationType $configurationType,
+ ): void {
+ if (
+ !$form->has('channels') ||
+ !$configurationType->shouldValidateBaseCurrency($gatewayConfig->getConfig())
+ ) {
+ return;
+ }
+
+ $seenMessages = [];
+ foreach ($paymentMethod->getChannels() as $channel) {
+ if (!$channel instanceof ChannelInterface) {
+ continue;
+ }
+
+ $baseCurrency = $channel->getBaseCurrency();
+
+ if (null === $baseCurrency || $configurationType->getBaseCurrencyCode() === $baseCurrency->getCode()) {
+ continue;
+ }
+
+ $message = $configurationType->baseCurrencyViolationMessage($channel);
+
+ if (\in_array($message, $seenMessages, true)) {
+ continue;
+ }
+
+ $seenMessages[] = $message;
+ $form->get('channels')->addError(new FormError($message));
+ }
+ }
+}
diff --git a/src/Gateway/Form/Extension/ScalapayGatewayConfigurationTypeExtension.php b/src/Gateway/Form/Extension/ScalapayGatewayConfigurationTypeExtension.php
new file mode 100644
index 00000000..4cecf182
--- /dev/null
+++ b/src/Gateway/Form/Extension/ScalapayGatewayConfigurationTypeExtension.php
@@ -0,0 +1,43 @@
+add(ScalapayGatewayFactory::MIN_AMOUNT, MoneyType::class, [
+ 'label' => 'payplug_sylius_payplug_plugin.ui.scalapay_gateway_config.min_amount',
+ 'help' => 'payplug_sylius_payplug_plugin.ui.scalapay_gateway_config.amount_help',
+ 'currency' => 'EUR',
+ 'required' => false,
+ 'validation_groups' => AbstractGatewayConfigurationType::VALIDATION_GROUPS,
+ ])
+ ->add(ScalapayGatewayFactory::MAX_AMOUNT, MoneyType::class, [
+ 'label' => 'payplug_sylius_payplug_plugin.ui.scalapay_gateway_config.max_amount',
+ 'help' => 'payplug_sylius_payplug_plugin.ui.scalapay_gateway_config.amount_help',
+ 'currency' => 'EUR',
+ 'required' => false,
+ 'validation_groups' => AbstractGatewayConfigurationType::VALIDATION_GROUPS,
+ ])
+ ;
+ }
+
+ public static function getExtendedTypes(): iterable
+ {
+ return [ScalapayGatewayConfigurationType::class];
+ }
+}
diff --git a/src/Gateway/Form/Type/AbstractGatewayConfigurationType.php b/src/Gateway/Form/Type/AbstractGatewayConfigurationType.php
index 80fce24a..867dbcab 100644
--- a/src/Gateway/Form/Type/AbstractGatewayConfigurationType.php
+++ b/src/Gateway/Form/Type/AbstractGatewayConfigurationType.php
@@ -4,19 +4,11 @@
namespace PayPlug\SyliusPayPlugPlugin\Gateway\Form\Type;
-use Doctrine\Common\Collections\Collection;
use PayPlug\SyliusPayPlugPlugin\Gateway\PayPlugGatewayFactory;
-use Sylius\Bundle\PayumBundle\Model\GatewayConfigInterface;
use Sylius\Component\Core\Model\ChannelInterface;
-use Sylius\Component\Resource\Repository\RepositoryInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\FormBuilderInterface;
-use Symfony\Component\Form\FormError;
-use Symfony\Component\Form\FormEvent;
-use Symfony\Component\Form\FormEvents;
-use Symfony\Component\Form\FormInterface;
-use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Contracts\Translation\TranslatorInterface;
class AbstractGatewayConfigurationType extends AbstractType
@@ -25,14 +17,10 @@ class AbstractGatewayConfigurationType extends AbstractType
protected string $gatewayFactoryTitle = '';
- protected string $gatewayFactoryName = '';
-
protected string $gatewayBaseCurrencyCode = PayPlugGatewayFactory::BASE_CURRENCY_CODE;
public function __construct(
protected TranslatorInterface $translator,
- private RepositoryInterface $gatewayConfigRepository,
- protected RequestStack $requestStack,
) {
}
@@ -56,67 +44,46 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
'mapped' => false,
'required' => false,
])
- ->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event): void {
- $this->checkCreationRequirements(
- $this->gatewayFactoryTitle,
- $this->gatewayFactoryName,
- $event->getForm(),
- );
-
- /** @phpstan-ignore-next-line */
- $formChannels = $event->getForm()->getParent()->getParent()->get('channels');
- $dataFormChannels = $formChannels->getData();
- if (!$dataFormChannels instanceof Collection) {
- return;
- }
- /** @var ChannelInterface $dataFormChannel */
- foreach ($dataFormChannels as $key => $dataFormChannel) {
- $baseCurrency = $dataFormChannel->getBaseCurrency();
- if (null === $baseCurrency) {
- continue;
- }
- $baseCurrencyCode = $baseCurrency->getCode();
- if ($this->gatewayBaseCurrencyCode !== $baseCurrencyCode) {
- $message = $this->translator->trans(
- 'payplug_sylius_payplug_plugin.form.base_currency_not_euro',
- [
- '#channel_code#' => $dataFormChannel->getCode(),
- '#payment_method#' => $this->gatewayFactoryTitle,
- ],
- );
- $formChannels->get((string) $key)->addError(new FormError($message));
- $this->requestStack->getSession()->getFlashBag()->add('error', $message);
- }
- }
- })
;
}
- private function canBeCreated(string $factoryName): bool
+ /**
+ * Hook for subtypes to scope the base-currency-per-channel restriction enforced by
+ * PaymentMethodTypeExtension.
+ * Default: always enforced, preserving today's behavior for every gateway that doesn't
+ * override this (Bancontact, American Express, Scalapay, Wero, Oney...).
+ *
+ * @see baseCurrencyViolationMessage() Companion hook customizing the message this guards.
+ *
+ * @param array $gatewayConfig Mapped gateway configuration, as stored on GatewayConfig.
+ *
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function shouldValidateBaseCurrency(array $gatewayConfig): bool
{
- $alreadyExists = $this->gatewayConfigRepository->findOneBy(['factoryName' => $factoryName]);
-
- return !$alreadyExists instanceof GatewayConfigInterface;
+ return true;
}
- private function checkCreationRequirements(
- string $factoryTitle,
- string $factoryName,
- FormInterface $form,
- ): void {
- /** @phpstan-ignore-next-line */
- $paymentMethod = $form->getParent()->getParent()->getData();
-
- if (null !== $paymentMethod->getId()) {
- return;
- }
-
- if ($this->canBeCreated($factoryName)) {
- return;
- }
+ /**
+ * Hook for subtypes to customize the currency-violation message. Default matches today's
+ * generic wording, used by every gateway subtype that doesn't override it (Bancontact,
+ * American Express, Scalapay, Wero, Oney...).
+ *
+ * @see shouldValidateBaseCurrency() Companion hook scoping when this message is used.
+ */
+ public function baseCurrencyViolationMessage(ChannelInterface $channel): string
+ {
+ return $this->translator->trans(
+ 'payplug_sylius_payplug_plugin.form.base_currency_not_euro',
+ [
+ '#channel_code#' => $channel->getCode(),
+ '#payment_method#' => $this->gatewayFactoryTitle,
+ ],
+ );
+ }
- $message = $this->translator->trans('payplug_sylius_payplug_plugin.form.only_one_gateway_allowed', ['%gateway_title%' => $factoryTitle]);
- /* @phpstan-ignore-next-line */
- $form->getParent()->getParent()->get('enabled')->addError(new FormError($message));
+ public function getBaseCurrencyCode(): string
+ {
+ return $this->gatewayBaseCurrencyCode;
}
}
diff --git a/src/Gateway/Form/Type/AmericanExpressGatewayConfigurationType.php b/src/Gateway/Form/Type/AmericanExpressGatewayConfigurationType.php
index 4387942b..7231686e 100644
--- a/src/Gateway/Form/Type/AmericanExpressGatewayConfigurationType.php
+++ b/src/Gateway/Form/Type/AmericanExpressGatewayConfigurationType.php
@@ -19,7 +19,5 @@ final class AmericanExpressGatewayConfigurationType extends AbstractGatewayConfi
{
protected string $gatewayFactoryTitle = AmericanExpressGatewayFactory::FACTORY_TITLE;
- protected string $gatewayFactoryName = AmericanExpressGatewayFactory::FACTORY_NAME;
-
protected string $gatewayBaseCurrencyCode = AmericanExpressGatewayFactory::BASE_CURRENCY_CODE;
}
diff --git a/src/Gateway/Form/Type/ApplePayGatewayConfigurationType.php b/src/Gateway/Form/Type/ApplePayGatewayConfigurationType.php
index 1f3da3ec..2c831a0d 100644
--- a/src/Gateway/Form/Type/ApplePayGatewayConfigurationType.php
+++ b/src/Gateway/Form/Type/ApplePayGatewayConfigurationType.php
@@ -19,7 +19,5 @@ final class ApplePayGatewayConfigurationType extends AbstractGatewayConfiguratio
{
protected string $gatewayFactoryTitle = ApplePayGatewayFactory::FACTORY_TITLE;
- protected string $gatewayFactoryName = ApplePayGatewayFactory::FACTORY_NAME;
-
protected string $gatewayBaseCurrencyCode = ApplePayGatewayFactory::BASE_CURRENCY_CODE;
}
diff --git a/src/Gateway/Form/Type/BancontactGatewayConfigurationType.php b/src/Gateway/Form/Type/BancontactGatewayConfigurationType.php
index 25eac35b..c7bf6803 100644
--- a/src/Gateway/Form/Type/BancontactGatewayConfigurationType.php
+++ b/src/Gateway/Form/Type/BancontactGatewayConfigurationType.php
@@ -19,7 +19,5 @@ final class BancontactGatewayConfigurationType extends AbstractGatewayConfigurat
{
protected string $gatewayFactoryTitle = BancontactGatewayFactory::FACTORY_TITLE;
- protected string $gatewayFactoryName = BancontactGatewayFactory::FACTORY_NAME;
-
protected string $gatewayBaseCurrencyCode = BancontactGatewayFactory::BASE_CURRENCY_CODE;
}
diff --git a/src/Gateway/Form/Type/OneyGatewayConfigurationType.php b/src/Gateway/Form/Type/OneyGatewayConfigurationType.php
index fd548251..b446b709 100644
--- a/src/Gateway/Form/Type/OneyGatewayConfigurationType.php
+++ b/src/Gateway/Form/Type/OneyGatewayConfigurationType.php
@@ -19,7 +19,5 @@ final class OneyGatewayConfigurationType extends AbstractGatewayConfigurationTyp
{
protected string $gatewayFactoryTitle = OneyGatewayFactory::FACTORY_TITLE;
- protected string $gatewayFactoryName = OneyGatewayFactory::FACTORY_NAME;
-
protected string $gatewayBaseCurrencyCode = OneyGatewayFactory::BASE_CURRENCY_CODE;
}
diff --git a/src/Gateway/Form/Type/PayPlugGatewayConfigurationType.php b/src/Gateway/Form/Type/PayPlugGatewayConfigurationType.php
index f9056fdf..309e5478 100644
--- a/src/Gateway/Form/Type/PayPlugGatewayConfigurationType.php
+++ b/src/Gateway/Form/Type/PayPlugGatewayConfigurationType.php
@@ -5,6 +5,7 @@
namespace PayPlug\SyliusPayPlugPlugin\Gateway\Form\Type;
use PayPlug\SyliusPayPlugPlugin\Gateway\PayPlugGatewayFactory;
+use Sylius\Component\Core\Model\ChannelInterface;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
#[AutoconfigureTag(
@@ -19,7 +20,32 @@ final class PayPlugGatewayConfigurationType extends AbstractGatewayConfiguration
{
protected string $gatewayFactoryTitle = PayPlugGatewayFactory::FACTORY_TITLE;
- protected string $gatewayFactoryName = PayPlugGatewayFactory::FACTORY_NAME;
-
protected string $gatewayBaseCurrencyCode = PayPlugGatewayFactory::BASE_CURRENCY_CODE;
+
+ /**
+ * Only `integrated_payment` requires every associated channel to be EUR; the redirected
+ * and `hosted_fields` display modes both work in any currency.
+ *
+ * The mode is read back through `resolveDisplayMode()` rather than off a display-mode key:
+ * `DISPLAY_MODE_FIELD` is an unmapped admin form field and never reaches the persisted config,
+ * which instead carries the two `INTEGRATED_PAYMENT`/`HOSTED_FIELDS` booleans written by
+ * `resolveDisplayModeFlags()`. Going through the canonical reader also inherits its
+ * hosted-fields-wins tie-break when both flags are somehow true.
+ *
+ * @param array $gatewayConfig Mapped gateway configuration, as stored on GatewayConfig.
+ */
+ public function shouldValidateBaseCurrency(array $gatewayConfig): bool
+ {
+ return PayPlugGatewayFactory::DISPLAY_MODE_INTEGRATED_PAYMENT === PayPlugGatewayFactory::resolveDisplayMode($gatewayConfig);
+ }
+
+ /**
+ * shouldValidateBaseCurrency() above only ever lets this fire for `integrated_payment` mode
+ * (redirected/hosted_fields both return false there), so this message can be specific to
+ * that mode rather than the generic per-gateway wording.
+ */
+ public function baseCurrencyViolationMessage(ChannelInterface $channel): string
+ {
+ return $this->translator->trans('payplug_sylius_payplug_plugin.form.integrated_payment_currency_incompatible');
+ }
}
diff --git a/src/Gateway/Form/Type/ScalapayGatewayConfigurationType.php b/src/Gateway/Form/Type/ScalapayGatewayConfigurationType.php
index ec89fb96..d240aa04 100644
--- a/src/Gateway/Form/Type/ScalapayGatewayConfigurationType.php
+++ b/src/Gateway/Form/Type/ScalapayGatewayConfigurationType.php
@@ -19,7 +19,5 @@ final class ScalapayGatewayConfigurationType extends AbstractGatewayConfiguratio
{
protected string $gatewayFactoryTitle = ScalapayGatewayFactory::FACTORY_TITLE;
- protected string $gatewayFactoryName = ScalapayGatewayFactory::FACTORY_NAME;
-
protected string $gatewayBaseCurrencyCode = ScalapayGatewayFactory::BASE_CURRENCY_CODE;
}
diff --git a/src/Gateway/Form/Type/WeroGatewayConfigurationType.php b/src/Gateway/Form/Type/WeroGatewayConfigurationType.php
index 39d1a035..79055931 100644
--- a/src/Gateway/Form/Type/WeroGatewayConfigurationType.php
+++ b/src/Gateway/Form/Type/WeroGatewayConfigurationType.php
@@ -19,7 +19,5 @@ final class WeroGatewayConfigurationType extends AbstractGatewayConfigurationTyp
{
protected string $gatewayFactoryTitle = WeroGatewayFactory::FACTORY_TITLE;
- protected string $gatewayFactoryName = WeroGatewayFactory::FACTORY_NAME;
-
protected string $gatewayBaseCurrencyCode = WeroGatewayFactory::BASE_CURRENCY_CODE;
}
diff --git a/src/Gateway/PayPlugGatewayFactory.php b/src/Gateway/PayPlugGatewayFactory.php
index 99cb0e41..8e0b37eb 100644
--- a/src/Gateway/PayPlugGatewayFactory.php
+++ b/src/Gateway/PayPlugGatewayFactory.php
@@ -4,6 +4,8 @@
namespace PayPlug\SyliusPayPlugPlugin\Gateway;
+use Sylius\Component\Payment\Model\GatewayConfigInterface;
+
final class PayPlugGatewayFactory extends AbstractGatewayFactory
{
public const FACTORY_NAME = 'payplug';
@@ -13,7 +15,112 @@ final class PayPlugGatewayFactory extends AbstractGatewayFactory
// Custom gateway configuration keys
public const ONE_CLICK = 'oneClick';
+ // Session-protocol sentinel meaning "the customer chose to pay with a different/new card
+ // rather than one of their saved aliases" — shared by PayPlugPaymentDataCreator (legacy SDK
+ // flow) and CaptureAliasPaymentRequestHandler (Hosted Fields flow).
+ public const CARD_CHOICE_OTHER = 'other';
+
public const INTEGRATED_PAYMENT = 'integratedPayment';
public const DEFERRED_CAPTURE = 'deferredCapture';
+
+ public const HOSTED_FIELDS = 'hostedFields';
+
+ public const HF_IDENTIFIER = 'hfIdentifier';
+
+ // Unmapped admin form field driving INTEGRATED_PAYMENT/HOSTED_FIELDS below
+ public const DISPLAY_MODE_FIELD = 'hostedFieldsMode';
+
+ public const DISPLAY_MODE_INTEGRATED_PAYMENT = 'integrated_payment';
+
+ public const DISPLAY_MODE_HOSTED_FIELDS = 'hosted_fields';
+
+ /**
+ * Derives the admin form radio's initial selection from persisted config.
+ * Hosted Fields wins if both flags are somehow true, since only it carries the
+ * mandatory Account ID field the merchant would otherwise lose sight of.
+ */
+ public static function resolveDisplayMode(array $config): ?string
+ {
+ if (true === ($config[self::HOSTED_FIELDS] ?? false)) {
+ return self::DISPLAY_MODE_HOSTED_FIELDS;
+ }
+ if (true === ($config[self::INTEGRATED_PAYMENT] ?? false)) {
+ return self::DISPLAY_MODE_INTEGRATED_PAYMENT;
+ }
+
+ return null;
+ }
+
+ /**
+ * Derives the two persisted booleans from the submitted radio value.
+ * Always returns both keys explicitly (rather than only the "true" one) so that switching
+ * away from a previously-selected mode clears the stale flag instead of leaving it behind.
+ *
+ * @return array{integratedPayment: bool, hostedFields: bool}
+ */
+ public static function resolveDisplayModeFlags(?string $displayMode): array
+ {
+ return [
+ self::INTEGRATED_PAYMENT => self::DISPLAY_MODE_INTEGRATED_PAYMENT === $displayMode,
+ self::HOSTED_FIELDS => self::DISPLAY_MODE_HOSTED_FIELDS === $displayMode,
+ ];
+ }
+
+ /**
+ * @param array $rawFormData Display-mode/HF-identifier values as submitted
+ * (assembled from the config form's already-submitted
+ * child forms at POST_SUBMIT, not PRE_SUBMIT's raw
+ * payload).
+ *
+ * @return list Config keys (HF_IDENTIFIER) that are blank while hosted_fields is
+ * selected; empty if hosted_fields isn't selected or the field is filled.
+ */
+ public static function missingHostedFieldsRequirements(array $rawFormData): array
+ {
+ if (self::DISPLAY_MODE_HOSTED_FIELDS !== ($rawFormData[self::DISPLAY_MODE_FIELD] ?? null)) {
+ return [];
+ }
+
+ $missing = [];
+ if (self::isBlank($rawFormData[self::HF_IDENTIFIER] ?? '')) {
+ $missing[] = self::HF_IDENTIFIER;
+ }
+
+ return $missing;
+ }
+
+ /**
+ * True only for a `payplug`-factory gateway config with Hosted Fields selected. Used to
+ * decide, within the single `payplug`-tagged command providers shared by every gateway
+ * (Capture/Notify/StatusPaymentRequestCommandProvider), whether to delegate to the
+ * Hosted-Fields-specific variant instead of the legacy PayPlug SDK flow — other gateways
+ * (Oney, Bancontact, ...) never satisfy this check, so their behavior is unaffected.
+ */
+ public static function isHostedFieldsConfig(?GatewayConfigInterface $gatewayConfig): bool
+ {
+ return self::FACTORY_NAME === $gatewayConfig?->getFactoryName() &&
+ true === ($gatewayConfig->getConfig()[self::HOSTED_FIELDS] ?? false);
+ }
+
+ /**
+ * True for a Hosted Fields config whose merchant opted into deferred capture: its payments are
+ * then created as authorizations only (UPC capture=false), captured or cancelled later from the
+ * admin order screen. The legacy SDK flow reads the same DEFERRED_CAPTURE flag on its own
+ * (PayPlugPaymentDataCreator), so this deliberately excludes it.
+ */
+ public static function isDeferredCaptureHostedFieldsConfig(?GatewayConfigInterface $gatewayConfig): bool
+ {
+ return self::isHostedFieldsConfig($gatewayConfig) &&
+ true === ($gatewayConfig?->getConfig()[self::DEFERRED_CAPTURE] ?? false);
+ }
+
+ private static function isBlank(mixed $value): bool
+ {
+ if (!is_scalar($value)) {
+ return true;
+ }
+
+ return '' === trim((string) $value);
+ }
}
diff --git a/src/Gateway/ScalapayGatewayFactory.php b/src/Gateway/ScalapayGatewayFactory.php
index 5e4b063a..e982ffaa 100644
--- a/src/Gateway/ScalapayGatewayFactory.php
+++ b/src/Gateway/ScalapayGatewayFactory.php
@@ -11,4 +11,8 @@ final class ScalapayGatewayFactory extends AbstractGatewayFactory
public const FACTORY_TITLE = 'Scalapay by PayPlug';
public const PAYMENT_METHOD_SCALAPAY = 'scalapay';
+
+ public const MIN_AMOUNT = 'min_amount';
+
+ public const MAX_AMOUNT = 'max_amount';
}
diff --git a/src/Gateway/Validator/Constraints/HasNoGatewayChannelConflict.php b/src/Gateway/Validator/Constraints/HasNoGatewayChannelConflict.php
new file mode 100644
index 00000000..2577dcf8
--- /dev/null
+++ b/src/Gateway/Validator/Constraints/HasNoGatewayChannelConflict.php
@@ -0,0 +1,42 @@
+enable()`).
+ *
+ * A conflicting config created through the admin API is therefore caught on its next ordinary
+ * update rather than at creation. This is a pre-existing gap — the `canBeCreated()` check that
+ * preceded PRE-3628 had the identical hole — and closing it properly means a class-level
+ * constraint on `PaymentMethodInterface` in the resource's validation config, not another hook
+ * here. Until then the invariant is a strong default, not a guarantee: read it as such wherever
+ * code assumes at most one enabled gateway per factory per channel.
+ *
+ * @Annotation
+ */
+final class HasNoGatewayChannelConflict extends Constraint
+{
+ /** @var string */
+ public $message = 'payplug_sylius_payplug_plugin.form.gateway_channel_conflict';
+
+ public function validatedBy(): string
+ {
+ return HasNoGatewayChannelConflictValidator::class;
+ }
+}
diff --git a/src/Gateway/Validator/Constraints/HasNoGatewayChannelConflictValidator.php b/src/Gateway/Validator/Constraints/HasNoGatewayChannelConflictValidator.php
new file mode 100644
index 00000000..5649fbbd
--- /dev/null
+++ b/src/Gateway/Validator/Constraints/HasNoGatewayChannelConflictValidator.php
@@ -0,0 +1,64 @@
+resolveFactoryName($value);
+
+ if (null === $factoryName) {
+ return;
+ }
+
+ foreach ($this->conflictChecker->findConflicts($value, $factoryName) as $conflict) {
+ $this->context->buildViolation($constraint->message)
+ ->setParameter('%channel%', (string) $conflict['channel']->getCode())
+ ->setParameter('%payment_method%', (string) $conflict['paymentMethod']->getName())
+ ->atPath('channels')
+ ->addViolation();
+ }
+ }
+
+ /**
+ * Null for anything this constraint has no opinion on — a method with no gateway config, or a
+ * disabled one. `findConflicts()` bails on a disabled subject anyway; short-circuiting here
+ * saves the query.
+ */
+ private function resolveFactoryName(PaymentMethodInterface $paymentMethod): ?string
+ {
+ if (!$paymentMethod->isEnabled()) {
+ return null;
+ }
+
+ $gatewayConfig = $paymentMethod->getGatewayConfig();
+
+ if (!$gatewayConfig instanceof GatewayConfigInterface) {
+ return null;
+ }
+
+ return $gatewayConfig->getFactoryName();
+ }
+}
diff --git a/src/Gateway/Validator/Constraints/IsCanSavePaymentMethodValidator.php b/src/Gateway/Validator/Constraints/IsCanSavePaymentMethodValidator.php
index 7cc33f5b..da67d8f7 100644
--- a/src/Gateway/Validator/Constraints/IsCanSavePaymentMethodValidator.php
+++ b/src/Gateway/Validator/Constraints/IsCanSavePaymentMethodValidator.php
@@ -5,7 +5,7 @@
namespace PayPlug\SyliusPayPlugPlugin\Gateway\Validator\Constraints;
use Payplug\Exception\UnauthorizedException;
-use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientFactory;
+use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientFactoryInterface;
use PayPlug\SyliusPayPlugPlugin\Checker\CanSavePayplugPaymentMethodChecker;
use PayPlug\SyliusPayPlugPlugin\Exception\GatewayConfigurationException;
use PayPlug\SyliusPayPlugPlugin\Gateway\OneyGatewayFactory;
@@ -21,9 +21,12 @@
*/
final class IsCanSavePaymentMethodValidator extends ConstraintValidator
{
+ // `payplug` processes card payments directly and `payplug_oney` has its own dedicated
+ // constraint (IsOneyEnabled); neither is an alternative payment method requiring its own
+ // per-account enablement flag from PayPlug.
private const GATEWAYS_SKIP = [PayPlugGatewayFactory::FACTORY_NAME, OneyGatewayFactory::FACTORY_NAME];
- public function __construct(private PayPlugApiClientFactory $apiClientFactory)
+ public function __construct(private PayPlugApiClientFactoryInterface $apiClientFactory)
{
}
diff --git a/src/Gateway/Validator/Constraints/IsOneyEnabledValidator.php b/src/Gateway/Validator/Constraints/IsOneyEnabledValidator.php
index 0d889031..7237b12e 100644
--- a/src/Gateway/Validator/Constraints/IsOneyEnabledValidator.php
+++ b/src/Gateway/Validator/Constraints/IsOneyEnabledValidator.php
@@ -5,7 +5,7 @@
namespace PayPlug\SyliusPayPlugPlugin\Gateway\Validator\Constraints;
use Payplug\Exception\UnauthorizedException;
-use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientFactory;
+use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientFactoryInterface;
use PayPlug\SyliusPayPlugPlugin\Checker\OneyChecker;
use PayPlug\SyliusPayPlugPlugin\Exception\GatewayConfigurationException;
use PayPlug\SyliusPayPlugPlugin\Gateway\OneyGatewayFactory;
@@ -18,7 +18,7 @@
final class IsOneyEnabledValidator extends ConstraintValidator
{
- public function __construct(private PayPlugApiClientFactory $apiClientFactory)
+ public function __construct(private PayPlugApiClientFactoryInterface $apiClientFactory)
{
}
diff --git a/src/Gateway/Validator/Constraints/IsScalapayAmountRangeValid.php b/src/Gateway/Validator/Constraints/IsScalapayAmountRangeValid.php
new file mode 100644
index 00000000..10c0d5ee
--- /dev/null
+++ b/src/Gateway/Validator/Constraints/IsScalapayAmountRangeValid.php
@@ -0,0 +1,22 @@
+resolveApplicableConfiguredAmounts($value, $constraint);
+ if (null === $configuredAmounts) {
+ return;
+ }
+
+ $authorizedRange = $this->resolveAuthorizedRange($value);
+ if (null === $authorizedRange) {
+ return;
+ }
+
+ $this->applyRangeViolations($configuredAmounts, $authorizedRange, $constraint);
+ }
+
+ /**
+ * Resolves the merchant-configured amounts, applying the early guards that don't need a
+ * live API call: the method must be enabled, amounts must be configured, and — when both
+ * sides are explicitly set — locally consistent.
+ *
+ * @return array{0: int|null, 1: int|null}|null
+ */
+ private function resolveApplicableConfiguredAmounts(
+ PaymentMethodInterface $paymentMethod,
+ IsScalapayAmountRangeValid $constraint,
+ ): ?array {
+ $configuredAmounts = false !== $paymentMethod->isEnabled() ? $this->resolveConfiguredAmounts($paymentMethod) : null;
+ if (null === $configuredAmounts) {
+ return null;
+ }
+
+ [$minAmount, $maxAmount] = $configuredAmounts;
+
+ if (\is_int($minAmount) && \is_int($maxAmount) && $minAmount > $maxAmount) {
+ $this->context->buildViolation($constraint->minGreaterThanMaxMessage)->addViolation();
+
+ return null;
+ }
+
+ return $configuredAmounts;
+ }
+
+ /**
+ * @param array{0: int|null, 1: int|null} $configuredAmounts
+ * @param array{min_amount: int, max_amount: int} $authorizedRange
+ */
+ private function applyRangeViolations(
+ array $configuredAmounts,
+ array $authorizedRange,
+ IsScalapayAmountRangeValid $constraint,
+ ): void {
+ [$minAmount, $maxAmount] = $configuredAmounts;
+
+ // A merchant may configure only one side of the range; the other falls back to the
+ // API bound at checkout (see SupportedMethodsProvider), so the min>max check must
+ // compare against that same effective range, not just the explicitly configured side.
+ $effectiveMinAmount = $minAmount ?? $authorizedRange['min_amount'];
+ $effectiveMaxAmount = $maxAmount ?? $authorizedRange['max_amount'];
+
+ if ($effectiveMinAmount > $effectiveMaxAmount) {
+ $this->context->buildViolation($constraint->minGreaterThanMaxMessage)->addViolation();
+
+ return;
+ }
+
+ if (
+ (\is_int($minAmount) && $minAmount < $authorizedRange['min_amount']) ||
+ (\is_int($maxAmount) && $maxAmount > $authorizedRange['max_amount'])
+ ) {
+ $this->context->buildViolation($constraint->outOfRangeMessage)
+ ->setParameter('%min_amount%', self::formatAmount($authorizedRange['min_amount']))
+ ->setParameter('%max_amount%', self::formatAmount($authorizedRange['max_amount']))
+ ->addViolation()
+ ;
+ }
+ }
+
+ /**
+ * The bounds are EUR cents (the form field is hardcoded to EUR), rendered with two decimals so
+ * 500 reads as "5.00" rather than "5".
+ */
+ private static function formatAmount(int $amountInCents): string
+ {
+ return number_format($amountInCents / 100, 2, '.', '');
+ }
+
+ /**
+ * @return array{0: int|null, 1: int|null}|null
+ */
+ private function resolveConfiguredAmounts(PaymentMethodInterface $paymentMethod): ?array
+ {
+ $gatewayConfig = $paymentMethod->getGatewayConfig();
+
+ if (!$gatewayConfig instanceof GatewayConfigInterface || ScalapayGatewayFactory::FACTORY_NAME !== $gatewayConfig->getFactoryName()) {
+ return null;
+ }
+
+ [$minAmount, $maxAmount] = $this->readConfiguredAmounts($gatewayConfig->getConfig());
+
+ return null === $minAmount && null === $maxAmount ? null : [$minAmount, $maxAmount];
+ }
+
+ /**
+ * The admin form only ever writes null or an int, but the gateway config is a plain serialized
+ * array that a direct DB edit, an import script or an admin API write can leave anything in.
+ * PaymentMethodValidator::process() has no surrounding try/catch, so a malformed value
+ * degrades to "not configured" — leaving the API bounds in force at checkout — rather than
+ * throwing an assertion error that would 500 the admin save.
+ *
+ * @param array $config
+ *
+ * @return array{0: int|null, 1: int|null}
+ */
+ private function readConfiguredAmounts(array $config): array
+ {
+ $minAmount = $config[ScalapayGatewayFactory::MIN_AMOUNT] ?? null;
+ $maxAmount = $config[ScalapayGatewayFactory::MAX_AMOUNT] ?? null;
+
+ try {
+ Assert::nullOrInteger($minAmount);
+ Assert::nullOrInteger($maxAmount);
+ } catch (InvalidArgumentException $exception) {
+ $this->logger->warning('Skipping Scalapay amount range validation: the stored range is malformed.', [
+ 'min_amount' => $minAmount,
+ 'max_amount' => $maxAmount,
+ 'exception' => $exception->getMessage(),
+ ]);
+
+ return [null, null];
+ }
+
+ return [$minAmount, $maxAmount];
+ }
+
+ /**
+ * Fails open: when the authorized range can't be established the config saves unvalidated,
+ * matching the plugin's convention of never blocking an admin save on an API hiccup. That is
+ * not free — a one-sided range that inverts against the live API bounds slips through and
+ * silently hides Scalapay at checkout — so the skip is logged rather than swallowed.
+ *
+ * @return array{min_amount: int, max_amount: int}|null
+ */
+ private function resolveAuthorizedRange(PaymentMethodInterface $paymentMethod): ?array
+ {
+ try {
+ $authorizedRange = $this->resolveApiAuthorizedRange($paymentMethod);
+ } catch (GatewayConfigurationException | PayplugException | InvalidArgumentException $exception) {
+ $this->logger->warning('Skipping Scalapay amount range validation: the PayPlug account could not be read.', [
+ 'payment_method' => $paymentMethod->getCode(),
+ 'exception' => $exception->getMessage(),
+ ]);
+
+ return null;
+ }
+
+ if (null === $authorizedRange) {
+ $this->logger->warning('Skipping Scalapay amount range validation: the PayPlug account authorizes no EUR range for Scalapay.', [
+ 'payment_method' => $paymentMethod->getCode(),
+ ]);
+ }
+
+ return $authorizedRange;
+ }
+
+ /**
+ * @return array{min_amount: int, max_amount: int}|null
+ */
+ private function resolveApiAuthorizedRange(PaymentMethodInterface $paymentMethod): ?array
+ {
+ $account = $this->apiClientFactory->createForPaymentMethod($paymentMethod)->getAccount();
+ $currencies = $this->amountRangeResolver->resolve($account, ScalapayGatewayFactory::PAYMENT_METHOD_SCALAPAY);
+
+ return $currencies['EUR'] ?? null;
+ }
+}
diff --git a/src/Gateway/Validator/Constraints/PayplugPermissionValidator.php b/src/Gateway/Validator/Constraints/PayplugPermissionValidator.php
index 328b47e8..eaf8f752 100644
--- a/src/Gateway/Validator/Constraints/PayplugPermissionValidator.php
+++ b/src/Gateway/Validator/Constraints/PayplugPermissionValidator.php
@@ -5,7 +5,7 @@
namespace PayPlug\SyliusPayPlugPlugin\Gateway\Validator\Constraints;
use Payplug\Exception\UnauthorizedException;
-use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientFactory;
+use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientFactoryInterface;
use Sylius\Component\Core\Model\PaymentMethodInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
@@ -13,7 +13,7 @@
final class PayplugPermissionValidator extends ConstraintValidator
{
- public function __construct(private PayPlugApiClientFactory $apiClientFactory)
+ public function __construct(private PayPlugApiClientFactoryInterface $apiClientFactory)
{
}
diff --git a/src/Handler/HostedFieldsWebhookNotificationHandler.php b/src/Handler/HostedFieldsWebhookNotificationHandler.php
new file mode 100644
index 00000000..df02fd72
--- /dev/null
+++ b/src/Handler/HostedFieldsWebhookNotificationHandler.php
@@ -0,0 +1,499 @@
+ $headers
+ *
+ * @throws InvalidNotificationException if the notification fails signature verification or
+ * parsing — the caller is expected to catch this, same
+ * as it already does for the legacy SDK's PayplugException.
+ */
+ public function treat(PaymentInterface $payment, string $rawBody, array $headers): void
+ {
+ // The shared secret the notification is verified against is stored per gateway config, so
+ // it has to be read from the account this payment was created on — another channel's
+ // header would reject a legitimate webhook (or, worse, accept a foreign one).
+ $method = $payment->getMethod();
+ if (!$method instanceof PaymentMethodInterface) {
+ throw new \LogicException('The payment has no payment method, so no webhook secret can be resolved for it.');
+ }
+
+ $expectedHeader = $this->configurationRepository->forPaymentMethod($method)->get(self::CONFIG_KEY_WEBHOOK_AUTHORIZATION_HEADER) ?? '';
+ $operationData = WebhookNotificationHelper::parse($headers, $rawBody, $expectedHeader);
+
+ if (PaymentOutcome::THREE_DS_PENDING === $operationData->outcome) {
+ // Not a final outcome — leave the payment as-is and, crucially, do not touch
+ // isTreated()/markTreated(): a later, final notification for this same operation
+ // must still be free to apply once it arrives. The 0001-is-pending knowledge itself
+ // now lives in payplug/unified-plugin-core's ExecCodeMapper (see its docblock for the
+ // full execcode-catalog reasoning), not duplicated here. Must run before any refund
+ // matching below: a 3DS-pending notification is never a refund confirmation, and
+ // classifying it as one this early would be wrong regardless of whether its
+ // operationId also happens to match a recorded refund.
+ return;
+ }
+
+ // A notification whose operation id matches one RefundPaymentProcessor already recorded
+ // under $details['refunds'] (for both full and partial UHF refunds) confirms a refund
+ // operation, not the payment's own outcome — ExecCodeMapper's "0000" => PAID mapping is
+ // payment-shaped and would otherwise misreport a successful refund as the payment being
+ // paid. UPC has no "refund" concept in its execCode/outcome vocabulary to lean on here
+ // (see ExecCodeMapper), so this classification is made locally, from ids this plugin
+ // itself generated and already knows the meaning of.
+ $refundAmount = self::findMatchingRefundAmount($payment, $operationData->operationId);
+
+ // Same local classification for a deferred-capture payment's captures/cancellations:
+ // AuthorizedPaymentOperationProcessor recorded each one's operation id when it triggered
+ // it, and already applied its effect on the payment state synchronously — the webhook
+ // only confirms it.
+ $authorizationOperation = null === $refundAmount
+ ? AuthorizationDetails::fromDetails($payment->getDetails())->findOperation($operationData->operationId)
+ : null;
+ if (null !== $authorizationOperation) {
+ $this->treatAuthorizationOperation($payment, $rawBody, $operationData, $authorizationOperation);
+
+ return;
+ }
+
+ if (null === $refundAmount) {
+ $this->resolveAuthorizationOutcome($payment, $rawBody, $operationData);
+ }
+
+ $this->applyConfirmation($payment, $rawBody, $operationData, $refundAmount);
+ }
+
+ // Split out of treat() to keep its own return count within SonarCloud's limit (php:S1142).
+ // $refundAmount is non-null when the notification confirms one of the payment's recorded refunds.
+ private function applyConfirmation(
+ PaymentInterface $payment,
+ string $rawBody,
+ OperationData $operationData,
+ ?int $refundAmount,
+ ): void {
+ if (!$this->matchesPayment($payment, $operationData, $refundAmount ?? $payment->getAmount())) {
+ return;
+ }
+
+ if (null === $refundAmount) {
+ $this->applyLocked($payment, $rawBody, $operationData);
+
+ return;
+ }
+
+ if (PaymentOutcome::PAID === $operationData->outcome) {
+ $operationData->outcome = PaymentOutcome::REFUNDED;
+ $this->applyLocked($payment, $rawBody, $operationData);
+
+ return;
+ }
+
+ $this->treatFailedRefund($payment, $rawBody, $operationData);
+ }
+
+ // The refund itself failed (or is still pending) per its own execCode — this must never be
+ // forced into REFUNDED (the money never moved), nor forwarded as-is to the payment's own state
+ // machine: PaymentOutcome::FAILED maps to TRANSITION_FAIL (see SyliusOrderStateMutator), which
+ // means "this PAYMENT failed," not "this refund attempt failed" — the underlying payment
+ // already succeeded, only the refund didn't. Track/log only, so this notification stops being
+ // redelivered without ever touching the Payment's own state.
+ private function treatFailedRefund(PaymentInterface $payment, string $rawBody, OperationData $operationData): void
+ {
+ $this->logger->error('[PayPlug][UPC] Refund confirmation reports a non-success outcome.', [
+ 'sylius_payment_id' => $payment->getId(),
+ 'operation_id' => $operationData->operationId,
+ 'outcome' => $operationData->outcome,
+ 'exec_code' => $operationData->execCode,
+ ]);
+
+ if (!$this->markMatchedRefundAsFailedLocked($payment, $operationData->operationId)) {
+ // Couldn't acquire the lock guarding this payment's $details['refunds'] —
+ // RefundPaymentProcessor is creating a refund for it right now (see
+ // markMatchedRefundAsFailedLocked()'s own docblock). Return without calling
+ // applyLocked(): isTreated()/markTreated() are never touched, so this notification
+ // stays free to be redelivered and retried once that refund creation has released the
+ // lock, instead of being marked treated without its 'failed' flag ever being recorded.
+ return;
+ }
+
+ $this->applyLocked($payment, $rawBody, $operationData, applyOutcome: false);
+ }
+
+ /**
+ * For an authorization-only payment, the confirmation of its own creation reads "authorized",
+ * never "paid" (see AuthorizationDetails::resolveCreationOutcome()) — and, after a 3DS
+ * challenge, is also the first place its capture deadline shows up. Any other operation id
+ * reaching here on such a payment is one Sylius never triggered: mapping it to AUTHORIZED
+ * too keeps it from ever completing the payment on a guess.
+ */
+ private function resolveAuthorizationOutcome(
+ PaymentInterface $payment,
+ string $rawBody,
+ OperationData $operationData,
+ ): void {
+ $authorization = AuthorizationDetails::fromDetails($payment->getDetails());
+ if (!$authorization->isDeferred()) {
+ return;
+ }
+
+ $operationData->outcome = $authorization->resolveCreationOutcome($operationData->outcome);
+ if (PaymentOutcome::AUTHORIZED === $operationData->outcome) {
+ $payment->setDetails(AuthorizationWebhookBody::withMaxCaptureDate($payment->getDetails(), $rawBody));
+ }
+ }
+
+ /**
+ * A success confirms what the payment state already reflects, so it is only tracked as
+ * treated. A failure means an operation accepted synchronously did not actually go through:
+ * its entry is flagged failed (so the remaining capturable amount counts it back in) and, since
+ * the payment may already have moved on to "completed"/"cancelled" on the strength of it,
+ * logged critically for the merchant to reconcile — no automatic transition can undo those.
+ *
+ * @param array{operation: string, amount: int} $authorizationOperation
+ */
+ private function treatAuthorizationOperation(
+ PaymentInterface $payment,
+ string $rawBody,
+ OperationData $operationData,
+ array $authorizationOperation,
+ ): void {
+ if (!$this->matchesPayment($payment, $operationData, $authorizationOperation['amount'])) {
+ return;
+ }
+
+ if (PaymentOutcome::PAID !== $operationData->outcome) {
+ $this->logger->critical('[PayPlug][UPC] An authorization operation accepted earlier is reported as not completed; the payment needs manual reconciliation.', [
+ 'sylius_payment_id' => $payment->getId(),
+ 'payment_state' => $payment->getState(),
+ 'operation' => $authorizationOperation['operation'],
+ 'operation_id' => $operationData->operationId,
+ 'outcome' => $operationData->outcome,
+ 'exec_code' => $operationData->execCode,
+ ]);
+
+ $lockKey = AuthorizationLockKey::forPaymentId($payment->getId());
+ if (!$this->lock->acquire($lockKey, self::LOCK_TTL_SECONDS)) {
+ // An operation on this payment is in progress; leave this notification untreated
+ // so its redelivery flags the entry once the lock is free.
+ return;
+ }
+
+ try {
+ $payment->setDetails(AuthorizationDetails::withOperationFailed($payment->getDetails(), $authorizationOperation['operation'], $operationData->operationId));
+ } finally {
+ $this->lock->release($lockKey);
+ }
+ }
+
+ $this->applyLocked($payment, $rawBody, $operationData, applyOutcome: false);
+ }
+
+ // Split out of treat() to keep its own return count within SonarCloud's limit (php:S1142) —
+ // same rationale as matchesPayment() below: this is its own self-contained "acquire, check
+ // idempotency, apply" unit, not a fragment that needs to share treat()'s return budget.
+ // $applyOutcome false skips the orderStateMutator call while still tracking the notification
+ // as treated — used when the resolved $operationData->outcome must not reach the Payment's
+ // own state machine at all (see treatFailedRefund() above); a refund
+ // confirmation never reaches maybeSaveCard() either way, since that only ever runs alongside
+ // a genuine PAID outcome being applied.
+ private function applyLocked(
+ PaymentInterface $payment,
+ string $rawBody,
+ OperationData $operationData,
+ bool $applyOutcome = true,
+ ): void {
+ $lockKey = self::LOCK_KEY_PREFIX . $operationData->operationId;
+ if (!$this->lock->acquire($lockKey, self::LOCK_TTL_SECONDS)) {
+ // Another delivery/poll for the same operation is already being processed — whichever
+ // holds the lock applies the outcome, nothing more to do here.
+ return;
+ }
+
+ try {
+ if ($this->paymentRepository->isTreated($operationData->operationId)) {
+ return;
+ }
+
+ $this->paymentRepository->save($operationData);
+ if ($applyOutcome) {
+ $this->orderStateMutator->apply(ResourceIdentifier::toString($payment->getId()), $operationData->outcome);
+ }
+ $this->paymentRepository->markTreated($operationData->operationId);
+
+ // AUTHORIZED too: a deferred-capture payment's card is saved once its authorization
+ // is confirmed, exactly like an immediate one's once it is paid. Never for a
+ // notification only tracked ($applyOutcome false) — a capture/cancellation
+ // confirmation also reads PAID, and has no card to save.
+ if ($applyOutcome && \in_array($operationData->outcome, [PaymentOutcome::PAID, PaymentOutcome::AUTHORIZED], true)) {
+ $this->maybeSaveCard($payment, $rawBody);
+ }
+ } finally {
+ $this->lock->release($lockKey);
+ }
+ }
+
+ // A 3DS-challenge capture never gets an alias back synchronously (CaptureHostedPaymentRequestHandler
+ // only sees one on a direct, frictionless success) — this webhook, fired once the challenge is
+ // validated, is the only place a 3DS payment's card ever gets saved. The alias/card metadata
+ // itself is already in $rawBody, as paymentMethod.{storedId, card, details}, so no extra API call
+ // is needed here. The operation resource CaptureHostedPaymentRequestHandler fetches separately
+ // carries the alias as paymentMethod.id; CardDataFromPaymentMethodExtractor reads both.
+ private function maybeSaveCard(PaymentInterface $payment, string $rawBody): void
+ {
+ $details = $payment->getDetails();
+ if (true !== ($details['hosted_fields_save_card'] ?? false)) {
+ return;
+ }
+
+ $method = $payment->getMethod();
+ if (null === $method) {
+ return;
+ }
+
+ $cardData = CardDataFromPaymentMethodExtractor::extractFromDecoded(\json_decode($rawBody, true));
+ $aliasId = $cardData['aliasId'] ?? null;
+ if (null === $aliasId) {
+ $this->logger->error('[PayPlug][UPC] Save-card was requested but the webhook notification carried no alias id.', [
+ 'sylius_payment_id' => $payment->getId(),
+ ]);
+
+ return;
+ }
+
+ $this->cardPersister->persist($aliasId, $payment, $method, $details, $cardData);
+ }
+
+ // Split out of treat() to keep its own return count within SonarCloud's limit (php:S1142) —
+ // both branches here mean "nothing to apply," they just differ in whether that's expected
+ // (still-pending) or a problem worth logging over (mismatch).
+ private function matchesPayment(PaymentInterface $payment, OperationData $operationData, ?int $expectedAmount): bool
+ {
+ if (PaymentOutcome::THREE_DS_PENDING === $operationData->outcome) {
+ // Not a final outcome — leave the payment as-is and, crucially, do not touch
+ // isTreated()/markTreated(): a later, final notification for this same operation
+ // must still be free to apply once it arrives. The 0001-is-pending knowledge itself
+ // now lives in payplug/unified-plugin-core's ExecCodeMapper (see its docblock for the
+ // full execcode-catalog reasoning), not duplicated here.
+ return false;
+ }
+
+ $expectedOrderId = PaymentOrderIdResolver::resolve($payment->getOrder(), $payment->getId());
+ if ($operationData->orderId !== $expectedOrderId || $operationData->amount !== $expectedAmount) {
+ $this->logger->error('[PayPlug][UPC] Hosted Fields webhook notification does not match the payment it was resolved against.', [
+ 'sylius_payment_id' => $payment->getId(),
+ 'expected_order_id' => $expectedOrderId,
+ 'received_order_id' => $operationData->orderId,
+ 'expected_amount' => $expectedAmount,
+ 'received_amount' => $operationData->amount,
+ ]);
+
+ return false;
+ }
+
+ return true;
+ }
+
+ // $details['refunds'] entries are RefundPaymentProcessor's own — see
+ // processHostedFields()/processHostedFieldsWithAmount() — {internal_id, id, amount}, id being
+ // the refund operation's own id (from createRefund()'s response operationIds[0]).
+ private static function findMatchingRefundAmount(PaymentInterface $payment, string $operationId): ?int
+ {
+ $refunds = self::resolveOwnRefunds($payment, $operationId);
+ if (null === $refunds) {
+ return null;
+ }
+
+ $index = self::findMatchingRefundIndex($refunds, $operationId);
+ if (null === $index) {
+ return null;
+ }
+
+ $entry = $refunds[$index];
+ $amount = \is_array($entry) ? ($entry['amount'] ?? null) : null;
+
+ return \is_int($amount) ? $amount : null;
+ }
+
+ /**
+ * Acquires RefundDetailsLockKey before calling markMatchedRefundAsFailed() below, so this
+ * read-modify-write of $details['refunds'] can't interleave with
+ * RefundPaymentProcessor::processHostedFields()/processHostedFieldsWithAmount()'s own — which
+ * acquire the very same key around their (network-call-spanning) read-modify-write of that
+ * same array — and silently lose one of the two writes. Returns false, without calling
+ * markMatchedRefundAsFailed() at all, when the lock is already held (a refund creation for
+ * this payment is in progress right now): the caller must not proceed to mark this
+ * notification treated in that case, so it stays free to be redelivered and retried once the
+ * lock is free.
+ */
+ private function markMatchedRefundAsFailedLocked(PaymentInterface $payment, string $operationId): bool
+ {
+ $lockKey = RefundDetailsLockKey::forPaymentId($payment->getId());
+ if (!$this->lock->acquire($lockKey, self::LOCK_TTL_SECONDS)) {
+ $this->logger->error('[PayPlug][UPC] Could not acquire the refund-details lock to flag a failed refund; a refund creation is likely in progress for this payment.', [
+ 'sylius_payment_id' => $payment->getId(),
+ 'operation_id' => $operationId,
+ ]);
+
+ return false;
+ }
+
+ try {
+ self::markMatchedRefundAsFailed($payment, $operationId);
+ } finally {
+ $this->lock->release($lockKey);
+ }
+
+ return true;
+ }
+
+ /**
+ * Neutralizes the matched refund entry's 'amount' contribution — flags it 'failed' => true —
+ * so a later RefundPaymentProcessor::processHostedFields() full-refund call (which sums every
+ * $details['refunds'] entry to derive the remaining balance still owed) doesn't count money
+ * that was accepted synchronously by createRefund() but never actually moved, per this same
+ * notification's own non-success outcome. The entry itself (id/amount) is kept, not removed,
+ * as an audit trail of the failed attempt. Only ever called while holding RefundDetailsLockKey
+ * — see markMatchedRefundAsFailedLocked() above, its only caller.
+ */
+ private static function markMatchedRefundAsFailed(PaymentInterface $payment, string $operationId): void
+ {
+ $refunds = self::resolveOwnRefunds($payment, $operationId);
+ if (null === $refunds) {
+ return;
+ }
+
+ $index = self::findMatchingRefundIndex($refunds, $operationId);
+ if (null === $index || !\is_array($refunds[$index])) {
+ return;
+ }
+
+ $refunds[$index]['failed'] = true;
+ $details = $payment->getDetails();
+ $details['refunds'] = $refunds;
+ $payment->setDetails($details);
+ }
+
+ /**
+ * @return mixed[]|null $details['refunds'] as an array, or null when $operationId is either
+ * the known payment-creation operation id (never a refund — see the inline comment
+ * below) or $details['refunds'] itself isn't a usable array.
+ */
+ private static function resolveOwnRefunds(PaymentInterface $payment, string $operationId): ?array
+ {
+ $details = $payment->getDetails();
+
+ // The original payment-creation notification always carries the exact operation id
+ // CaptureHostedPaymentRequestHandler recorded under hosted_fields_operation_id at
+ // creation time — never a refund. The unresolved-entry fallback in
+ // findMatchingRefundIndex() must not misclassify a delayed/redelivered copy of THAT
+ // notification as an unrelated refund just because a refund with no captured id also
+ // happens to exist on this payment.
+ $paymentOperationId = $details['hosted_fields_operation_id'] ?? null;
+ if (\is_string($paymentOperationId) && $paymentOperationId === $operationId) {
+ return null;
+ }
+
+ $refunds = $details['refunds'] ?? null;
+
+ return \is_array($refunds) ? $refunds : null;
+ }
+
+ /**
+ * @param mixed[] $refunds
+ *
+ * A refund entry with a null id means RefundPaymentProcessor's own createRefund() call
+ * returned a 2xx response whose body carried no operationIds (logged there as an error at
+ * the time) — this confirmation is the only remaining way to learn which refund it belongs
+ * to, so fall back to the most recent such unresolved entry rather than dropping the
+ * notification entirely (or, worse, letting it fall through unmatched and get misapplied as
+ * a plain payment confirmation). Ambiguous only if more than one refund for the same payment
+ * independently hit that same malformed-response edge case, which the upstream error log
+ * already flags as needing manual attention.
+ */
+ private static function findMatchingRefundIndex(array $refunds, string $operationId): ?int
+ {
+ $unresolvedIndex = null;
+
+ foreach ($refunds as $index => $refund) {
+ if (!\is_int($index) || !\is_array($refund) || !\is_int($refund['amount'] ?? null)) {
+ continue;
+ }
+
+ $refundOperationId = $refund['id'] ?? null;
+ if ($refundOperationId === $operationId) {
+ return $index;
+ }
+
+ if (null === $refundOperationId) {
+ $unresolvedIndex = $index;
+ }
+ }
+
+ return $unresolvedIndex;
+ }
+}
diff --git a/src/Handler/PaymentNotificationHandler.php b/src/Handler/PaymentNotificationHandler.php
index 7e1c8851..c6671c1b 100644
--- a/src/Handler/PaymentNotificationHandler.php
+++ b/src/Handler/PaymentNotificationHandler.php
@@ -5,7 +5,9 @@
namespace PayPlug\SyliusPayPlugPlugin\Handler;
use DateTimeImmutable;
+use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use Doctrine\ORM\EntityManagerInterface;
+use Doctrine\Persistence\ManagerRegistry;
use Payplug\Resource\IVerifiableAPIResource;
use Payplug\Resource\Payment;
use Payplug\Resource\PaymentAuthorization;
@@ -31,6 +33,7 @@ public function __construct(
private EntityManagerInterface $entityManager,
private LockFactory $lockFactory,
private RequestStack $requestStack,
+ private ManagerRegistry $managerRegistry,
) {
}
@@ -137,6 +140,9 @@ private function saveCard(PaymentInterface $payment, IVerifiableAPIResource $pay
return;
}
+ // This check-then-act is not by itself race-proof — two payments notified concurrently
+ // for the same card alias can both pass this guard — so a DB-level unique constraint on
+ // (external_id, is_live) backs it up; see the catch below.
$card = $this->payplugCardRepository->findOneBy([
'externalId' => $paymentResource->__get('card')->id,
'isLive' => $paymentResource->is_live,
@@ -163,7 +169,16 @@ private function saveCard(PaymentInterface $payment, IVerifiableAPIResource $pay
->setIsLive($paymentResource->is_live)
;
- $this->payplugCardRepository->add($card);
+ try {
+ $this->payplugCardRepository->add($card);
+ } catch (UniqueConstraintViolationException) {
+ // The findOneBy() guard above lost a race against a concurrent save for the same
+ // alias — that other call already stored the canonical Card row, so there is nothing
+ // left to do here. Doctrine's UnitOfWork closes the EntityManager on ANY flush
+ // failure, catch included — reset the registry so Doctrine work resolved fresh after
+ // this point doesn't inherit the now-closed instance.
+ $this->managerRegistry->resetManager();
+ }
}
private function isResourceIsAuthorized(IVerifiableAPIResource $paymentResource): bool
diff --git a/src/OrderPay/Provider/CaptureHttpResponseProvider.php b/src/OrderPay/Provider/CaptureHttpResponseProvider.php
index c5e1b972..5d8b6806 100644
--- a/src/OrderPay/Provider/CaptureHttpResponseProvider.php
+++ b/src/OrderPay/Provider/CaptureHttpResponseProvider.php
@@ -43,8 +43,13 @@ class CaptureHttpResponseProvider implements HttpResponseProviderInterface
{
public function supports(RequestConfiguration $requestConfiguration, PaymentRequestInterface $paymentRequest): bool
{
- return $paymentRequest->getAction() === PaymentRequestInterface::ACTION_CAPTURE &&
- ($paymentRequest->getResponseData()['redirect_url'] ?? null) !== null;
+ if ($paymentRequest->getAction() !== PaymentRequestInterface::ACTION_CAPTURE) {
+ return false;
+ }
+
+ $data = $paymentRequest->getResponseData();
+
+ return null !== ($data['redirect_url'] ?? null) || null !== ($data['redirect_html'] ?? null);
}
public function getResponse(
@@ -53,6 +58,14 @@ public function getResponse(
): Response {
// This is called after the capture payment request has been handled
$data = $paymentRequest->getResponseData();
+
+ // The Unified API's "recommended for web" 3DS-pending shape (see PaymentOutput, returned by
+ // both CaptureHostedPaymentRequestHandler and CaptureAliasPaymentRequestHandler): a
+ // self-submitting HTML form to render as-is, rather than a plain redirect target.
+ if (\is_string($data['redirect_html'] ?? null)) {
+ return new Response($data['redirect_html']);
+ }
+
if (!\is_string($data['redirect_url'] ?? null)) {
throw new \LogicException('Redirect URL is not set in the payment request response data.');
}
diff --git a/src/PaymentProcessing/AuthorizedPaymentOperationProcessor.php b/src/PaymentProcessing/AuthorizedPaymentOperationProcessor.php
new file mode 100644
index 00000000..80b119b2
--- /dev/null
+++ b/src/PaymentProcessing/AuthorizedPaymentOperationProcessor.php
@@ -0,0 +1,468 @@
+getMethod();
+
+ return $method instanceof PaymentMethodInterface &&
+ PayPlugGatewayFactory::isHostedFieldsConfig($method->getGatewayConfig()) &&
+ AuthorizationDetails::fromDetails($payment->getDetails())->isDeferred();
+ }
+
+ /**
+ * Whether an amount in $currencyCode can be converted between the decimal notation the
+ * merchant types and Sylius's two-decimal minor unit: only when UPC's AmountHelper converts it
+ * with that same factor of 100. An empty, malformed or 3-decimal code is rejected by the
+ * helper; a zero-decimal one (JPY, XPF...) converts with a factor of 1.
+ */
+ public static function supportsTypedAmounts(?string $currencyCode): bool
+ {
+ if (null === $currencyCode) {
+ return false;
+ }
+
+ try {
+ return 100 === AmountHelper::toCents(1.0, $currencyCode);
+ } catch (InvalidCurrencyException) {
+ return false;
+ }
+ }
+
+ public function canCapture(PaymentInterface $payment): bool
+ {
+ $authorization = AuthorizationDetails::fromDetails($payment->getDetails());
+
+ return $this->isAuthorized($payment) &&
+ !$authorization->isExpired($this->clock->now()) &&
+ $authorization->remainingAmount() > 0;
+ }
+
+ public function canCancel(PaymentInterface $payment): bool
+ {
+ return $this->canCapture($payment) && !AuthorizationDetails::fromDetails($payment->getDetails())->hasCaptures();
+ }
+
+ /**
+ * @param int|null $amount null captures everything still capturable
+ * @param int|null $expectedVersion the AuthorizationDetails::version() the caller's form was
+ * built against; null skips the replay check (programmatic use)
+ *
+ * @throws AuthorizationOperationException
+ */
+ public function capture(PaymentInterface $payment, ?int $amount, ?int $expectedVersion = null): void
+ {
+ $this->runLocked($payment, function () use ($payment, $amount, $expectedVersion): void {
+ $this->assertOperable($payment, $expectedVersion, AuthorizationDetails::OPERATION_CAPTURE);
+ $this->captureRecorded($payment, $amount);
+
+ if (0 === AuthorizationDetails::fromDetails($payment->getDetails())->remainingAmount()) {
+ $this->applyTransition($payment, PaymentTransitions::TRANSITION_COMPLETE);
+ $this->entityManager->flush();
+ }
+ });
+ }
+
+ /**
+ * @param int|null $amount null cancels everything still authorized
+ *
+ * @throws AuthorizationOperationException
+ */
+ public function cancel(PaymentInterface $payment, ?int $amount, ?int $expectedVersion = null): void
+ {
+ $this->runLocked($payment, function () use ($payment, $amount, $expectedVersion): void {
+ $this->assertOperable($payment, $expectedVersion, AuthorizationDetails::OPERATION_CANCELLATION);
+
+ $authorization = AuthorizationDetails::fromDetails($payment->getDetails());
+ $remaining = $authorization->remainingAmount();
+ $amount = $this->resolveAmount($payment, $amount, $remaining);
+
+ try {
+ $output = $this->authorizationOperator->cancel(
+ $this->resolveMethod($payment),
+ $this->resolveUnifiedApiPaymentId($payment),
+ PaymentOrderIdResolver::resolve($payment->getOrder(), $payment->getId()),
+ // Without an amount only on an untouched authorization: an explicit amount is
+ // what UPC reads as a partial cancellation, which the merchant's contract may
+ // not allow even when it equals the whole remainder. Once part of it has been
+ // cancelled, "no amount" would target the original authorization again, so the
+ // exact remainder is sent instead.
+ $amount === $remaining && 0 === $authorization->version() ? null : $amount,
+ $payment->getCurrencyCode(),
+ $authorization->version() + 1,
+ );
+ } catch (PayplugException $exception) {
+ throw $this->refusal($payment, AuthorizationDetails::OPERATION_CANCELLATION, $exception);
+ }
+
+ $this->record($payment, AuthorizationDetails::OPERATION_CANCELLATION, $output->status, $output->body, $amount);
+ $this->entityManager->flush();
+
+ if ($amount === $remaining) {
+ $this->applyTransition($payment, PaymentTransitions::TRANSITION_CANCEL);
+ $this->entityManager->flush();
+ }
+ });
+ }
+
+ /**
+ * Keeps Sylius's own "complete" transition meaningful for a deferred Hosted Fields payment:
+ * applied while money is still only authorized — by the native admin "Complete" button, by
+ * payplug:capture-authorized-payments, or by a merchant's own shipping listener (see
+ * doc/authorized_payment.md) — it first captures whatever is still capturable. Throwing here
+ * aborts the transition, so the payment never reads "completed" without the capture behind it.
+ *
+ * A no-op when this processor applies "complete" itself after the last capture, since nothing
+ * is left capturable by then.
+ */
+ #[AsTransitionListener(workflow: PaymentTransitions::GRAPH, transition: PaymentTransitions::TRANSITION_COMPLETE)]
+ public function onCompleteTransition(TransitionEvent $event): void
+ {
+ $payment = $event->getSubject();
+ if (!$payment instanceof PaymentInterface || !self::supports($payment) || !$this->isAuthorized($payment)) {
+ return;
+ }
+
+ if (0 === AuthorizationDetails::fromDetails($payment->getDetails())->remainingAmount()) {
+ return;
+ }
+
+ $this->runLocked($payment, function () use ($payment): void {
+ $this->assertOperable($payment, null, AuthorizationDetails::OPERATION_CAPTURE);
+ $this->captureRecorded($payment, null);
+ });
+ }
+
+ /**
+ * @throws AuthorizationOperationException
+ */
+ private function captureRecorded(PaymentInterface $payment, ?int $amount): void
+ {
+ $remaining = AuthorizationDetails::fromDetails($payment->getDetails())->remainingAmount();
+ $amount = $this->resolveAmount($payment, $amount, $remaining);
+
+ try {
+ $output = $this->authorizationOperator->capture(
+ $this->resolveMethod($payment),
+ $this->resolveUnifiedApiPaymentId($payment),
+ PaymentOrderIdResolver::resolve($payment->getOrder(), $payment->getId()),
+ // Always explicit: without an amount the Unified API captures the ORIGINAL
+ // authorized amount, not what remains — after a partial capture that is refused as
+ // a duplicate (execCode 4011, staging 2026-09-24).
+ $amount,
+ $payment->getCurrencyCode(),
+ AuthorizationDetails::fromDetails($payment->getDetails())->version() + 1,
+ );
+ } catch (PayplugException $exception) {
+ throw $this->refusal($payment, AuthorizationDetails::OPERATION_CAPTURE, $exception);
+ }
+
+ $this->record($payment, AuthorizationDetails::OPERATION_CAPTURE, $output->status, $output->body, $amount);
+
+ if (null !== $output->maxCaptureDate) {
+ $payment->setDetails([...$payment->getDetails(), AuthorizationDetails::MAX_CAPTURE_DATE => $output->maxCaptureDate]);
+ }
+
+ // Durable before anything else can throw, and before runLocked() lets go of the lock.
+ $this->entityManager->flush();
+ }
+
+ /**
+ * @throws AuthorizationOperationException
+ */
+ private function assertOperable(PaymentInterface $payment, ?int $expectedVersion, string $operation): void
+ {
+ if (!self::supports($payment) || !$this->isAuthorized($payment)) {
+ throw new AuthorizationOperationException(self::ERROR_KEY_PREFIX . 'not_authorized');
+ }
+
+ $authorization = AuthorizationDetails::fromDetails($payment->getDetails());
+
+ if (null !== $expectedVersion && $expectedVersion !== $authorization->version()) {
+ throw new AuthorizationOperationException(self::ERROR_KEY_PREFIX . 'stale');
+ }
+
+ if ($authorization->isExpired($this->clock->now())) {
+ throw new AuthorizationOperationException(self::ERROR_KEY_PREFIX . 'expired');
+ }
+
+ if (AuthorizationDetails::OPERATION_CANCELLATION === $operation && $authorization->hasCaptures()) {
+ throw new AuthorizationOperationException(self::ERROR_KEY_PREFIX . 'cancel_after_capture');
+ }
+
+ if (0 === $authorization->remainingAmount()) {
+ throw new AuthorizationOperationException(self::ERROR_KEY_PREFIX . 'nothing_remaining');
+ }
+ }
+
+ /**
+ * @throws AuthorizationOperationException
+ */
+ private function resolveAmount(PaymentInterface $payment, ?int $amount, int $remaining): int
+ {
+ $currencyCode = $payment->getCurrencyCode();
+ if (null !== $amount && !self::supportsTypedAmounts($currencyCode)) {
+ throw new AuthorizationOperationException(self::ERROR_KEY_PREFIX . 'unsupported_currency', [
+ '%currency%' => $currencyCode ?? '',
+ ]);
+ }
+
+ $amount ??= $remaining;
+
+ if ($amount <= 0) {
+ throw new AuthorizationOperationException(self::ERROR_KEY_PREFIX . 'invalid_amount');
+ }
+
+ if ($amount > $remaining) {
+ throw new AuthorizationOperationException(self::ERROR_KEY_PREFIX . 'amount_exceeds_remaining', [
+ '%remaining%' => self::formatAmount($remaining, (string) $currencyCode),
+ ]);
+ }
+
+ return $amount;
+ }
+
+ /**
+ * The operation id is what lets HostedFieldsWebhookNotificationHandler recognize this
+ * operation's own webhook later on (and UnifiedApiIpnAction resolve the payment for it at
+ * all). Its absence is logged rather than failing the call: the operation did happen, and
+ * refusing to record it would leave the payment out of step with the transaction.
+ */
+ private function record(
+ PaymentInterface $payment,
+ string $operation,
+ int $status,
+ string $responseBody,
+ int $amount,
+ ): void {
+ $operationId = self::extractFirstOperationId($responseBody);
+ if (null === $operationId) {
+ $this->logger->error('[PayPlug][UPC] Authorization operation succeeded but the response carried no operationIds.', [
+ 'sylius_payment_id' => $payment->getId(),
+ 'operation' => $operation,
+ // Status and execCode only: the body is the API's to shape, keep it out of the logs.
+ 'status' => $status,
+ 'exec_code' => self::extractExecCode($responseBody),
+ ]);
+ }
+
+ $payment->setDetails(AuthorizationDetails::withOperation($payment->getDetails(), $operation, $operationId, $amount));
+
+ $this->logger->info('[PayPlug][UPC] Authorization operation recorded.', [
+ 'sylius_payment_id' => $payment->getId(),
+ 'operation' => $operation,
+ 'operation_id' => $operationId,
+ 'amount' => $amount,
+ ]);
+ }
+
+ private function refusal(
+ PaymentInterface $payment,
+ string $operation,
+ PayplugException $exception,
+ ): AuthorizationOperationException {
+ $this->logger->error('[PayPlug][UPC] Authorization operation refused.', [
+ 'sylius_payment_id' => $payment->getId(),
+ 'operation' => $operation,
+ 'exception' => $exception::class,
+ 'error' => $exception->getMessage(),
+ ]);
+
+ $reason = match (true) {
+ $exception instanceof AuthorizationExpiredException => 'expired',
+ $exception instanceof AmountExceedsAvailableException,
+ $exception instanceof CaptureAmountException,
+ $exception instanceof CancellationAmountException => 'amount_refused',
+ $exception instanceof PaymentAlreadyCapturedException => 'already_captured',
+ $exception instanceof PaymentAlreadyCancelledException => 'already_cancelled',
+ // Same message from the API whether the payment was already captured or cancelled.
+ $exception instanceof PaymentNotCapturableException,
+ $exception instanceof PaymentNotVoidableException => 'not_operable',
+ $exception instanceof MultipleCaptureNotAllowedException => 'multiple_capture_not_allowed',
+ $exception instanceof PartialCancellationNotAllowedException => 'partial_cancellation_not_allowed',
+ $exception instanceof OperationConflictException => 'conflict',
+ $exception instanceof CardOperationException => 'refused_by_issuer',
+ $exception instanceof PaymentNotFoundException => 'payment_not_found',
+ default => 'api_error',
+ };
+
+ return new AuthorizationOperationException(self::ERROR_KEY_PREFIX . $reason, [], $exception);
+ }
+
+ /**
+ * @throws AuthorizationOperationException
+ */
+ private function runLocked(PaymentInterface $payment, \Closure $operation): void
+ {
+ $lockKey = AuthorizationLockKey::forPaymentId($payment->getId());
+ if (!$this->lock->acquire($lockKey, self::LOCK_TTL_SECONDS)) {
+ throw new AuthorizationOperationException(self::ERROR_KEY_PREFIX . 'in_progress');
+ }
+
+ try {
+ $operation();
+ } finally {
+ $this->lock->release($lockKey);
+ }
+ }
+
+ private function applyTransition(PaymentInterface $payment, string $transition): void
+ {
+ if (!$this->stateMachine->can($payment, PaymentTransitions::GRAPH, $transition)) {
+ $this->logger->warning('[PayPlug][UPC] Cannot apply payment transition after an authorization operation.', [
+ 'sylius_payment_id' => $payment->getId(),
+ 'current_state' => $payment->getState(),
+ 'transition' => $transition,
+ ]);
+
+ return;
+ }
+
+ $this->stateMachine->apply($payment, PaymentTransitions::GRAPH, $transition);
+ }
+
+ private function isAuthorized(PaymentInterface $payment): bool
+ {
+ return PaymentInterface::STATE_AUTHORIZED === $payment->getState();
+ }
+
+ private function resolveMethod(PaymentInterface $payment): PaymentMethodInterface
+ {
+ $method = $payment->getMethod();
+ if (!$method instanceof PaymentMethodInterface) {
+ throw new \LogicException('Payment method is not set for the payment.');
+ }
+
+ return $method;
+ }
+
+ private function resolveUnifiedApiPaymentId(PaymentInterface $payment): string
+ {
+ $paymentId = $payment->getDetails()['hosted_fields_payment_id'] ?? null;
+ if (!\is_string($paymentId) || '' === $paymentId) {
+ throw new AuthorizationOperationException(self::ERROR_KEY_PREFIX . 'payment_not_found');
+ }
+
+ return $paymentId;
+ }
+
+ private static function extractFirstOperationId(string $body): ?string
+ {
+ $decoded = \json_decode($body, true);
+ $operationIds = \is_array($decoded) ? ($decoded['operationIds'] ?? null) : null;
+ $operationId = \is_array($operationIds) ? ($operationIds[0] ?? null) : null;
+
+ return \is_string($operationId) && '' !== $operationId ? $operationId : null;
+ }
+
+ private static function extractExecCode(string $body): int|string|null
+ {
+ $decoded = \json_decode($body, true);
+ $execCode = \is_array($decoded) ? ($decoded['execCode'] ?? null) : null;
+
+ return \is_int($execCode) || \is_string($execCode) ? $execCode : null;
+ }
+
+ /**
+ * Only reached for an explicit amount, so resolveAmount() has already checked that
+ * $currencyCode is one supportsTypedAmounts() accepts: a 2-decimal currency.
+ */
+ private static function formatAmount(int $amount, string $currencyCode): string
+ {
+ return \trim(\number_format(AmountHelper::fromCents($amount, $currencyCode), 2, '.', ' ') . ' ' . $currencyCode);
+ }
+}
diff --git a/src/PaymentProcessing/CaptureAuthorizedPaymentProcessor.php b/src/PaymentProcessing/CaptureAuthorizedPaymentProcessor.php
index 4b16ee5b..b876a361 100644
--- a/src/PaymentProcessing/CaptureAuthorizedPaymentProcessor.php
+++ b/src/PaymentProcessing/CaptureAuthorizedPaymentProcessor.php
@@ -4,7 +4,7 @@
namespace PayPlug\SyliusPayPlugPlugin\PaymentProcessing;
-use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientFactory;
+use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientFactoryInterface;
use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientInterface;
use PayPlug\SyliusPayPlugPlugin\Gateway\PayPlugGatewayFactory;
use PayPlug\SyliusPayPlugPlugin\Handler\PaymentNotificationHandler;
@@ -18,7 +18,7 @@
final class CaptureAuthorizedPaymentProcessor
{
public function __construct(
- private PayPlugApiClientFactory $apiClientFactory,
+ private PayPlugApiClientFactoryInterface $apiClientFactory,
private PaymentNotificationHandler $paymentNotificationHandler,
) {
}
diff --git a/src/PaymentProcessing/HostedFieldsCaptureData.php b/src/PaymentProcessing/HostedFieldsCaptureData.php
new file mode 100644
index 00000000..201a12b6
--- /dev/null
+++ b/src/PaymentProcessing/HostedFieldsCaptureData.php
@@ -0,0 +1,24 @@
+logger->info('Hosted Fields token received, awaiting UPC payment processing.', [
+ 'payment_id' => $payment->getId(),
+ 'selected_brand' => $captureData->selectedBrand,
+ 'save_card' => $captureData->saveCard,
+ ]);
+
+ $payment->setDetails(\array_merge(
+ $payment->getDetails(),
+ [
+ 'hosted_fields_token' => $captureData->hfToken,
+ 'hosted_fields_selected_brand' => $captureData->selectedBrand,
+ 'hosted_fields_save_card' => $captureData->saveCard,
+ 'hosted_fields_last4' => $captureData->last4,
+ 'hosted_fields_expiration_month' => $captureData->expirationMonth,
+ 'hosted_fields_expiration_year' => $captureData->expirationYear,
+ 'hosted_fields_country' => $captureData->countryCode,
+ 'status' => PaymentInterface::STATE_PROCESSING,
+ ],
+ ));
+ }
+}
diff --git a/src/PaymentProcessing/PaymentTransitionApplier.php b/src/PaymentProcessing/PaymentTransitionApplier.php
index a5af2e8b..1836a80f 100644
--- a/src/PaymentProcessing/PaymentTransitionApplier.php
+++ b/src/PaymentProcessing/PaymentTransitionApplier.php
@@ -24,11 +24,17 @@ public function apply(PaymentInterface $payment): bool
$status = $details['status'] ?? '';
// These are known PayPlug statuses that do not map to a Sylius payment transition.
- if (\in_array($status, [
- PayPlugApiClientInterface::STATUS_CREATED,
- PayPlugApiClientInterface::REFUNDED,
- PayPlugApiClientInterface::INTERNAL_STATUS_ONE_CLICK,
- ], true)) {
+ if (
+ \in_array(
+ $status,
+ [
+ PayPlugApiClientInterface::STATUS_CREATED,
+ PayPlugApiClientInterface::REFUNDED,
+ PayPlugApiClientInterface::INTERNAL_STATUS_ONE_CLICK,
+ ],
+ true,
+ )
+ ) {
return false;
}
@@ -41,23 +47,29 @@ public function apply(PaymentInterface $payment): bool
};
if (null === $transition) {
- $this->logger->warning('[PayPlug] Cannot apply payment transition: unknown status.', [
+ $this->logger->warning(
+ '[PayPlug] Cannot apply payment transition: unknown status.',
+ [
'sylius_payment_id' => $payment->getId(),
'payplug_payment_id' => $details['payment_id'] ?? null,
'status' => $status,
- ]);
+ ],
+ );
return false;
}
if (!$this->stateMachine->can($payment, PaymentTransitions::GRAPH, $transition)) {
- $this->logger->warning('[PayPlug] Cannot apply payment transition (already applied or incompatible with current state).', [
+ $this->logger->warning(
+ '[PayPlug] Cannot apply payment transition (already applied or incompatible with current state).',
+ [
'sylius_payment_id' => $payment->getId(),
'payplug_payment_id' => $details['payment_id'] ?? null,
'current_state' => $payment->getState(),
'transition' => $transition,
'status' => $status,
- ]);
+ ],
+ );
return false;
}
diff --git a/src/PaymentProcessing/RefundPaymentProcessor.php b/src/PaymentProcessing/RefundPaymentProcessor.php
index 4f9bb13b..1085f0b5 100644
--- a/src/PaymentProcessing/RefundPaymentProcessor.php
+++ b/src/PaymentProcessing/RefundPaymentProcessor.php
@@ -16,6 +16,11 @@
use PayPlug\SyliusPayPlugPlugin\Gateway\ScalapayGatewayFactory;
use PayPlug\SyliusPayPlugPlugin\Gateway\WeroGatewayFactory;
use PayPlug\SyliusPayPlugPlugin\Repository\RefundHistoryRepositoryInterface;
+use PayPlug\SyliusPayPlugPlugin\Upc\AuthorizationDetails;
+use PayPlug\SyliusPayPlugPlugin\Upc\PaymentOrderIdResolver;
+use PayPlug\SyliusPayPlugPlugin\Upc\RefundCreatorInterface;
+use PayPlug\SyliusPayPlugPlugin\Upc\RefundDetailsLockKey;
+use PayplugUnifiedCore\Contracts\ILock;
use Psr\Log\LoggerInterface;
use Sylius\Component\Core\Model\PaymentInterface;
use Sylius\Component\Core\Model\PaymentMethodInterface;
@@ -26,6 +31,7 @@
use Sylius\RefundPlugin\Entity\RefundPayment;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use Symfony\Component\HttpFoundation\RequestStack;
+use Symfony\Component\HttpFoundation\Session\FlashBagAwareSessionInterface;
use Symfony\Component\Workflow\Attribute\AsCompletedListener;
use Symfony\Component\Workflow\Event\CompletedEvent;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -34,6 +40,8 @@
#[Autoconfigure(public: true)]
final class RefundPaymentProcessor implements PaymentProcessorInterface
{
+ private const REFUND_LOCK_TTL_SECONDS = 30;
+
private PayPlugApiClientInterface $payPlugApiClient;
public function __construct(
@@ -43,6 +51,8 @@ public function __construct(
private RepositoryInterface $refundPaymentRepository,
private RefundHistoryRepositoryInterface $payplugRefundHistoryRepository,
private PayPlugApiClientFactoryInterface $apiClientFactory,
+ private RefundCreatorInterface $refundCreator,
+ private ILock $lock,
) {
}
@@ -60,6 +70,13 @@ public function onRefundCompleteTransitionEvent(CompletedEvent $event): void
public function process(PaymentInterface $payment): void
{
$this->prepare($payment);
+
+ if (self::isHostedFields($payment)) {
+ $this->processHostedFields($payment);
+
+ return;
+ }
+
$details = $payment->getDetails();
Assert::string($details['payment_id']);
@@ -77,6 +94,13 @@ public function process(PaymentInterface $payment): void
public function processWithAmount(PaymentInterface $payment, int $amount, int $refundId): void
{
$this->prepare($payment);
+
+ if (self::isHostedFields($payment)) {
+ $this->processHostedFieldsWithAmount($payment, $amount, $refundId);
+
+ return;
+ }
+
$details = $payment->getDetails();
Assert::string($details['payment_id']);
@@ -112,6 +136,345 @@ public function processWithAmount(PaymentInterface $payment, int $amount, int $r
}
}
+ /**
+ * UHF counterpart of process() — same full-refund shape, but via UPC's createRefund() rather
+ * than the legacy PayPlugApiClient. No RefundHistory bookkeeping here, matching process()'s
+ * own behavior (only processWithAmount() persists one) — but the refund's own operation id is
+ * still recorded under $details['refunds'] (internal_id null: there's no Sylius
+ * RefundPayment/$refundId in this flow), since HostedFieldsWebhookNotificationHandler resolves
+ * the payment for a refund's async webhook confirmation by matching against ids present
+ * somewhere in Payment::details — without this, that confirmation could never be resolved.
+ *
+ * Guarded by $lock (same ILock contract HostedFieldsWebhookNotificationHandler already uses),
+ * keyed by RefundDetailsLockKey — the same key processHostedFieldsWithAmount() uses for this
+ * same payment, so a full and a partial refund triggered concurrently on it serialize against
+ * each other too, not just two calls of the same kind; it's also the same key
+ * HostedFieldsWebhookNotificationHandler::markMatchedRefundAsFailed() acquires before its own
+ * write to this same $details['refunds'] array, so a refund creation here (which spans the
+ * createRefund() network call) and a webhook concurrently flagging an earlier refund failed
+ * can't interleave their read-modify-write and silently drop one of the two writes. Unlike the
+ * legacy flow, which forwards Sylius's own refund id to PayPlug as a de-facto idempotency key,
+ * UPC's createRefund() has no idempotency-key parameter at all — a concurrent second call for
+ * this same payment (e.g. a double form submission, or a full refund racing a partial one)
+ * would otherwise be free to trigger a second, real refund on the account. There's no
+ * RefundHistory/refundId to check-then-act on here (full refunds don't create one, mirroring
+ * process()'s legacy behavior), so the lock is the only guard available for this path.
+ */
+ private function processHostedFields(PaymentInterface $payment): void
+ {
+ $hostedFieldsPaymentId = $this->resolveHostedFieldsPaymentId($payment);
+ $originalAmount = $payment->getAmount();
+ if (null === $originalAmount) {
+ throw new \LogicException('Payment amount is not set.');
+ }
+
+ /** @var PaymentMethodInterface $method */
+ $method = $payment->getMethod();
+ $lockKey = RefundDetailsLockKey::forPaymentId($payment->getId());
+
+ $this->runLockedRefund($lockKey, ['sylius_payment_id' => $payment->getId()], function () use ($payment, $method, $originalAmount, $hostedFieldsPaymentId): void {
+ // Re-read now that the lock is held, not a snapshot taken before it: the lock is what
+ // actually keeps this read-modify-write from racing
+ // HostedFieldsWebhookNotificationHandler::markMatchedRefundAsFailed()'s own — see this
+ // method's own docblock above.
+ $details = $payment->getDetails();
+ $externalId = $this->createRefundOperation(
+ $method,
+ $hostedFieldsPaymentId,
+ PaymentOrderIdResolver::resolve($payment->getOrder(), $payment->getId()),
+ null,
+ $payment->getCurrencyCode(),
+ ['sylius_payment_id' => $payment->getId()],
+ );
+
+ $refunds = self::normalizeRefunds($details);
+ // Omitting $amount to createRefund() above refunds the payment's full REMAINING
+ // amount (per UnifiedApiPaymentService::createRefund()'s own docblock), not
+ // $originalAmount — those two only coincide when no prior refund exists yet.
+ // Subtracting whatever this plugin already recorded as refunded (computed BEFORE
+ // appending the new entry below) keeps this one accurate, which matchesPayment() then
+ // relies on to ever match this refund's own webhook confirmation.
+ // A deferred-capture payment only ever received what was captured — less than its
+ // own amount once part of the authorization was cancelled.
+ $authorization = AuthorizationDetails::fromDetails($details);
+ $receivedAmount = $authorization->isDeferred() ? $authorization->capturedAmount() : $originalAmount;
+ $refundedAmount = $receivedAmount - self::sumRecordedRefunds($refunds);
+ self::appendRefundEntry($payment, $details, $refunds, null, $externalId, $refundedAmount);
+ });
+ }
+
+ /**
+ * Shared by processHostedFields()/processHostedFieldsWithAmount(): both acquire $lockKey (see
+ * each method's own docblock for why a lock is needed at all here), run $refund inside it, and
+ * convert any \Exception it throws — including one from $refundCreator->createRefund() itself
+ * — into the same logged UpdateHandlingException, releasing the lock either way.
+ *
+ * @param mixed[] $lockFailureContext
+ */
+ private function runLockedRefund(string $lockKey, array $lockFailureContext, \Closure $refund): void
+ {
+ if (!$this->lock->acquire($lockKey, self::REFUND_LOCK_TTL_SECONDS)) {
+ $this->logger->error('[PayPlug][UPC] Refund already in progress for this payment, refusing a concurrent call.', $lockFailureContext);
+
+ throw new UpdateHandlingException();
+ }
+
+ try {
+ $refund();
+ } catch (Exception $exception) {
+ $this->logger->error('[PayPlug][UPC] Refund Payment', ['error' => $exception->getMessage()]);
+
+ throw new UpdateHandlingException();
+ } finally {
+ $this->lock->release($lockKey);
+ }
+ }
+
+ /**
+ * Actionable, not just informational: without an operation id, the eventual async webhook
+ * confirmation for this refund can never be matched back to it (see
+ * HostedFieldsWebhookNotificationHandler::findMatchingRefundAmount()) and either gets dropped
+ * or, worse, misapplied as a plain payment confirmation.
+ *
+ * @param mixed[] $context
+ */
+ private function logIfOperationIdMissing(?string $externalId, string $responseBody, array $context): void
+ {
+ if (null !== $externalId) {
+ return;
+ }
+
+ $this->logger->error('[PayPlug][UPC] Refund succeeded but the response carried no operationIds.', [
+ ...$context,
+ 'response_body' => $responseBody,
+ ]);
+ }
+
+ /**
+ * Shared by processHostedFields()/processHostedFieldsWithAmount(): calls
+ * $refundCreator->createRefund() and extracts the refund's own operation id from the response
+ * (logging via logIfOperationIdMissing() when the response carried none) — the one piece
+ * genuinely identical between the two callers, $amount aside (null here means a full refund;
+ * a given value means a partial one).
+ *
+ * $currency is required rather than defaulted, so that a future caller cannot silently omit it
+ * and fall back to letting the platform infer $amount's minor units from the account — which is
+ * only unambiguous for a single-currency merchant. Pass $payment->getCurrencyCode(); it is
+ * nullable at the Sylius contract level, and null legitimately reaches UPC as "not supplied".
+ *
+ * @param mixed[] $logContext
+ */
+ private function createRefundOperation(
+ PaymentMethodInterface $method,
+ string $hostedFieldsPaymentId,
+ string $orderId,
+ ?int $amount,
+ ?string $currency,
+ array $logContext,
+ ): ?string {
+ $response = $this->refundCreator->createRefund($method, $hostedFieldsPaymentId, $orderId, $amount, $currency);
+
+ $externalId = self::extractFirstOperationId($response['body']);
+ $this->logIfOperationIdMissing($externalId, $response['body'], $logContext);
+
+ return $externalId;
+ }
+
+ /**
+ * @param mixed[] $details
+ *
+ * @return mixed[]
+ */
+ private static function normalizeRefunds(array $details): array
+ {
+ $refunds = $details['refunds'] ?? [];
+
+ return \is_array($refunds) ? $refunds : [];
+ }
+
+ /**
+ * Shared by processHostedFields()/processHostedFieldsWithAmount(): appends one entry to
+ * $refunds (already normalized via normalizeRefunds()) and persists the resulting
+ * $details['refunds'] via setDetails() — the bookkeeping
+ * HostedFieldsWebhookNotificationHandler later matches a refund's async webhook confirmation
+ * against (see its own findMatchingRefundAmount()).
+ *
+ * @param mixed[] $details
+ * @param mixed[] $refunds
+ */
+ private static function appendRefundEntry(
+ PaymentInterface $payment,
+ array $details,
+ array $refunds,
+ ?int $internalId,
+ ?string $externalId,
+ int $amount,
+ ): void {
+ $refunds[] = [
+ 'internal_id' => $internalId,
+ 'id' => $externalId,
+ 'amount' => $amount,
+ ];
+ $details['refunds'] = $refunds;
+ $payment->setDetails($details);
+ }
+
+ /**
+ * @param mixed[] $refunds
+ *
+ * Skips any entry HostedFieldsWebhookNotificationHandler::markMatchedRefundAsFailed() flagged
+ * 'failed' => true — its synchronous createRefund() call was accepted (2xx), but the async
+ * confirmation later reported the refund itself never actually completed, so that money was
+ * never moved and must not count against the remaining balance a subsequent full refund
+ * derives from this sum.
+ */
+ private static function sumRecordedRefunds(array $refunds): int
+ {
+ $total = 0;
+ foreach ($refunds as $refund) {
+ if (!\is_array($refund) || true === ($refund['failed'] ?? false)) {
+ continue;
+ }
+
+ $amount = $refund['amount'] ?? null;
+ $total += \is_int($amount) ? $amount : 0;
+ }
+
+ return $total;
+ }
+
+ /**
+ * UHF counterpart of processWithAmount(). The refund's own operation id — extracted from the
+ * createRefund() response's operationIds[0], same convention resolveHostedFieldsIds() uses
+ * for a payment's operationIds[0] at creation time — takes the place of the legacy SDK
+ * Refund object's ->id in $details['refunds']. RefundHistory::externalId stays null, exactly
+ * like the legacy flow: that field is reserved for the id an async webhook confirmation would
+ * carry, not the id already known synchronously here.
+ *
+ * Unlike the legacy flow (which forwards Sylius's own $refundId to the API as a de-facto
+ * idempotency key), UPC's createRefund() has no idempotency-key parameter at all — so, on top
+ * of a RefundHistory already recorded for this $refundId being checked for up front (before
+ * calling createRefund() again), the whole check-then-act sequence is now also guarded by
+ * $lock, keyed by RefundDetailsLockKey (same key processHostedFields() uses for this same
+ * payment, so a partial and a full refund racing each other also serialize, not just two
+ * partial refunds — and the same key HostedFieldsWebhookNotificationHandler acquires around
+ * its own write to this $details['refunds'] array, see processHostedFields()'s own docblock):
+ * without it, two concurrent calls could both pass the RefundHistory check before either one
+ * persists, and both would go on to call createRefund() — double-refunding money that a plain
+ * "check first" can't prevent, only a lock actually serializing the attempts can.
+ */
+ private function processHostedFieldsWithAmount(PaymentInterface $payment, int $amount, int $refundId): void
+ {
+ $hostedFieldsPaymentId = $this->resolveHostedFieldsPaymentId($payment);
+
+ /** @var PaymentMethodInterface $method */
+ $method = $payment->getMethod();
+ $lockKey = RefundDetailsLockKey::forPaymentId($payment->getId());
+
+ $this->runLockedRefund(
+ $lockKey,
+ ['sylius_payment_id' => $payment->getId(), 'refund_id' => $refundId],
+ function () use ($payment, $method, $amount, $refundId, $hostedFieldsPaymentId): void {
+ /** @var RefundPayment $refundPayment */
+ $refundPayment = $this->refundPaymentRepository->findOneBy(['id' => $refundId]);
+
+ if ($this->payplugRefundHistoryRepository->findOneBy(['refundPayment' => $refundPayment]) instanceof RefundHistory) {
+ $this->logger->info('[PayPlug][UPC] Refund already recorded for this refund id, skipping duplicate call.', ['refund_id' => $refundId]);
+
+ return;
+ }
+
+ // Re-read now that the lock is held — see processHostedFields()'s own comment on
+ // its equivalent re-read.
+ $details = $payment->getDetails();
+ $externalId = $this->createRefundOperation(
+ $method,
+ $hostedFieldsPaymentId,
+ PaymentOrderIdResolver::resolve($payment->getOrder(), $payment->getId()),
+ $amount,
+ $payment->getCurrencyCode(),
+ ['refund_id' => $refundId],
+ );
+
+ self::appendRefundEntry($payment, $details, self::normalizeRefunds($details), $refundId, $externalId, $amount);
+
+ $refundHistory = new RefundHistory();
+ $refundHistory
+ ->setExternalId(null)
+ ->setPayment($payment)
+ ->setRefundPayment($refundPayment)
+ ->setValue($amount)
+ ->setProcessed(true)
+ ;
+ $this->payplugRefundHistoryRepository->add($refundHistory);
+ },
+ );
+ }
+
+ /**
+ * Refunds are keyed by the payment's own id; the refund route refuses the operation id ("The
+ * reference transaction has not been found."), so there is nothing to fall back on. Refuses
+ * before the lock, the API call and any bookkeeping, tells the admin and logs ids only. A
+ * stored id is never changed afterwards, so the value read here is the one used under the lock.
+ */
+ private function resolveHostedFieldsPaymentId(PaymentInterface $payment): string
+ {
+ $details = $payment->getDetails();
+ $paymentId = $details['hosted_fields_payment_id'] ?? null;
+ if (\is_string($paymentId) && '' !== $paymentId) {
+ return $paymentId;
+ }
+
+ $orderNumber = $payment->getOrder()?->getNumber();
+ $this->logger->error('[PayPlug][UPC] Refund refused: the payment has no hosted_fields_payment_id.', [
+ 'sylius_payment_id' => $payment->getId(),
+ 'order_number' => $orderNumber,
+ 'hosted_fields_operation_id' => $details['hosted_fields_operation_id'] ?? null,
+ ]);
+ $this->notifyAdminRefundUnavailable($orderNumber);
+
+ throw new UpdateHandlingException();
+ }
+
+ /**
+ * No main request or no session (CLI, worker) means there is no admin to tell; the log entry
+ * and the exception still record the refusal.
+ */
+ private function notifyAdminRefundUnavailable(?string $orderNumber): void
+ {
+ $request = $this->requestStack->getMainRequest();
+ if (null === $request || !$request->hasSession()) {
+ return;
+ }
+
+ $session = $request->getSession();
+ if (!$session instanceof FlashBagAwareSessionInterface) {
+ return;
+ }
+
+ $session->getFlashBag()->add('error', $this->translator->trans(
+ 'payplug_sylius_payplug_plugin.ui.payment_refund_missing_payment_id',
+ ['%order%' => $orderNumber ?? ''],
+ ));
+ }
+
+ private static function extractFirstOperationId(string $body): ?string
+ {
+ $decoded = \json_decode($body, true);
+ $operationIds = \is_array($decoded) ? ($decoded['operationIds'] ?? null) : null;
+ $operationId = \is_array($operationIds) ? ($operationIds[0] ?? null) : null;
+
+ return \is_string($operationId) && '' !== $operationId ? $operationId : null;
+ }
+
+ private static function isHostedFields(PaymentInterface $payment): bool
+ {
+ /** @var PaymentMethodInterface $paymentMethod */
+ $paymentMethod = $payment->getMethod();
+
+ return PayPlugGatewayFactory::isHostedFieldsConfig($paymentMethod->getGatewayConfig());
+ }
+
private function prepare(PaymentInterface $payment): void
{
/** @var PaymentMethodInterface $paymentMethod */
@@ -133,6 +496,16 @@ private function prepare(PaymentInterface $payment): void
return;
}
+ // UHF has no "payment_id" detail (see resolveHostedFieldsIds() in
+ // CaptureHostedPaymentRequestHandler — it stores hosted_fields_payment_id instead), so the
+ // check below would otherwise misfire the "refunded locally only" flash message on every
+ // UHF refund even though process()/processWithAmount() now genuinely call the Unified API
+ // for it. The legacy $payPlugApiClient this method would otherwise build below is unused
+ // by the UHF path, so skipping it here also avoids an unnecessary token mint.
+ if (self::isHostedFields($payment)) {
+ return;
+ }
+
if (!isset($details['payment_id'])) {
$this->requestStack->getSession()->getFlashBag()->add(
'info',
diff --git a/src/Provider/OneySimulation/CachedSimulationDataProvider.php b/src/Provider/OneySimulation/CachedSimulationDataProvider.php
index c0b5f4ca..95935cf6 100644
--- a/src/Provider/OneySimulation/CachedSimulationDataProvider.php
+++ b/src/Provider/OneySimulation/CachedSimulationDataProvider.php
@@ -25,11 +25,14 @@ public function __construct(
public function getForCart(OrderInterface $cart): array
{
$country = \explode('_', $cart->getLocaleCode() ?? 'fr_FR')[1];
+ // The channel is part of the key because `fees_for` is resolved from the Oney gateway
+ // config serving the *current* channel (PRE-3440), and each channel may run its own.
$cacheKey = \sprintf(
- 'oney_simulation_%s_%s_%s',
+ 'oney_simulation_%s_%s_%s_%s',
$country,
$cart->getTotal(),
$this->oneySupportedPaymentChoiceProvider->getFeesFor(),
+ $cart->getChannel()?->getCode() ?? 'no_channel',
);
return $this->cache->get($cacheKey, function (ItemInterface $item) use ($cart): array {
diff --git a/src/Provider/OneySupportedPaymentChoiceProvider.php b/src/Provider/OneySupportedPaymentChoiceProvider.php
index 9635b7b3..10b62938 100644
--- a/src/Provider/OneySupportedPaymentChoiceProvider.php
+++ b/src/Provider/OneySupportedPaymentChoiceProvider.php
@@ -6,13 +6,18 @@
use PayPlug\SyliusPayPlugPlugin\Gateway\OneyGatewayFactory;
use PayPlug\SyliusPayPlugPlugin\Repository\PaymentMethodRepositoryInterface;
+use Sylius\Component\Channel\Context\ChannelContextInterface;
+use Sylius\Component\Channel\Context\ChannelNotFoundException;
+use Sylius\Component\Core\Model\ChannelInterface;
use Sylius\Component\Core\Model\PaymentMethodInterface;
use Sylius\Component\Payment\Model\GatewayConfigInterface;
class OneySupportedPaymentChoiceProvider
{
- public function __construct(private PaymentMethodRepositoryInterface $paymentMethodRepository)
- {
+ public function __construct(
+ private PaymentMethodRepositoryInterface $paymentMethodRepository,
+ private ChannelContextInterface $channelContext,
+ ) {
}
public function getSupportedPaymentChoices(bool $useOneyPrefix = false): array
@@ -37,9 +42,14 @@ public function getFeesFor(): string
return $this->getPaymentGatewayConfig()['fees_for'] ?? '';
}
+ /**
+ * Scoped to the current channel: `fees_for` drives which instalment choices are offered to the
+ * shopper, and since PRE-3628 each channel may run its own Oney gateway config with its own
+ * value. A factory-name-only lookup would show one channel's instalment plans on another's.
+ */
private function getPaymentGatewayConfig(): array
{
- $paymentMethod = $this->paymentMethodRepository->findOneByGatewayName(OneyGatewayFactory::FACTORY_NAME);
+ $paymentMethod = $this->resolveOneyPaymentMethod();
if (!$paymentMethod instanceof PaymentMethodInterface) {
return [];
@@ -50,4 +60,27 @@ private function getPaymentGatewayConfig(): array
return $gateway->getConfig();
}
+
+ /**
+ * `getFeesFor()` is called outside the try/catch in `getSupportedPaymentChoices()`, so this
+ * absorbs the no-channel case rather than letting it escape — there being no current channel is
+ * an ordinary state (CLI, an admin request), not an error.
+ */
+ private function resolveOneyPaymentMethod(): ?PaymentMethodInterface
+ {
+ try {
+ $channel = $this->channelContext->getChannel();
+ } catch (ChannelNotFoundException) {
+ return null;
+ }
+
+ if (!$channel instanceof ChannelInterface) {
+ return null;
+ }
+
+ return $this->paymentMethodRepository->findOneEnabledByGatewayNameAndChannel(
+ OneyGatewayFactory::FACTORY_NAME,
+ $channel,
+ );
+ }
}
diff --git a/src/Provider/Payment/ApplePayPaymentProvider.php b/src/Provider/Payment/ApplePayPaymentProvider.php
index 96592302..82e875c4 100644
--- a/src/Provider/Payment/ApplePayPaymentProvider.php
+++ b/src/Provider/Payment/ApplePayPaymentProvider.php
@@ -27,6 +27,7 @@
use Sylius\Component\Core\OrderPaymentTransitions;
use Sylius\Component\Core\Payment\Exception\NotProvidedOrderPaymentException;
use Sylius\Component\Core\TokenAssigner\OrderTokenAssignerInterface;
+use Sylius\Component\Customer\Model\CustomerInterface;
use Sylius\Component\Payment\Factory\PaymentFactoryInterface;
use Sylius\Component\Payment\PaymentTransitions;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
@@ -49,16 +50,16 @@ public function __construct(
public function provide(Request $request, OrderInterface $order): PaymentInterface
{
- $paymentMethod = $this->paymentMethodRepository->findOneByGatewayName(ApplePayGatewayFactory::FACTORY_NAME);
+ $paymentMethod = $this->resolveApplePayPaymentMethod($order);
- if (!$paymentMethod instanceof PaymentMethodInterface || !$paymentMethod->isEnabled()) {
+ if (!$paymentMethod instanceof PaymentMethodInterface) {
throw new LogicException('Apple Pay is not enabled');
}
- $payment = $this->initApplePaySyliusPaymentState($order);
+ $payment = $this->initApplePaySyliusPaymentState($order, $paymentMethod);
Assert::notNull($order->getBillingAddress());
- if (($customer = $order->getBillingAddress()->getCustomer()) instanceof \Sylius\Component\Customer\Model\CustomerInterface) {
+ if (($customer = $order->getBillingAddress()->getCustomer()) instanceof CustomerInterface) {
$order->setCustomer($customer);
}
@@ -198,21 +199,45 @@ public function cancel(OrderInterface $order): void
}
/**
+ * Takes the method resolved by the caller rather than resolving it again: provide() has already
+ * rejected the null case, so accepting it as a non-nullable parameter keeps that single gate the
+ * only place the "Apple Pay is not enabled" decision is made.
+ *
* @throws NotProvidedOrderPaymentException
*/
- private function initApplePaySyliusPaymentState(OrderInterface $order): PaymentInterface
- {
+ private function initApplePaySyliusPaymentState(
+ OrderInterface $order,
+ PaymentMethodInterface $paymentMethod,
+ ): PaymentInterface {
Assert::notNull($order->getCurrencyCode());
$payment = $this->getPayment($order);
- $paymentMethod = $this->paymentMethodRepository->findOneByGatewayName(ApplePayGatewayFactory::FACTORY_NAME);
$payment->setMethod($paymentMethod);
$order->addPayment($payment);
return $payment;
}
+ /**
+ * The enabled Apple Pay method serving the order's own channel. Since PRE-3628 several Apple
+ * Pay configs may coexist, one per channel and each connected to a different PayPlug account,
+ * so resolving by factory name alone would attach an arbitrary channel's method to the payment.
+ */
+ private function resolveApplePayPaymentMethod(OrderInterface $order): ?PaymentMethodInterface
+ {
+ $channel = $order->getChannel();
+
+ if (!$channel instanceof ChannelInterface) {
+ return null;
+ }
+
+ return $this->paymentMethodRepository->findOneEnabledByGatewayNameAndChannel(
+ ApplePayGatewayFactory::FACTORY_NAME,
+ $channel,
+ );
+ }
+
private function getPayment(OrderInterface $order): PaymentInterface
{
$lastPayment = $order->getLastPayment();
diff --git a/src/Provider/SupportedMethodsProvider.php b/src/Provider/SupportedMethodsProvider.php
index 352665db..f285683b 100644
--- a/src/Provider/SupportedMethodsProvider.php
+++ b/src/Provider/SupportedMethodsProvider.php
@@ -5,28 +5,50 @@
namespace PayPlug\SyliusPayPlugPlugin\Provider;
use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientFactoryInterface;
+use PayPlug\SyliusPayPlugPlugin\Gateway\PayPlugGatewayFactory;
+use PayPlug\SyliusPayPlugPlugin\Gateway\ScalapayGatewayFactory;
+use PayPlug\SyliusPayPlugPlugin\Resolver\AccountAmountRangeResolver;
+use Psr\Log\LoggerInterface;
use Sylius\Component\Core\Model\PaymentMethodInterface;
use Sylius\Component\Currency\Context\CurrencyContextInterface;
use Sylius\Component\Payment\Model\GatewayConfigInterface;
use Webmozart\Assert\Assert;
+use Webmozart\Assert\InvalidArgumentException;
final class SupportedMethodsProvider
{
public function __construct(
private CurrencyContextInterface $currencyContext,
private PayPlugApiClientFactoryInterface $clientFactory,
+ private AccountAmountRangeResolver $amountRangeResolver,
+ private LoggerInterface $logger,
) {
}
+ /**
+ * $paymentCurrencyCode must be the currency $paymentAmount is denominated in — i.e. the
+ * payment's own, which Sylius copies from the order (OrderPaymentProcessor sets both amount and
+ * currency from $order). Passing it in rather than reading CurrencyContextInterface here is
+ * what keeps the two halves of every comparison below in the same currency: the context holds
+ * the currency being *displayed* right now, which on a multi-currency channel need not be the
+ * one the order was placed in — and comparing an amount against another currency's min/max
+ * silently filters the wrong methods.
+ *
+ * The context remains the fallback for a payment with no currency of its own, which the Sylius
+ * contract permits (PaymentInterface::getCurrencyCode() is nullable) even though the checkout
+ * flow always sets one.
+ */
public function provide(
array $supportedMethods,
string $factoryName,
int $paymentAmount,
+ ?string $paymentCurrencyCode = null,
?string $billingCountryCode = null,
): array {
- $activeCurrencyCode = $this->currencyContext->getCurrencyCode();
- $authorizedCurrencies = null;
- $allowedCountries = null;
+ $activeCurrencyCode = $paymentCurrencyCode ?? $this->currencyContext->getCurrencyCode();
+
+ /** @var array> $accounts */
+ $accounts = [];
foreach ($supportedMethods as $key => $paymentMethod) {
Assert::isInstanceOf($paymentMethod, PaymentMethodInterface::class);
@@ -38,8 +60,11 @@ public function provide(
continue;
}
- $authorizedCurrencies ??= $this->resolveAuthorizedCurrencies($factoryName);
- $allowedCountries ??= $this->resolveAllowedCountries($factoryName);
+ $memoKey = $this->accountMemoKey($gatewayConfig);
+ $account = $accounts[$memoKey] ??= $this->clientFactory->createForPaymentMethod($paymentMethod)->getAccount();
+
+ $authorizedCurrencies = $this->resolveAuthorizedCurrencies($account, $gatewayConfig);
+ $allowedCountries = $this->resolveAllowedCountries($account, $gatewayConfig);
if ($billingCountryCode !== null && $allowedCountries !== [] && !\in_array($billingCountryCode, $allowedCountries, true)) {
unset($supportedMethods[$key]);
@@ -48,70 +73,146 @@ public function provide(
}
if (!\array_key_exists($activeCurrencyCode, $authorizedCurrencies)) {
- unset($supportedMethods[$key]);
-
+ // Unified Hosted Fields is exempt from the currency gate, pending a source of truth
+ // that actually knows a UHF account's currencies. The Retail `/account` payload only
+ // advertises the legacy acquiring setup (`configuration.min_amounts` /
+ // `max_amounts`), which is demonstrably wrong for a UHF account: account 1461487
+ // reports `currencies: ['EUR']` while a USD payment on that same account completed
+ // on staging (schemeTransactionId 6a9ad8eb905d3). Hiding a method that works is the
+ // worse failure, so UHF is kept and left to the API to accept or refuse.
+ //
+ // Every other gateway keeps the gate: Bancontact, Scalapay, Apple Pay, American
+ // Express and Wero are EUR-only, and `payplug` in integrated_payment mode is served
+ // by the very legacy setup this payload does describe correctly.
+ if (!PayPlugGatewayFactory::isHostedFieldsConfig($gatewayConfig)) {
+ unset($supportedMethods[$key]);
+ }
+
+ // An unadvertised currency carries no amount limits either, so a method kept above
+ // skips the bounds check rather than indexing a missing key.
continue;
}
- if (
- $paymentAmount < $authorizedCurrencies[$activeCurrencyCode]['min_amount'] ||
- $paymentAmount > $authorizedCurrencies[$activeCurrencyCode]['max_amount']
- ) {
- unset($supportedMethods[$key]);
+ [$minAmount, $maxAmount] = $this->resolveAmountBounds(
+ $gatewayConfig,
+ $activeCurrencyCode,
+ $authorizedCurrencies[$activeCurrencyCode],
+ );
- continue;
+ if ($paymentAmount < $minAmount || $paymentAmount > $maxAmount) {
+ unset($supportedMethods[$key]);
}
}
return $supportedMethods;
}
- private function resolveAuthorizedCurrencies(string $factoryName): array
- {
- $account = $this->clientFactory->create($factoryName)->getAccount();
+ /**
+ * ScalapayGatewayConfigurationTypeExtension lets the merchant tighten the API-provided bounds
+ * via the min_amount/max_amount config keys. The override is deliberately scoped to Scalapay:
+ * IsScalapayAmountRangeValidValidator — the save-time guardrail that keeps the configured
+ * range inside what PayPlug authorizes — is wired for Scalapay only, so honouring the same
+ * keys on another gateway would grant it a checkout override with no validation behind it.
+ * The values are entered in EUR, so the override also only applies to an EUR checkout; other
+ * currencies keep the raw API bounds.
+ *
+ * @param array{min_amount: int, max_amount: int} $authorizedRange
+ *
+ * @return array{0: int, 1: int}
+ */
+ private function resolveAmountBounds(
+ GatewayConfigInterface $gatewayConfig,
+ string $activeCurrencyCode,
+ array $authorizedRange,
+ ): array {
+ if ('EUR' !== $activeCurrencyCode || ScalapayGatewayFactory::FACTORY_NAME !== $gatewayConfig->getFactoryName()) {
+ return [$authorizedRange['min_amount'], $authorizedRange['max_amount']];
+ }
- $configuration = $account['configuration'] ?? [];
- Assert::isArray($configuration);
- $defaultMin = $configuration['min_amounts'] ?? [];
- Assert::isArray($defaultMin);
- $defaultMax = $configuration['max_amounts'] ?? [];
- Assert::isArray($defaultMax);
+ [$minAmount, $maxAmount] = $this->readConfiguredAmounts($gatewayConfig->getConfig());
- $underscorePos = strpos($factoryName, '_');
- if ($underscorePos !== false) {
- $pmKey = substr($factoryName, $underscorePos + 1);
- $paymentMethods = $account['payment_methods'] ?? [];
- Assert::isArray($paymentMethods);
- $pmData = $paymentMethods[$pmKey] ?? [];
- Assert::isArray($pmData);
- $minAmounts = isset($pmData['min_amounts']) && \is_array($pmData['min_amounts']) ? $pmData['min_amounts'] : $defaultMin;
- $maxAmounts = isset($pmData['max_amounts']) && \is_array($pmData['max_amounts']) ? $pmData['max_amounts'] : $defaultMax;
- } else {
- $minAmounts = $defaultMin;
- $maxAmounts = $defaultMax;
+ return [
+ $minAmount ?? $authorizedRange['min_amount'],
+ $maxAmount ?? $authorizedRange['max_amount'],
+ ];
+ }
+
+ /**
+ * The admin form only ever writes null or an int, but the gateway config is a plain serialized
+ * array that a direct DB edit, an import script or an admin API write can leave anything in.
+ * provide() runs unguarded on every checkout page (via the gateway resolver decorators), so a
+ * malformed value degrades to "not configured" — falling back to the API bounds — rather than
+ * throwing an assertion error that would break payment-method resolution for the whole
+ * checkout, not just hide Scalapay.
+ *
+ * @param array $config
+ *
+ * @return array{0: int|null, 1: int|null}
+ */
+ private function readConfiguredAmounts(array $config): array
+ {
+ $minAmount = $config[ScalapayGatewayFactory::MIN_AMOUNT] ?? null;
+ $maxAmount = $config[ScalapayGatewayFactory::MAX_AMOUNT] ?? null;
+
+ try {
+ Assert::nullOrInteger($minAmount);
+ Assert::nullOrInteger($maxAmount);
+ } catch (InvalidArgumentException $exception) {
+ $this->logger->warning('Ignoring malformed Scalapay amount range in gateway config; falling back to the PayPlug API bounds.', [
+ 'min_amount' => $minAmount,
+ 'max_amount' => $maxAmount,
+ 'exception' => $exception->getMessage(),
+ ]);
+
+ return [null, null];
}
- $currencies = [];
- foreach ($minAmounts as $currency => $min) {
- Assert::string($currency);
- Assert::integer($min);
- if (isset($maxAmounts[$currency]) && \is_int($maxAmounts[$currency])) {
- $currencies[$currency] = ['min_amount' => $min, 'max_amount' => $maxAmounts[$currency]];
- }
+ return [$minAmount, $maxAmount];
+ }
+
+ /**
+ * Two payment methods of the same factory can be configured on different PayPlug accounts, so
+ * the `/account` payload is memoized per gateway config rather than once per call — sharing one
+ * lookup across the loop let the first method's account govern every later one. The persisted
+ * id is the key; object identity covers a config that has not been flushed yet, whose null id
+ * would otherwise collide with every other unsaved one.
+ */
+ private function accountMemoKey(GatewayConfigInterface $gatewayConfig): string
+ {
+ $id = $gatewayConfig->getId();
+
+ if (\is_int($id) || (\is_string($id) && '' !== $id)) {
+ return 'config:' . $id;
}
- return $currencies;
+ return 'object:' . spl_object_id($gatewayConfig);
}
- private function resolveAllowedCountries(string $factoryName): array
+ /**
+ * Both resolvers below read the factory name off the gateway config the $account was fetched
+ * for, rather than off provide()'s $factoryName argument. The loop guard above makes the two
+ * equal today, but keeping the account payload and the key used to index it sourced from the
+ * same config is what stops the pair drifting apart if that guard is ever relaxed.
+ *
+ * @param array $account
+ *
+ * @return array
+ */
+ private function resolveAuthorizedCurrencies(array $account, GatewayConfigInterface $gatewayConfig): array
{
- $underscorePos = strpos($factoryName, '_');
- if ($underscorePos === false) {
+ return $this->amountRangeResolver->resolve($account, $this->paymentMethodKey($gatewayConfig));
+ }
+
+ /**
+ * @param array $account
+ */
+ private function resolveAllowedCountries(array $account, GatewayConfigInterface $gatewayConfig): array
+ {
+ $pmKey = $this->paymentMethodKey($gatewayConfig);
+ if (null === $pmKey) {
return [];
}
- $account = $this->clientFactory->create($factoryName)->getAccount();
- $pmKey = substr($factoryName, $underscorePos + 1);
$paymentMethods = $account['payment_methods'] ?? [];
Assert::isArray($paymentMethods);
$pmData = $paymentMethods[$pmKey] ?? [];
@@ -126,4 +227,20 @@ private function resolveAllowedCountries(string $factoryName): array
return $allowedCountries;
}
+
+ /**
+ * The `/account` payload keys each PPRO method under the factory name's suffix — `payplug_oney`
+ * is advertised as `oney`. A suffix-less factory name (`payplug`) is the card gateway, which
+ * has no such sub-payload.
+ */
+ private function paymentMethodKey(GatewayConfigInterface $gatewayConfig): ?string
+ {
+ // provide()'s loop guard has already matched this config against a non-null factory name,
+ // so the null coalesce is unreachable from there; it keeps the helper total for any later
+ // caller, and an empty name carries no suffix anyway.
+ $factoryName = $gatewayConfig->getFactoryName() ?? '';
+ $underscorePos = strpos($factoryName, '_');
+
+ return false !== $underscorePos ? substr($factoryName, $underscorePos + 1) : null;
+ }
}
diff --git a/src/Repository/PaymentMethodRepository.php b/src/Repository/PaymentMethodRepository.php
index d2e12daa..70d10524 100644
--- a/src/Repository/PaymentMethodRepository.php
+++ b/src/Repository/PaymentMethodRepository.php
@@ -4,20 +4,79 @@
namespace PayPlug\SyliusPayPlugPlugin\Repository;
+use Doctrine\ORM\QueryBuilder;
use Sylius\Bundle\CoreBundle\Doctrine\ORM\PaymentMethodRepository as BasePaymentMethodRepository;
+use Sylius\Component\Core\Model\ChannelInterface;
use Sylius\Component\Core\Model\PaymentMethodInterface;
+use Webmozart\Assert\Assert;
final class PaymentMethodRepository extends BasePaymentMethodRepository implements PaymentMethodRepositoryInterface
{
public function findOneByGatewayName(string $gatewayFactoryName): ?PaymentMethodInterface
+ {
+ $paymentMethod = $this->createGatewayFactoryQueryBuilder($gatewayFactoryName)
+ ->getQuery()
+ ->setMaxResults(1)
+ ->getOneOrNullResult()
+ ;
+
+ Assert::nullOrIsInstanceOf($paymentMethod, PaymentMethodInterface::class);
+
+ return $paymentMethod;
+ }
+
+ public function findOneEnabledByGatewayNameAndChannel(
+ string $gatewayFactoryName,
+ ChannelInterface $channel,
+ ): ?PaymentMethodInterface {
+ $paymentMethod = $this->createGatewayFactoryQueryBuilder($gatewayFactoryName)
+ ->innerJoin('o.channels', 'channel')
+ ->andWhere('channel = :channel')
+ ->andWhere('o.enabled = true')
+ ->setParameter('channel', $channel)
+ // PRE-3628 admits at most one enabled config per channel per factory, so this orders a
+ // set of one. It only matters if some non-form write path ever breaks that invariant,
+ // where picking deterministically beats picking arbitrarily.
+ ->addOrderBy('o.position', 'ASC')
+ ->addOrderBy('o.id', 'ASC')
+ ->getQuery()
+ ->setMaxResults(1)
+ ->getOneOrNullResult()
+ ;
+
+ Assert::nullOrIsInstanceOf($paymentMethod, PaymentMethodInterface::class);
+
+ return $paymentMethod;
+ }
+
+ public function findEnabledByGatewayName(string $gatewayFactoryName): array
+ {
+ $paymentMethods = $this->createGatewayFactoryQueryBuilder($gatewayFactoryName)
+ ->leftJoin('o.channels', 'channel')
+ ->addSelect('channel')
+ ->andWhere('o.enabled = true')
+ ->getQuery()
+ ->getResult()
+ ;
+
+ Assert::isList($paymentMethods);
+ Assert::allIsInstanceOf($paymentMethods, PaymentMethodInterface::class);
+
+ return $paymentMethods;
+ }
+
+ /**
+ * The shared head of every lookup here: payment methods whose gateway config names $factoryName.
+ *
+ * Each caller narrows it further — by channel, by enabled, or not at all — so the join, the
+ * predicate and the bound parameter live in one place rather than being repeated three times.
+ */
+ private function createGatewayFactoryQueryBuilder(string $gatewayFactoryName): QueryBuilder
{
return $this->createQueryBuilder('o')
->innerJoin('o.gatewayConfig', 'gatewayConfig')
- ->where('gatewayConfig.factoryName = :gatewayFactoryName')
+ ->andWhere('gatewayConfig.factoryName = :gatewayFactoryName')
->setParameter('gatewayFactoryName', $gatewayFactoryName)
- ->getQuery()
- ->setMaxResults(1)
- ->getSingleResult()
;
}
}
diff --git a/src/Repository/PaymentMethodRepositoryInterface.php b/src/Repository/PaymentMethodRepositoryInterface.php
index 54f8c2e9..259a8908 100644
--- a/src/Repository/PaymentMethodRepositoryInterface.php
+++ b/src/Repository/PaymentMethodRepositoryInterface.php
@@ -4,10 +4,34 @@
namespace PayPlug\SyliusPayPlugPlugin\Repository;
+use Sylius\Component\Core\Model\ChannelInterface;
use Sylius\Component\Core\Model\PaymentMethodInterface;
use Sylius\Component\Core\Repository\PaymentMethodRepositoryInterface as BasePaymentMethodRepositoryInterface;
interface PaymentMethodRepositoryInterface extends BasePaymentMethodRepositoryInterface
{
+ /**
+ * @deprecated since PRE-3440. Several gateway configs may share a factory name — one per
+ * channel — so this returns an arbitrary one of them. Use
+ * {@see self::findOneEnabledByGatewayNameAndChannel()} wherever a channel is in
+ * scope.
+ */
public function findOneByGatewayName(string $gatewayFactoryName): ?PaymentMethodInterface;
+
+ /**
+ * The enabled payment method of that factory serving $channel, or null if the channel has none.
+ *
+ * This is the channel-safe replacement for {@see self::findOneByGatewayName()}: since PRE-3628
+ * a merchant may run several configs of one factory, each connected to a different PayPlug
+ * account, and a name-only lookup picks between them arbitrarily.
+ */
+ public function findOneEnabledByGatewayNameAndChannel(
+ string $gatewayFactoryName,
+ ChannelInterface $channel,
+ ): ?PaymentMethodInterface;
+
+ /**
+ * @return list
+ */
+ public function findEnabledByGatewayName(string $gatewayFactoryName): array;
}
diff --git a/src/Resolver/AccountAmountRangeResolver.php b/src/Resolver/AccountAmountRangeResolver.php
new file mode 100644
index 00000000..91271cab
--- /dev/null
+++ b/src/Resolver/AccountAmountRangeResolver.php
@@ -0,0 +1,53 @@
+ $account
+ *
+ * @return array
+ */
+ public function resolve(array $account, ?string $paymentMethodKey): array
+ {
+ $configuration = $account['configuration'] ?? [];
+ Assert::isArray($configuration);
+ $defaultMinAmounts = $configuration['min_amounts'] ?? [];
+ Assert::isArray($defaultMinAmounts);
+ $defaultMaxAmounts = $configuration['max_amounts'] ?? [];
+ Assert::isArray($defaultMaxAmounts);
+
+ if (null !== $paymentMethodKey) {
+ $paymentMethods = $account['payment_methods'] ?? [];
+ Assert::isArray($paymentMethods);
+ $pmData = $paymentMethods[$paymentMethodKey] ?? [];
+ Assert::isArray($pmData);
+ $minAmounts = isset($pmData['min_amounts']) && \is_array($pmData['min_amounts']) ? $pmData['min_amounts'] : $defaultMinAmounts;
+ $maxAmounts = isset($pmData['max_amounts']) && \is_array($pmData['max_amounts']) ? $pmData['max_amounts'] : $defaultMaxAmounts;
+ } else {
+ $minAmounts = $defaultMinAmounts;
+ $maxAmounts = $defaultMaxAmounts;
+ }
+
+ $currencies = [];
+ foreach ($minAmounts as $currency => $min) {
+ Assert::string($currency);
+ Assert::integer($min);
+ if (isset($maxAmounts[$currency]) && \is_int($maxAmounts[$currency])) {
+ $currencies[$currency] = ['min_amount' => $min, 'max_amount' => $maxAmounts[$currency]];
+ }
+ }
+
+ return $currencies;
+ }
+}
diff --git a/src/Resolver/AmericanExpressPaymentMethodsResolverDecorator.php b/src/Resolver/AmericanExpressPaymentMethodsResolverDecorator.php
index d525bf61..8f421ca8 100644
--- a/src/Resolver/AmericanExpressPaymentMethodsResolverDecorator.php
+++ b/src/Resolver/AmericanExpressPaymentMethodsResolverDecorator.php
@@ -37,7 +37,8 @@ public function getSupportedMethods(BasePaymentInterface $subject): array
$supportedMethods,
AmericanExpressGatewayFactory::FACTORY_NAME,
$subject->getAmount() ?? 0,
- $billingCountryCode,
+ paymentCurrencyCode: $subject->getCurrencyCode(),
+ billingCountryCode: $billingCountryCode,
);
}
diff --git a/src/Resolver/ApplePayPaymentMethodsResolverDecorator.php b/src/Resolver/ApplePayPaymentMethodsResolverDecorator.php
index 5205eaf7..99cb874c 100644
--- a/src/Resolver/ApplePayPaymentMethodsResolverDecorator.php
+++ b/src/Resolver/ApplePayPaymentMethodsResolverDecorator.php
@@ -37,7 +37,8 @@ public function getSupportedMethods(BasePaymentInterface $subject): array
$supportedMethods,
ApplePayGatewayFactory::FACTORY_NAME,
$subject->getAmount() ?? 0,
- $billingCountryCode,
+ paymentCurrencyCode: $subject->getCurrencyCode(),
+ billingCountryCode: $billingCountryCode,
);
}
diff --git a/src/Resolver/BancontactPaymentMethodsResolverDecorator.php b/src/Resolver/BancontactPaymentMethodsResolverDecorator.php
index c003b444..56ad0753 100644
--- a/src/Resolver/BancontactPaymentMethodsResolverDecorator.php
+++ b/src/Resolver/BancontactPaymentMethodsResolverDecorator.php
@@ -37,7 +37,8 @@ public function getSupportedMethods(BasePaymentInterface $subject): array
$supportedMethods,
BancontactGatewayFactory::FACTORY_NAME,
$subject->getAmount() ?? 0,
- $billingCountryCode,
+ paymentCurrencyCode: $subject->getCurrencyCode(),
+ billingCountryCode: $billingCountryCode,
);
}
diff --git a/src/Resolver/OneyPaymentMethodsResolverDecorator.php b/src/Resolver/OneyPaymentMethodsResolverDecorator.php
index 73802220..bb8bde70 100644
--- a/src/Resolver/OneyPaymentMethodsResolverDecorator.php
+++ b/src/Resolver/OneyPaymentMethodsResolverDecorator.php
@@ -36,7 +36,11 @@ public function getSupportedMethods(BasePaymentInterface $subject): array
/** @var OrderInterface $order */
$order = $subject->getOrder();
- $activeCurrencyCode = $this->currencyContext->getCurrencyCode();
+ // The payment's own currency, not the one currently displayed: isPriceEligible() below
+ // compares $subject->getAmount() against Oney's per-currency bounds, and on a
+ // multi-currency channel the display currency need not be the order's. Falls back to the
+ // context for a payment carrying no currency, which the Sylius contract allows.
+ $activeCurrencyCode = $subject->getCurrencyCode() ?? $this->currencyContext->getCurrencyCode();
foreach ($supportedMethods as $key => $paymentMethod) {
Assert::isInstanceOf($paymentMethod, PaymentMethodInterface::class);
diff --git a/src/Resolver/PayPlugPaymentMethodsResolverDecorator.php b/src/Resolver/PayPlugPaymentMethodsResolverDecorator.php
index 39412fa1..67d652da 100644
--- a/src/Resolver/PayPlugPaymentMethodsResolverDecorator.php
+++ b/src/Resolver/PayPlugPaymentMethodsResolverDecorator.php
@@ -37,7 +37,8 @@ public function getSupportedMethods(BasePaymentInterface $subject): array
$supportedMethods,
PayPlugGatewayFactory::FACTORY_NAME,
$subject->getAmount() ?? 0,
- $billingCountryCode,
+ paymentCurrencyCode: $subject->getCurrencyCode(),
+ billingCountryCode: $billingCountryCode,
);
}
diff --git a/src/Resolver/PaymentStateResolver.php b/src/Resolver/PaymentStateResolver.php
index 1c8de161..9d749928 100644
--- a/src/Resolver/PaymentStateResolver.php
+++ b/src/Resolver/PaymentStateResolver.php
@@ -7,7 +7,7 @@
use Doctrine\ORM\EntityManagerInterface;
use Payplug\Resource\Payment;
use Payplug\Resource\PaymentAuthorization;
-use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientFactory;
+use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientFactoryInterface;
use PayPlug\SyliusPayPlugPlugin\Gateway\PayPlugGatewayFactory;
use Sylius\Abstraction\StateMachine\StateMachineInterface;
use Sylius\Component\Core\Model\PaymentInterface;
@@ -19,7 +19,7 @@ final class PaymentStateResolver implements PaymentStateResolverInterface
{
public function __construct(
private StateMachineInterface $stateMachine,
- private PayPlugApiClientFactory $payPlugApiClientFactory,
+ private PayPlugApiClientFactoryInterface $payPlugApiClientFactory,
private EntityManagerInterface $paymentEntityManager,
) {
}
diff --git a/src/Resolver/ScalapayPaymentMethodsResolverDecorator.php b/src/Resolver/ScalapayPaymentMethodsResolverDecorator.php
index 05664c39..a3856c30 100644
--- a/src/Resolver/ScalapayPaymentMethodsResolverDecorator.php
+++ b/src/Resolver/ScalapayPaymentMethodsResolverDecorator.php
@@ -37,7 +37,8 @@ public function getSupportedMethods(BasePaymentInterface $subject): array
$supportedMethods,
ScalapayGatewayFactory::FACTORY_NAME,
$subject->getAmount() ?? 0,
- $billingCountryCode,
+ paymentCurrencyCode: $subject->getCurrencyCode(),
+ billingCountryCode: $billingCountryCode,
);
}
diff --git a/src/Resolver/SelectedCardResolver.php b/src/Resolver/SelectedCardResolver.php
new file mode 100644
index 00000000..0c43d88b
--- /dev/null
+++ b/src/Resolver/SelectedCardResolver.php
@@ -0,0 +1,36 @@
+requestStack->getSession()->get('payplug_payment_method');
+ if (null === $cardId || PayPlugGatewayFactory::CARD_CHOICE_OTHER === $cardId) {
+ return null;
+ }
+
+ $card = $this->payplugCardRepository->find($cardId);
+
+ return $card instanceof Card ? $card : null;
+ }
+}
diff --git a/src/Resolver/WeroPaymentMethodsResolverDecorator.php b/src/Resolver/WeroPaymentMethodsResolverDecorator.php
index 0c8b5601..8c342934 100644
--- a/src/Resolver/WeroPaymentMethodsResolverDecorator.php
+++ b/src/Resolver/WeroPaymentMethodsResolverDecorator.php
@@ -37,7 +37,8 @@ public function getSupportedMethods(BasePaymentInterface $subject): array
$supportedMethods,
WeroGatewayFactory::FACTORY_NAME,
$subject->getAmount() ?? 0,
- $billingCountryCode,
+ paymentCurrencyCode: $subject->getCurrencyCode(),
+ billingCountryCode: $billingCountryCode,
);
}
diff --git a/src/Twig/AuthorizationExtension.php b/src/Twig/AuthorizationExtension.php
new file mode 100644
index 00000000..ff67c6bf
--- /dev/null
+++ b/src/Twig/AuthorizationExtension.php
@@ -0,0 +1,88 @@
+describe(...)),
+ ];
+ }
+
+ /**
+ * @return array{
+ * authorized_amount: int,
+ * captured_amount: int,
+ * cancelled_amount: int,
+ * remaining_amount: int,
+ * remaining_amount_input: string,
+ * max_capture_date: \DateTimeImmutable|null,
+ * expired: bool,
+ * expiring_soon: bool,
+ * can_capture: bool,
+ * can_cancel: bool,
+ * version: int,
+ * }|null null for any payment that isn't a deferred-capture Hosted Fields one
+ */
+ public function describe(PaymentInterface $payment): ?array
+ {
+ if (!AuthorizedPaymentOperationProcessor::supports($payment)) {
+ return null;
+ }
+
+ $authorization = AuthorizationDetails::fromDetails($payment->getDetails());
+ $now = $this->clock->now();
+
+ return [
+ 'authorized_amount' => $authorization->authorizedAmount(),
+ 'captured_amount' => $authorization->capturedAmount(),
+ 'cancelled_amount' => $authorization->cancelledAmount(),
+ 'remaining_amount' => $authorization->remainingAmount(),
+ 'remaining_amount_input' => self::remainingAmountInput($authorization->remainingAmount(), $payment->getCurrencyCode()),
+ 'max_capture_date' => $authorization->maxCaptureDate(),
+ 'expired' => $authorization->isExpired($now),
+ 'expiring_soon' => $authorization->isExpiringSoon($now),
+ 'can_capture' => $this->processor->canCapture($payment),
+ 'can_cancel' => $this->processor->canCancel($payment),
+ 'version' => $authorization->version(),
+ ];
+ }
+
+ /**
+ * What the amount fields' placeholder shows: the same decimal notation they accept. Empty for a
+ * currency in which a typed amount would be refused (see
+ * AuthorizedPaymentOperationProcessor::supportsTypedAmounts()), which is 2-decimal otherwise.
+ */
+ private static function remainingAmountInput(int $remainingAmount, ?string $currencyCode): string
+ {
+ if (null === $currencyCode || !AuthorizedPaymentOperationProcessor::supportsTypedAmounts($currencyCode)) {
+ return '';
+ }
+
+ return \number_format(AmountHelper::fromCents($remainingAmount, $currencyCode), 2, '.', '');
+ }
+}
diff --git a/src/Twig/OneyExtension.php b/src/Twig/OneyExtension.php
index e7952400..53513245 100644
--- a/src/Twig/OneyExtension.php
+++ b/src/Twig/OneyExtension.php
@@ -6,17 +6,17 @@
use PayPlug\SyliusPayPlugPlugin\Checker\OneyChecker;
use PayPlug\SyliusPayPlugPlugin\Gateway\OneyGatewayFactory;
+use PayPlug\SyliusPayPlugPlugin\Repository\PaymentMethodRepositoryInterface;
use Sylius\Component\Channel\Context\ChannelContextInterface;
-use Sylius\Component\Core\Model\PaymentMethod;
-use Sylius\Component\Resource\Repository\RepositoryInterface;
+use Sylius\Component\Core\Model\ChannelInterface;
+use Sylius\Component\Core\Model\PaymentMethodInterface;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
final class OneyExtension extends AbstractExtension
{
public function __construct(
- private RepositoryInterface $gatewayConfigRepository,
- private RepositoryInterface $paymentMethodRepository,
+ private PaymentMethodRepositoryInterface $paymentMethodRepository,
private ChannelContextInterface $channelContext,
private OneyChecker $oneyChecker,
) {
@@ -29,22 +29,26 @@ public function getFunctions(): array
];
}
+ /**
+ * Resolved in one channel-scoped query rather than "find any Oney gateway config, then check
+ * it happens to serve this channel". Since PRE-3628 a merchant may run one Oney config per
+ * channel; the old shape picked an arbitrary one and then usually rejected it, hiding Oney on
+ * every channel but whichever the database returned first.
+ */
public function isOneyEnabled(): bool
{
- /** @var \Sylius\Bundle\PayumBundle\Model\GatewayConfig|null $gateway */
- $gateway = $this->gatewayConfigRepository->findOneBy(['factoryName' => OneyGatewayFactory::FACTORY_NAME]);
- if (null === $gateway) {
- return false;
- }
+ $channel = $this->channelContext->getChannel();
- /** @var PaymentMethod|null $paymentMethod */
- $paymentMethod = $this->paymentMethodRepository->findOneBy(['gatewayConfig' => $gateway]);
- if (null === $paymentMethod || false === $paymentMethod->isEnabled()) {
+ if (!$channel instanceof ChannelInterface) {
return false;
}
- $currentChannel = $this->channelContext->getChannel();
- if (!$paymentMethod->getChannels()->contains($currentChannel)) {
+ $paymentMethod = $this->paymentMethodRepository->findOneEnabledByGatewayNameAndChannel(
+ OneyGatewayFactory::FACTORY_NAME,
+ $channel,
+ );
+
+ if (!$paymentMethod instanceof PaymentMethodInterface) {
return false;
}
diff --git a/src/Twig/PayPlugExtension.php b/src/Twig/PayPlugExtension.php
index 9ebfcfc7..446d75b6 100644
--- a/src/Twig/PayPlugExtension.php
+++ b/src/Twig/PayPlugExtension.php
@@ -4,8 +4,9 @@
namespace PayPlug\SyliusPayPlugPlugin\Twig;
-use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientFactory;
+use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientFactoryInterface;
use PayPlug\SyliusPayPlugPlugin\Checker\CanSaveCardCheckerInterface;
+use PayPlug\SyliusPayPlugPlugin\Gateway\PayPlugGatewayFactory;
use Sylius\Component\Core\Model\PaymentMethodInterface;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
@@ -14,7 +15,8 @@ final class PayPlugExtension extends AbstractExtension
{
public function __construct(
private CanSaveCardCheckerInterface $canSaveCardChecker,
- private PayPlugApiClientFactory $apiClientFactory,
+ private PayPlugApiClientFactoryInterface $apiClientFactory,
+ private string $hostedFieldsSdkUrl,
) {
}
@@ -23,9 +25,25 @@ public function getFunctions(): array
return [
new TwigFunction('is_save_card_enabled', $this->isSaveCardAllowed(...)),
new TwigFunction('is_payplug_test_mode_enabled', $this->isTest(...)),
+ new TwigFunction('payplug_hosted_fields_company_id', $this->hostedFieldsCompanyId(...)),
+ new TwigFunction('payplug_hosted_fields_sdk_url', $this->hostedFieldsSdkUrl(...)),
+ new TwigFunction('payplug_display_mode', $this->displayMode(...)),
];
}
+ public function hostedFieldsSdkUrl(): string
+ {
+ return $this->hostedFieldsSdkUrl;
+ }
+
+ /**
+ * @param array $config
+ */
+ public function displayMode(array $config): ?string
+ {
+ return PayPlugGatewayFactory::resolveDisplayMode($config);
+ }
+
public function isSaveCardAllowed(PaymentMethodInterface $paymentMethod): bool
{
return $this->canSaveCardChecker->isAllowed($paymentMethod);
@@ -37,4 +55,12 @@ public function isTest(PaymentMethodInterface $paymentMethod): bool
return !(bool) $client->getAccount()['is_live'];
}
+
+ public function hostedFieldsCompanyId(PaymentMethodInterface $paymentMethod): string
+ {
+ $client = $this->apiClientFactory->createForPaymentMethod($paymentMethod);
+ $companyId = $client->getAccount()['company_ref'] ?? '';
+
+ return \is_string($companyId) ? $companyId : '';
+ }
}
diff --git a/src/Upc/AuthorizationDetails.php b/src/Upc/AuthorizationDetails.php
new file mode 100644
index 00000000..332c2ab4
--- /dev/null
+++ b/src/Upc/AuthorizationDetails.php
@@ -0,0 +1,264 @@
+ true (its synchronous call was accepted but its webhook later reported it did not
+ * go through) no longer counts toward the totals, mirroring RefundPaymentProcessor's handling of
+ * a failed refund.
+ */
+final class AuthorizationDetails
+{
+ public const DEFERRED = 'hosted_fields_deferred_capture';
+
+ public const AUTHORIZED_AMOUNT = 'hosted_fields_authorized_amount';
+
+ public const MAX_CAPTURE_DATE = 'hosted_fields_max_capture_date';
+
+ public const CAPTURES = 'hosted_fields_captures';
+
+ public const CANCELLATIONS = 'hosted_fields_cancellations';
+
+ public const OPERATION_CAPTURE = 'capture';
+
+ public const OPERATION_CANCELLATION = 'cancellation';
+
+ /**
+ * How long before maxCaptureDate the admin starts warning the merchant that the
+ * authorization is about to lapse.
+ */
+ private const EXPIRY_WARNING_INTERVAL = 'PT48H';
+
+ /**
+ * @param mixed[] $details
+ */
+ private function __construct(private array $details)
+ {
+ }
+
+ /**
+ * @param mixed[] $details
+ */
+ public static function fromDetails(array $details): self
+ {
+ return new self($details);
+ }
+
+ /**
+ * The details a freshly created authorization-only payment starts with. $fallbackAmount (the
+ * Sylius payment's own amount) only stands in when the creation response carried no amount
+ * at all — e.g. a 3DS-pending response, whose authorization is not granted yet.
+ *
+ * @param mixed[] $details
+ *
+ * @return mixed[]
+ */
+ public static function open(array $details, PaymentOutput $output, int $fallbackAmount): array
+ {
+ return [
+ ...$details,
+ self::DEFERRED => true,
+ self::AUTHORIZED_AMOUNT => $output->remainingCapturableAmount ?? $fallbackAmount,
+ self::MAX_CAPTURE_DATE => $output->maxCaptureDate,
+ self::CAPTURES => [],
+ self::CANCELLATIONS => [],
+ ];
+ }
+
+ /**
+ * @param mixed[] $details
+ *
+ * @return mixed[]
+ */
+ public static function withOperation(array $details, string $operation, ?string $operationId, int $amount): array
+ {
+ $key = self::keyFor($operation);
+ $entries = \is_array($details[$key] ?? null) ? $details[$key] : [];
+ $entries[] = ['id' => $operationId, 'amount' => $amount];
+
+ return [...$details, $key => $entries];
+ }
+
+ /**
+ * @param mixed[] $details
+ *
+ * @return mixed[]
+ */
+ public static function withOperationFailed(array $details, string $operation, string $operationId): array
+ {
+ $key = self::keyFor($operation);
+ $entries = \is_array($details[$key] ?? null) ? $details[$key] : [];
+ foreach ($entries as $index => $entry) {
+ if (\is_array($entry) && ($entry['id'] ?? null) === $operationId) {
+ $entries[$index] = [...$entry, 'failed' => true];
+ }
+ }
+
+ return [...$details, $key => $entries];
+ }
+
+ public function isDeferred(): bool
+ {
+ return true === ($this->details[self::DEFERRED] ?? false);
+ }
+
+ /**
+ * ExecCodeMapper reads every successful execCode as PAID; for the authorization-only
+ * creation of a deferred payment, that success means "authorized", never "paid" — no money
+ * has moved yet.
+ */
+ public function resolveCreationOutcome(string $outcome): string
+ {
+ return $this->isDeferred() && PaymentOutcome::PAID === $outcome ? PaymentOutcome::AUTHORIZED : $outcome;
+ }
+
+ /**
+ * @return array{operation: string, amount: int}|null which of this payment's own recorded
+ * captures/cancellations $operationId belongs to, if any
+ */
+ public function findOperation(string $operationId): ?array
+ {
+ foreach ([self::OPERATION_CAPTURE, self::OPERATION_CANCELLATION] as $operation) {
+ foreach ($this->entries($operation, includeFailed: true) as $entry) {
+ if ($entry['id'] === $operationId) {
+ return ['operation' => $operation, 'amount' => $entry['amount']];
+ }
+ }
+ }
+
+ return null;
+ }
+
+ public function authorizedAmount(): int
+ {
+ $amount = $this->details[self::AUTHORIZED_AMOUNT] ?? 0;
+
+ return \is_int($amount) ? $amount : 0;
+ }
+
+ public function capturedAmount(): int
+ {
+ return self::sum($this->entries(self::OPERATION_CAPTURE));
+ }
+
+ public function cancelledAmount(): int
+ {
+ return self::sum($this->entries(self::OPERATION_CANCELLATION));
+ }
+
+ public function remainingAmount(): int
+ {
+ return \max(0, $this->authorizedAmount() - $this->capturedAmount() - $this->cancelledAmount());
+ }
+
+ public function hasCaptures(): bool
+ {
+ return [] !== $this->entries(self::OPERATION_CAPTURE);
+ }
+
+ public function maxCaptureDate(): ?\DateTimeImmutable
+ {
+ $value = $this->details[self::MAX_CAPTURE_DATE] ?? null;
+ if (!\is_string($value) || '' === $value) {
+ return null;
+ }
+
+ try {
+ return new \DateTimeImmutable($value);
+ } catch (\Exception) {
+ return null;
+ }
+ }
+
+ /**
+ * Unknown deadline is never "expired": the Unified API stays the authority and refuses a
+ * capture on a lapsed authorization itself (AuthorizationExpiredException).
+ */
+ public function isExpired(\DateTimeImmutable $now): bool
+ {
+ $maxCaptureDate = $this->maxCaptureDate();
+
+ return null !== $maxCaptureDate && $now >= $maxCaptureDate;
+ }
+
+ public function isExpiringSoon(\DateTimeImmutable $now): bool
+ {
+ $maxCaptureDate = $this->maxCaptureDate();
+
+ return null !== $maxCaptureDate &&
+ !$this->isExpired($now) &&
+ $now->add(new \DateInterval(self::EXPIRY_WARNING_INTERVAL)) >= $maxCaptureDate;
+ }
+
+ /**
+ * Changes with every capture/cancellation recorded, failed or not. Carried by the admin forms
+ * and checked again under the lock, so a replayed or double-submitted form — built against a
+ * state that has since moved on — is refused rather than triggering a second real operation.
+ */
+ public function version(): int
+ {
+ return \count($this->entries(self::OPERATION_CAPTURE, includeFailed: true)) +
+ \count($this->entries(self::OPERATION_CANCELLATION, includeFailed: true));
+ }
+
+ private static function keyFor(string $operation): string
+ {
+ return match ($operation) {
+ self::OPERATION_CAPTURE => self::CAPTURES,
+ self::OPERATION_CANCELLATION => self::CANCELLATIONS,
+ default => throw new \InvalidArgumentException(\sprintf('Unknown authorization operation "%s".', $operation)),
+ };
+ }
+
+ /**
+ * @return list
+ */
+ private function entries(string $operation, bool $includeFailed = false): array
+ {
+ $raw = $this->details[self::keyFor($operation)] ?? [];
+ if (!\is_array($raw)) {
+ return [];
+ }
+
+ $entries = [];
+ foreach ($raw as $entry) {
+ if (!\is_array($entry) || !\is_int($entry['amount'] ?? null)) {
+ continue;
+ }
+ if (!$includeFailed && true === ($entry['failed'] ?? false)) {
+ continue;
+ }
+
+ $id = $entry['id'] ?? null;
+ $entries[] = ['id' => \is_string($id) ? $id : null, 'amount' => $entry['amount']];
+ }
+
+ return $entries;
+ }
+
+ /**
+ * @param list $entries
+ */
+ private static function sum(array $entries): int
+ {
+ return \array_sum(\array_column($entries, 'amount'));
+ }
+}
diff --git a/src/Upc/AuthorizationLockKey.php b/src/Upc/AuthorizationLockKey.php
new file mode 100644
index 00000000..76eeb8ab
--- /dev/null
+++ b/src/Upc/AuthorizationLockKey.php
@@ -0,0 +1,29 @@
+ $maxCaptureDate];
+ }
+}
diff --git a/src/Upc/CardDataFromPaymentMethodExtractor.php b/src/Upc/CardDataFromPaymentMethodExtractor.php
new file mode 100644
index 00000000..7269e568
--- /dev/null
+++ b/src/Upc/CardDataFromPaymentMethodExtractor.php
@@ -0,0 +1,122 @@
+ $aliasId];
+ }
+ }
+
+ return [];
+ }
+
+ /** @return array{brand?: string, last4?: string} */
+ private static function extractFromCard(mixed $card): array
+ {
+ if (!\is_array($card)) {
+ return [];
+ }
+
+ $result = [];
+
+ $network = $card['network'] ?? null;
+ if (\is_string($network) && '' !== $network) {
+ $result['brand'] = $network;
+ }
+
+ $code6x4 = $card['code6x4'] ?? null;
+ if (\is_string($code6x4) && \strlen($code6x4) >= 4) {
+ $result['last4'] = \substr($code6x4, -4);
+ }
+
+ return $result;
+ }
+
+ /**
+ * @param bool $hasBrand true when a brand was already resolved from the card object, taking
+ * precedence over the details' own selectedBrand
+ *
+ * @return array{brand?: string, expirationMonth?: int, expirationYear?: int}
+ */
+ private static function extractFromDetails(mixed $cardDetails, bool $hasBrand): array
+ {
+ if (!\is_array($cardDetails)) {
+ return [];
+ }
+
+ $result = [];
+
+ $selectedBrand = $cardDetails['selectedBrand'] ?? null;
+ if (!$hasBrand && \is_string($selectedBrand) && '' !== $selectedBrand) {
+ $result['brand'] = $selectedBrand;
+ }
+
+ $validityDate = $cardDetails['validityDate'] ?? null;
+ if (\is_string($validityDate) && 1 === \preg_match('/^(\d{4})-(\d{2})$/', $validityDate, $matches)) {
+ $month = (int) $matches[2];
+ if ($month >= 1 && $month <= 12) {
+ $result['expirationYear'] = (int) $matches[1];
+ $result['expirationMonth'] = $month;
+ }
+ }
+
+ return $result;
+ }
+}
diff --git a/src/Upc/CustomerTitleResolver.php b/src/Upc/CustomerTitleResolver.php
new file mode 100644
index 00000000..61d9e27b
--- /dev/null
+++ b/src/Upc/CustomerTitleResolver.php
@@ -0,0 +1,30 @@
+ 'MR',
+ 'f' => 'MRS',
+ default => null,
+ };
+ }
+}
diff --git a/src/Upc/GatewayCredentialsResolver.php b/src/Upc/GatewayCredentialsResolver.php
new file mode 100644
index 00000000..6650b8d5
--- /dev/null
+++ b/src/Upc/GatewayCredentialsResolver.php
@@ -0,0 +1,50 @@
+getGatewayConfig()?->getConfig() ?? [];
+ $accountId = $gatewayConfig[PayPlugGatewayFactory::HF_IDENTIFIER] ?? null;
+ if (!\is_string($accountId) || '' === $accountId) {
+ throw new \LogicException('Hosted Fields account id is not configured for this payment method.');
+ }
+
+ return $accountId;
+ }
+}
diff --git a/src/Upc/IntegrationDescriptionProvider.php b/src/Upc/IntegrationDescriptionProvider.php
new file mode 100644
index 00000000..e3663bb5
--- /dev/null
+++ b/src/Upc/IntegrationDescriptionProvider.php
@@ -0,0 +1,62 @@
+getBillingAddress();
+ if (null === $address) {
+ return null;
+ }
+
+ return new BillingDto(
+ $this->buildAddress($address),
+ $this->buildContact($address),
+ $this->title($order),
+ );
+ }
+
+ public function createShipping(OrderInterface $order): ?ShippingDto
+ {
+ $address = $order->getShippingAddress();
+ if (null === $address) {
+ return null;
+ }
+
+ return new ShippingDto(
+ $this->buildAddress($address),
+ $this->buildContact($address),
+ $order->getCustomer()?->getEmail(),
+ $address->getCompany(),
+ );
+ }
+
+ private function buildContact(AddressInterface $address): ContactDto
+ {
+ [$phone, $mobilePhone] = $this->splitPhone($address);
+
+ return new ContactDto($address->getFirstName(), $address->getLastName(), $phone, $mobilePhone);
+ }
+
+ private function buildAddress(AddressInterface $address): AddressDto
+ {
+ return new AddressDto(
+ $address->getStreet(),
+ $address->getCity(),
+ $address->getCountryCode(),
+ // AddressDto::$state is documented as 0-3 chars, so a short province CODE (e.g. "75")
+ // fits where the full province NAME (used elsewhere in this plugin's legacy
+ // PayPlugPaymentDataCreator) would not — but Sylius province codes aren't always that
+ // short (e.g. ISO-3166-2-style "US-CA"), so only pass one through when it actually
+ // fits, rather than risk sending a malformed state to the Unified API.
+ $this->shortProvinceCode($address),
+ $address->getPostcode(),
+ );
+ }
+
+ private function shortProvinceCode(AddressInterface $address): ?string
+ {
+ $provinceCode = $address->getProvinceCode();
+
+ return null !== $provinceCode && \strlen($provinceCode) <= 3 ? $provinceCode : null;
+ }
+
+ /**
+ * @return array{0: string|null, 1: string|null} [phone, mobilePhone] — Sylius stores only one
+ * phone number per address, so only one of the two ever comes back non-null here, matching
+ * whichever type PhoneHelper::isMobile() detects it as; an unparseable/invalid number is
+ * treated as absent rather than failing the whole payment over supplementary contact data.
+ */
+ private function splitPhone(AddressInterface $address): array
+ {
+ $rawPhone = $address->getPhoneNumber();
+ $countryCode = $address->getCountryCode();
+ if (null === $rawPhone || '' === $rawPhone || null === $countryCode) {
+ return [null, null];
+ }
+
+ try {
+ $e164Phone = PhoneHelper::toE164($rawPhone, $countryCode);
+ $isMobile = PhoneHelper::isMobile($rawPhone, $countryCode);
+ } catch (InvalidPhoneNumberException) {
+ return [null, null];
+ }
+
+ return $isMobile ? [null, $e164Phone] : [$e164Phone, null];
+ }
+
+ private function title(OrderInterface $order): ?string
+ {
+ $gender = $order->getCustomer()?->getGender();
+
+ return null !== $gender ? CustomerTitleResolver::resolve($gender) : null;
+ }
+}
diff --git a/src/Upc/PaymentCaptureContextBuilder.php b/src/Upc/PaymentCaptureContextBuilder.php
new file mode 100644
index 00000000..cedea723
--- /dev/null
+++ b/src/Upc/PaymentCaptureContextBuilder.php
@@ -0,0 +1,183 @@
+getPayment()->getId());
+ // No submerchantExternalId: only the EUR MID configurations carry one, and Hosted Fields
+ // here targets the multi-currency ones. UPC omits the key entirely rather than sending null.
+ $common = new CommonFieldsDto($accountId, $amount, \strtoupper($currencyCode), $orderId);
+ $common->description = $this->resolveDescription($order);
+ // Deferred capture: open an authorization only, captured/cancelled later from the admin
+ // order screen (see AuthorizedPaymentOperationProcessor).
+ $common->capture = !PayPlugGatewayFactory::isDeferredCaptureHostedFieldsConfig(
+ $paymentRequest->getPayment()->getMethod()?->getGatewayConfig(),
+ );
+ // Confirmed with PayPlug: this field has no effect on their side regardless of value for
+ // Hosted Fields/UPC — the only working notification path is the static Cockpit-configured
+ // Receiver at /payplug/v2/ipn (see UnifiedApiIpnAction's docblock). Set anyway to keep the
+ // DTO's contract intact rather than leaving the field unset.
+ $common->notificationUrl = $this->urlGenerator->generate(
+ 'sylius_payment_request_notify',
+ ['hash' => (string) $paymentRequest->getHash()],
+ UrlGeneratorInterface::ABSOLUTE_URL,
+ );
+ $successUrl = $this->afterPayUrlProvider->getUrl($paymentRequest, UrlGeneratorInterface::ABSOLUTE_URL);
+ $common->successUrl = $successUrl;
+ $common->cancelUrl = $successUrl . '?' . http_build_query(['status' => 'canceled']);
+ if (null !== $order) {
+ $common->billing = $this->orderAddressDtoCreator->createBilling($order);
+ $common->shipping = $this->orderAddressDtoCreator->createShipping($order);
+ }
+
+ return $common;
+ }
+
+ public function buildBrowserDto(): ?BrowserDto
+ {
+ $request = $this->requestStack->getCurrentRequest();
+
+ return null !== $request
+ ? new BrowserDto(
+ $request->getClientIp() ?? '',
+ $request->headers->get('referer', '') ?? '',
+ $request->headers->get('User-Agent', '') ?? '',
+ )
+ : null;
+ }
+
+ public function resolvePaymentMethod(PaymentInterface $payment): PaymentMethodInterface
+ {
+ $method = $payment->getMethod();
+ if (null === $method) {
+ throw new \LogicException('Payment method is not set for the payment.');
+ }
+
+ return $method;
+ }
+
+ /** @return array{0: int, 1: string} */
+ public function resolveAmountAndCurrency(PaymentInterface $payment): array
+ {
+ $amount = $payment->getAmount();
+ $currencyCode = $payment->getCurrencyCode();
+ if (null === $amount || null === $currencyCode) {
+ throw new \LogicException('Payment amount or currency is not set.');
+ }
+
+ return [$amount, $currencyCode];
+ }
+
+ /**
+ * Extracted from a payment-creation response body rather than PaymentOutput itself (which
+ * carries no such fields) — needed by StatusHostedPaymentRequestHandler's 3DS polling
+ * fallback and by UnifiedApiIpnAction::resolveHostedFieldsPayment(), which looks these ids up
+ * on Payment::details. Shared by both capture handlers (token and alias) since either flow's
+ * response can carry a pending 3DS challenge that only the webhook/polling path resolves.
+ *
+ * @return array{hosted_fields_payment_id?: string, hosted_fields_operation_id?: string}
+ */
+ public function resolveHostedFieldsIds(string $body): array
+ {
+ $decoded = \json_decode($body, true);
+ $paymentId = \is_array($decoded) ? ($decoded['id'] ?? null) : null;
+ $operationIds = \is_array($decoded) ? ($decoded['operationIds'] ?? null) : null;
+ $operationId = \is_array($operationIds) ? ($operationIds[0] ?? null) : null;
+
+ // Each id is stored independently — a response carrying only one of the two (e.g. no
+ // operationIds yet) must not also drop the other, since dropping hosted_fields_operation_id
+ // silently disables the card-metadata enrichment in PayplugCardPersister::persist().
+ $result = [];
+ if (\is_string($paymentId) && '' !== $paymentId) {
+ $result['hosted_fields_payment_id'] = $paymentId;
+ }
+ if (\is_string($operationId) && '' !== $operationId) {
+ $result['hosted_fields_operation_id'] = $operationId;
+ }
+
+ return $result;
+ }
+
+ public function buildCustomerDto(?OrderInterface $order): CustomerDto
+ {
+ $customer = $order?->getCustomer();
+ if (null === $customer || null === $customer->getEmail()) {
+ throw new \LogicException('Customer email is not set for the payment.');
+ }
+
+ return new CustomerDto(ResourceIdentifier::toString($customer->getId()), $customer->getEmail());
+ }
+
+ /**
+ * Falls back to the billing address's own full name, then the customer's, so a
+ * paymentMethod.details.fullName the Unified API requires whenever saveFutureUsage is
+ * requested isn't left unset just because the billing address itself has none set.
+ */
+ public function resolveFullNameForCardDetails(?OrderInterface $order): ?string
+ {
+ $fullName = $order?->getBillingAddress()?->getFullName();
+ if (null !== $fullName && '' !== $fullName) {
+ return $fullName;
+ }
+
+ $fullName = $order?->getCustomer()?->getFullName();
+
+ return null !== $fullName && '' !== $fullName ? $fullName : null;
+ }
+
+ // Identifies the order's own product for PayPlug's back office; falls back to a generic
+ // integration/version string only when there's no order or no line item to name (e.g. this
+ // is also used by the pay-with-an-existing-alias flow, which shares the same order shape).
+ private function resolveDescription(?OrderInterface $order): ?string
+ {
+ $firstItemOrFalse = $order?->getItems()->first();
+ $firstItem = false !== $firstItemOrFalse ? $firstItemOrFalse : null;
+
+ return null !== $firstItem ? $firstItem->getProductName() : IntegrationDescriptionProvider::build();
+ }
+}
diff --git a/src/Upc/PaymentCaptureOutcomeApplier.php b/src/Upc/PaymentCaptureOutcomeApplier.php
new file mode 100644
index 00000000..d1825d45
--- /dev/null
+++ b/src/Upc/PaymentCaptureOutcomeApplier.php
@@ -0,0 +1,145 @@
+logger->error(\sprintf('[PayPlug][UPC] %s payment creation failed.', $flow->value), [
+ 'sylius_payment_id' => $payment->getId(),
+ 'error' => $e->getMessage(),
+ ]);
+ $paymentRequest->setResponseData(['error' => $e->getMessage()]);
+ $this->notifyShopper();
+ $this->stateMachine->apply($paymentRequest, PaymentRequestTransitions::GRAPH, PaymentRequestTransitions::TRANSITION_FAIL);
+ }
+
+ /**
+ * Without this the capture failure is entirely silent to the customer: the Payment stays
+ * "new" and the order "awaiting_payment" (both intentional — the payment is still
+ * retryable), so they are simply redirected to the order summary with no indication that
+ * anything went wrong.
+ *
+ * A missing session is not an error to report: this runs from the CLI
+ * (UpdatePaymentStateCommand) and from worker contexts too, where Request::getSession()
+ * throws. There is no shopper to tell in those cases, and a failed payment must not turn
+ * into a 500 because there was nowhere to put the message.
+ */
+ private function notifyShopper(): void
+ {
+ $request = $this->requestStack->getMainRequest();
+ if (null === $request || !$request->hasSession()) {
+ return;
+ }
+
+ $session = $request->getSession();
+ if (!$session instanceof FlashBagAwareSessionInterface) {
+ return;
+ }
+
+ $session->getFlashBag()->add('error', self::SHOPPER_ERROR_FLASH_KEY);
+ }
+
+ /**
+ * Refunds are keyed by the payment's top-level id, so a creation response without one leaves
+ * the payment unrefundable from the back office. Ids only: the response body carries customer
+ * data.
+ *
+ * @param array{hosted_fields_payment_id?: string, hosted_fields_operation_id?: string} $hostedFieldsIds
+ */
+ public function warnWhenPaymentIdMissing(
+ PaymentInterface $payment,
+ PaymentCaptureFlow $flow,
+ array $hostedFieldsIds,
+ ): void {
+ if (isset($hostedFieldsIds['hosted_fields_payment_id'])) {
+ return;
+ }
+
+ $this->logger->warning('[PayPlug][UPC] Payment created but the response carried no payment id; it cannot be refunded from the back office until PayPlug returns a payment id.', [
+ 'sylius_payment_id' => $payment->getId(),
+ 'flow' => $flow->value,
+ 'hosted_fields_operation_id' => $hostedFieldsIds['hosted_fields_operation_id'] ?? null,
+ ]);
+ }
+
+ public function applyOutcome(
+ PaymentRequestInterface $paymentRequest,
+ PaymentInterface $payment,
+ PaymentOutput $output,
+ ): void {
+ if (null !== $output->redirectHtml) {
+ // The "recommended for web" 3DS-pending shape — an auto-submitting HTML form the
+ // browser must render directly (see CaptureHttpResponseProvider). This is what the
+ // Unified API actually returns by default; redirectUrl only appears when the request
+ // explicitly set card.threeDSecure.displayMode=raw, which this plugin never does.
+ $paymentRequest->setResponseData(['redirect_html' => $output->redirectHtml]);
+
+ return;
+ }
+
+ if (null !== $output->redirectUrl) {
+ $paymentRequest->setResponseData(['redirect_url' => $output->redirectUrl]);
+
+ return;
+ }
+
+ $paymentRequest->setResponseData(['status' => $output->status]);
+
+ // No 3DS redirect means the outcome is already known synchronously — apply it to the
+ // actual Sylius Payment right away instead of waiting on the async webhook, which may
+ // be delayed or, in this test environment, never arrive at all. SyliusOrderStateMutator
+ // is idempotent (checks the state machine before transitioning), so it's safe to also
+ // run again if/when the webhook (e.g. NotifyHostedPaymentRequestHandler) eventually shows up.
+ $responseBody = \json_decode($output->body, true);
+ $execCode = \is_array($responseBody) ? ($responseBody['execCode'] ?? null) : null;
+ if (\is_string($execCode)) {
+ // An authorization-only payment's success is "authorized", not "paid" — see
+ // AuthorizationDetails::resolveCreationOutcome().
+ $outcome = AuthorizationDetails::fromDetails($payment->getDetails())->resolveCreationOutcome(ExecCodeMapper::toPaymentOutcome($execCode));
+ $this->orderStateMutator->apply(ResourceIdentifier::toString($payment->getId()), $outcome);
+ }
+ }
+}
diff --git a/src/Upc/PaymentOrderIdResolver.php b/src/Upc/PaymentOrderIdResolver.php
new file mode 100644
index 00000000..12252001
--- /dev/null
+++ b/src/Upc/PaymentOrderIdResolver.php
@@ -0,0 +1,29 @@
+getNumber() ?? ResourceIdentifier::toString($paymentId);
+ }
+}
diff --git a/src/Upc/PayplugCardPersister.php b/src/Upc/PayplugCardPersister.php
new file mode 100644
index 00000000..58fb90bb
--- /dev/null
+++ b/src/Upc/PayplugCardPersister.php
@@ -0,0 +1,149 @@
+getOrder();
+
+ $customer = $order?->getCustomer();
+ if (null === $customer) {
+ return;
+ }
+
+ // Payment::getMethod() is only typed to the base Payment component's PaymentMethodInterface,
+ // but Card::$paymentMethod is a mandatory (non-nullable) association requiring Sylius
+ // Core's narrower PaymentMethodInterface (the only kind Sylius actually wires up at
+ // runtime) — bail out before building a Card at all rather than persisting one with that
+ // required field left unset, which would only fail later at flush time.
+ if (!$method instanceof CorePaymentMethodInterface) {
+ return;
+ }
+
+ $gatewayConfig = $method->getGatewayConfig()?->getConfig() ?? [];
+ $isLive = true === ($gatewayConfig['live'] ?? false);
+
+ // Guards against double-saving the same alias — e.g. a 3DS payment whose webhook fires
+ // more than once for reasons outside isTreated()'s own operation-id dedupe (a different
+ // operation id notifying the same alias), or any future path that also calls persist()
+ // for an alias already stored. This check-then-act is not by itself race-proof — the
+ // synchronous frictionless capture and the async webhook can both reach persist() for the
+ // same alias — so a DB-level unique constraint on (external_id, is_live) backs it up; see
+ // the catch below.
+ if (null !== $this->payplugCardRepository->findOneBy(['externalId' => $aliasId, 'isLive' => $isLive])) {
+ return;
+ }
+
+ // $details' own hosted_fields_* values are fully client-controlled (see
+ // hosted-fields_controller.js) — used only as a display-only fallback for whatever
+ // $fetchedCardData (PayPlug's own API/webhook data) doesn't carry, and validated here
+ // rather than trusted as-is: anything not matching the expected shape is discarded as if
+ // it were absent.
+ $currentYear = (int) (new \DateTimeImmutable())->format('Y');
+ $brand = $fetchedCardData['brand'] ?? $this->sanitizeBrand($details['hosted_fields_selected_brand'] ?? null);
+ $last4 = $fetchedCardData['last4']
+ ?? $this->sanitizeString($details['hosted_fields_last4'] ?? null, self::LAST4_PATTERN);
+ $expirationMonth = $fetchedCardData['expirationMonth']
+ ?? $this->sanitizeIntInRange($details['hosted_fields_expiration_month'] ?? null, 1, 12);
+ $expirationYear = $fetchedCardData['expirationYear']
+ ?? $this->sanitizeIntInRange(
+ $details['hosted_fields_expiration_year'] ?? null,
+ $currentYear,
+ $currentYear + self::MAX_EXPIRATION_YEARS_AHEAD,
+ );
+ // No card country field exists on the operation resource (confirmed against a real
+ // staging response) — $countryCode keeps relying entirely on the client-submitted
+ // $details value.
+ $countryCodeCandidate = $this->sanitizeString($details['hosted_fields_country'] ?? null, self::COUNTRY_CODE_PATTERN);
+ $countryCode = null !== $countryCodeCandidate ? \strtoupper($countryCodeCandidate) : null;
+
+ /** @var Card $card */
+ $card = $this->payplugCardFactory->createNew();
+ $card
+ ->setCustomer($customer)
+ ->setExternalId($aliasId)
+ ->setBrand(\is_string($brand) ? $brand : '')
+ ->setLast4(\is_string($last4) ? $last4 : '')
+ ->setExpirationMonth(\is_int($expirationMonth) ? $expirationMonth : 0)
+ ->setExpirationYear(\is_int($expirationYear) ? $expirationYear : 0)
+ ->setCountryCode(\is_string($countryCode) ? $countryCode : '')
+ ->setIsLive($isLive)
+ ->setPaymentMethod($method)
+ ;
+
+ try {
+ $this->payplugCardRepository->add($card);
+ } catch (UniqueConstraintViolationException) {
+ // The findOneBy() guard above lost a race against a concurrent persist() call for the
+ // same alias — that other call already stored the canonical Card row, so there is
+ // nothing left to do here. Doctrine's UnitOfWork closes the EntityManager on ANY
+ // flush failure, catch included — reset the registry so Doctrine work resolved fresh
+ // after this point doesn't inherit the now-closed instance.
+ $this->managerRegistry->resetManager();
+ }
+ }
+
+ private function sanitizeBrand(mixed $value): ?string
+ {
+ return \is_string($value) && \in_array($value, self::ALLOWED_BRANDS, true) ? $value : null;
+ }
+
+ private function sanitizeString(mixed $value, string $pattern): ?string
+ {
+ return \is_string($value) && 1 === \preg_match($pattern, $value) ? $value : null;
+ }
+
+ private function sanitizeIntInRange(mixed $value, int $min, int $max): ?int
+ {
+ return \is_int($value) && $value >= $min && $value <= $max ? $value : null;
+ }
+}
diff --git a/src/Upc/RefundCreatorInterface.php b/src/Upc/RefundCreatorInterface.php
new file mode 100644
index 00000000..3e84bff4
--- /dev/null
+++ b/src/Upc/RefundCreatorInterface.php
@@ -0,0 +1,35 @@
+paymentRepository->find((int) $orderId);
+ if (null === $payment) {
+ $this->logger->error('[PayPlug][UPC] Cannot apply payment outcome: payment not found.', [
+ 'sylius_payment_id' => $orderId,
+ 'outcome' => $outcome,
+ ]);
+
+ return;
+ }
+
+ $transition = match ($outcome) {
+ PaymentOutcome::PAID, PaymentOutcome::CAPTURE_REQUIRED => PaymentTransitions::TRANSITION_COMPLETE,
+ PaymentOutcome::AUTHORIZED => PaymentTransitions::TRANSITION_AUTHORIZE,
+ PaymentOutcome::REFUNDED => PaymentTransitions::TRANSITION_REFUND,
+ PaymentOutcome::FAILED => PaymentTransitions::TRANSITION_FAIL,
+ // THREE_DS_PENDING is not applied as a state transition here: it's not a final
+ // outcome, so there is nothing to transition to yet — the payment simply stays
+ // "processing" until a real outcome arrives. The three async notification call sites
+ // (HostedFieldsWebhookNotificationHandler::treat(), NotifyHostedPaymentRequestHandler,
+ // and StatusHostedPaymentRequestHandler via delegation to treat()) already guard
+ // against THREE_DS_PENDING before ever reaching this mutator; this arm is the
+ // defensive backstop for the one caller that doesn't (CaptureHostedPaymentRequestHandler's
+ // synchronous branch, which passes ExecCodeMapper::toPaymentOutcome() straight through).
+ default => null,
+ };
+
+ if (null === $transition) {
+ return;
+ }
+
+ if (!$this->stateMachine->can($payment, PaymentTransitions::GRAPH, $transition)) {
+ $this->logger->warning('[PayPlug][UPC] Cannot apply payment transition (already applied or incompatible with current state).', [
+ 'sylius_payment_id' => $orderId,
+ 'current_state' => $payment->getState(),
+ 'transition' => $transition,
+ 'outcome' => $outcome,
+ ]);
+
+ return;
+ }
+
+ $this->stateMachine->apply($payment, PaymentTransitions::GRAPH, $transition);
+ }
+}
diff --git a/src/Upc/SyliusPaymentOperationRepository.php b/src/Upc/SyliusPaymentOperationRepository.php
new file mode 100644
index 00000000..58b9b13c
--- /dev/null
+++ b/src/Upc/SyliusPaymentOperationRepository.php
@@ -0,0 +1,86 @@
+findOneBy(['orderId' => $orderId]);
+ if (null === $entity) {
+ throw new PaymentNotFoundException(\sprintf('No operation for order "%s".', $orderId));
+ }
+
+ return $entity->toOperationData();
+ }
+
+ public function getByOperationId(string $operationId): OperationData
+ {
+ $entity = $this->findOneByOperationId($operationId);
+ if (null === $entity) {
+ throw new PaymentNotFoundException(\sprintf('No operation "%s".', $operationId));
+ }
+
+ return $entity->toOperationData();
+ }
+
+ public function save(OperationData $operationData): void
+ {
+ $entity = $this->findOneByOperationId($operationData->operationId);
+ if (null === $entity) {
+ $entity = new PayPlugOperation(
+ $operationData->orderId,
+ $operationData->operationId,
+ $operationData->execCode,
+ $operationData->outcome,
+ $operationData->amount,
+ );
+ $this->entityManager->persist($entity);
+ }
+
+ $this->entityManager->flush();
+ }
+
+ public function markTreated(string $operationId): void
+ {
+ $entity = $this->findOneByOperationId($operationId);
+ if (null === $entity) {
+ throw new PaymentNotFoundException(\sprintf('No operation "%s".', $operationId));
+ }
+
+ $entity->markTreated();
+ $this->entityManager->flush();
+ }
+
+ public function isTreated(string $operationId): bool
+ {
+ $entity = $this->findOneByOperationId($operationId);
+
+ return null !== $entity && $entity->isTreated();
+ }
+
+ /**
+ * @param array $criteria
+ */
+ private function findOneBy(array $criteria): ?PayPlugOperation
+ {
+ return $this->entityManager->getRepository(PayPlugOperation::class)->findOneBy($criteria);
+ }
+
+ private function findOneByOperationId(string $operationId): ?PayPlugOperation
+ {
+ return $this->findOneBy(['operationId' => $operationId]);
+ }
+}
diff --git a/src/Upc/SyliusUnifiedApiHttpClient.php b/src/Upc/SyliusUnifiedApiHttpClient.php
new file mode 100644
index 00000000..a15a82bd
--- /dev/null
+++ b/src/Upc/SyliusUnifiedApiHttpClient.php
@@ -0,0 +1,82 @@
+send('GET', $url, ['headers' => $headers]);
+ }
+
+ public function postJson(string $url, array $body, array $headers = []): array
+ {
+ return $this->send('POST', $url, ['json' => $body, 'headers' => $headers]);
+ }
+
+ /**
+ * @param array $options
+ *
+ * @return array{status: int, body: string}
+ */
+ private function send(string $method, string $url, array $options): array
+ {
+ // Off only when payplug.unified_api_verify_tls is explicitly disabled for a QA/staging
+ // host with an untrusted internal CA (see config/services.yaml) — never in production.
+ if (!$this->unifiedApiVerifyTls) {
+ $options['verify_peer'] = false;
+ $options['verify_host'] = false;
+ }
+
+ $options['timeout'] = self::REQUEST_TIMEOUT_SECONDS;
+
+ $this->logger->debug('[PayPlug debug] Unified API raw request.', [
+ 'method' => $method,
+ 'url' => $url,
+ 'headers' => $options['headers'] ?? [],
+ 'json' => $options['json'] ?? null,
+ ]);
+
+ try {
+ $response = $this->httpClient->request($method, $url, $options);
+ $status = $response->getStatusCode();
+ $body = $response->getContent(false);
+
+ $this->logger->debug('[PayPlug debug] Unified API raw response.', [
+ 'status' => $status,
+ 'body' => $body,
+ ]);
+
+ return [
+ 'status' => $status,
+ 'body' => $body,
+ ];
+ } catch (TransportExceptionInterface $e) {
+ $this->logger->debug('[PayPlug debug] Unified API transport exception.', [
+ 'message' => $e->getMessage(),
+ ]);
+
+ return ['status' => 0, 'body' => $e->getMessage()];
+ }
+ }
+}
diff --git a/src/Upc/SyliusUpcConfigurationRepository.php b/src/Upc/SyliusUpcConfigurationRepository.php
new file mode 100644
index 00000000..24c6d8be
--- /dev/null
+++ b/src/Upc/SyliusUpcConfigurationRepository.php
@@ -0,0 +1,111 @@
+gatewayConfig = $gatewayConfig;
+
+ return $scoped;
+ }
+
+ public function forPaymentMethod(PaymentMethodInterface $paymentMethod): ScopedConfigurationRepositoryInterface
+ {
+ return $this->withGatewayConfig(
+ $paymentMethod->getGatewayConfig()
+ ?? throw new \LogicException('The payment method has no gateway config, so no PayPlug account can be resolved for it.'),
+ );
+ }
+
+ public function get(string $key): ?string
+ {
+ $value = $this->findGatewayConfig()->getConfig()[$key] ?? null;
+
+ return \is_string($value) ? $value : null;
+ }
+
+ public function set(string $key, string $value): void
+ {
+ $gatewayConfig = $this->findGatewayConfig();
+ $gatewayConfig->setConfig([...$gatewayConfig->getConfig(), $key => $value]);
+ $this->entityManager->flush();
+ }
+
+ public function getClientId(): string
+ {
+ $value = $this->getClientConfig()['client_id'] ?? '';
+
+ return \is_string($value) ? $value : '';
+ }
+
+ public function getClientSecret(): string
+ {
+ $value = $this->getClientConfig()['client_secret'] ?? '';
+
+ return \is_string($value) ? $value : '';
+ }
+
+ public function getPublicKeyId(): string
+ {
+ $value = $this->findGatewayConfig()->getConfig()[PayPlugGatewayFactory::HF_IDENTIFIER] ?? '';
+
+ return \is_string($value) ? $value : '';
+ }
+
+ /**
+ * Not populated by any current admin field. Returns '' until a future ticket adds a
+ * distinct public-key-value config field.
+ */
+ public function getPublicKeyValue(): string
+ {
+ return '';
+ }
+
+ /**
+ * @return array
+ */
+ private function getClientConfig(): array
+ {
+ $config = $this->findGatewayConfig()->getConfig();
+ $isLive = true === ($config['live'] ?? false);
+ $rawClientConfig = $isLive ? ($config['live_client'] ?? null) : ($config['test_client'] ?? null);
+
+ if (!\is_array($rawClientConfig)) {
+ return [];
+ }
+
+ return $rawClientConfig;
+ }
+
+ /**
+ * Deliberately no fallback to findOneBy(['factoryName' => …]). That lookup was correct only
+ * while exactly one CB gateway config could exist; since PRE-3628 it returns an arbitrary one
+ * of several, which would sign a channel's payment with another channel's account. An
+ * unscoped call is a bug at the call site, so it fails loudly here rather than silently
+ * resolving the wrong merchant.
+ */
+ private function findGatewayConfig(): GatewayConfigInterface
+ {
+ return $this->gatewayConfig ?? throw new \LogicException(
+ 'The UPC configuration repository has not been scoped to a gateway config. ' .
+ 'Call withGatewayConfig() with the gateway config of the payment method being handled.',
+ );
+ }
+}
diff --git a/src/Upc/SyliusUpcLock.php b/src/Upc/SyliusUpcLock.php
new file mode 100644
index 00000000..5e801dea
--- /dev/null
+++ b/src/Upc/SyliusUpcLock.php
@@ -0,0 +1,39 @@
+ */
+ private array $locks = [];
+
+ public function __construct(private LockFactory $lockFactory)
+ {
+ }
+
+ public function acquire(string $key, int $ttlSeconds): bool
+ {
+ $lock = $this->lockFactory->createLock($key, $ttlSeconds);
+ if (!$lock->acquire(false)) {
+ return false;
+ }
+
+ $this->locks[$key] = $lock;
+
+ return true;
+ }
+
+ public function release(string $key): void
+ {
+ if (isset($this->locks[$key])) {
+ $this->locks[$key]->release();
+ unset($this->locks[$key]);
+ }
+ }
+}
diff --git a/src/Upc/SyliusUpcLogger.php b/src/Upc/SyliusUpcLogger.php
new file mode 100644
index 00000000..b64ccab1
--- /dev/null
+++ b/src/Upc/SyliusUpcLogger.php
@@ -0,0 +1,30 @@
+psrLogger->debug($message, $context);
+ }
+
+ public function info(string $message, array $context = []): void
+ {
+ $this->psrLogger->info($message, $context);
+ }
+
+ public function error(string $message, array $context = []): void
+ {
+ $this->psrLogger->error($message, $context);
+ }
+}
diff --git a/src/Upc/UnifiedApiAuthorizationOperator.php b/src/Upc/UnifiedApiAuthorizationOperator.php
new file mode 100644
index 00000000..8a2bc739
--- /dev/null
+++ b/src/Upc/UnifiedApiAuthorizationOperator.php
@@ -0,0 +1,74 @@
+createService($method)->capturePayment(
+ $paymentId,
+ GatewayCredentialsResolver::resolve($method),
+ $orderId,
+ \sprintf('Capture #%d for order %s', $sequence, $orderId),
+ $amount,
+ null,
+ $currency,
+ );
+ }
+
+ public function cancel(
+ PaymentMethodInterface $method,
+ string $paymentId,
+ string $orderId,
+ ?int $amount,
+ ?string $currency = null,
+ int $sequence = 1,
+ ): CancellationOutput {
+ return $this->createService($method)->cancelPayment(
+ $paymentId,
+ GatewayCredentialsResolver::resolve($method),
+ $orderId,
+ \sprintf('Cancellation #%d for order %s', $sequence, $orderId),
+ $amount,
+ null,
+ $currency,
+ );
+ }
+
+ private function createService(PaymentMethodInterface $method): UnifiedApiPaymentService
+ {
+ $configuration = $this->configurationRepository->forPaymentMethod($method);
+
+ return new UnifiedApiPaymentService(
+ $this->httpClient,
+ $this->tokenManager,
+ $this->unifiedApiBaseUrl,
+ $configuration->getClientId(),
+ $configuration->getClientSecret(),
+ );
+ }
+}
diff --git a/src/Upc/UnifiedApiOperationStatusFetcher.php b/src/Upc/UnifiedApiOperationStatusFetcher.php
new file mode 100644
index 00000000..38994784
--- /dev/null
+++ b/src/Upc/UnifiedApiOperationStatusFetcher.php
@@ -0,0 +1,36 @@
+configurationRepository->forPaymentMethod($method);
+
+ $service = new UnifiedApiPaymentService(
+ $this->httpClient,
+ $this->tokenManager,
+ $this->unifiedApiBaseUrl,
+ $configuration->getClientId(),
+ $configuration->getClientSecret(),
+ );
+
+ return $service->getOperation($operationId);
+ }
+}
diff --git a/src/Upc/UnifiedApiPaymentCreator.php b/src/Upc/UnifiedApiPaymentCreator.php
new file mode 100644
index 00000000..779714ad
--- /dev/null
+++ b/src/Upc/UnifiedApiPaymentCreator.php
@@ -0,0 +1,38 @@
+configurationRepository->forPaymentMethod($method);
+
+ $service = new UnifiedApiPaymentService(
+ $this->httpClient,
+ $this->tokenManager,
+ $this->unifiedApiBaseUrl,
+ $configuration->getClientId(),
+ $configuration->getClientSecret(),
+ );
+
+ return $service->createPayment($dto);
+ }
+}
diff --git a/src/Upc/UnifiedApiPaymentCreatorInterface.php b/src/Upc/UnifiedApiPaymentCreatorInterface.php
new file mode 100644
index 00000000..e33cd35d
--- /dev/null
+++ b/src/Upc/UnifiedApiPaymentCreatorInterface.php
@@ -0,0 +1,22 @@
+configurationRepository->forPaymentMethod($method);
+
+ $service = new UnifiedApiPaymentService(
+ $this->httpClient,
+ $this->tokenManager,
+ $this->unifiedApiBaseUrl,
+ $configuration->getClientId(),
+ $configuration->getClientSecret(),
+ );
+
+ return $service->createRefund(
+ $operationId,
+ $accountId,
+ $orderId,
+ \sprintf('Refund for order %s', $orderId),
+ null,
+ $amount,
+ $currency,
+ );
+ }
+}
diff --git a/src/Validator/PaymentMethodValidator.php b/src/Validator/PaymentMethodValidator.php
index 1ce571aa..26aa539f 100644
--- a/src/Validator/PaymentMethodValidator.php
+++ b/src/Validator/PaymentMethodValidator.php
@@ -12,8 +12,10 @@
use PayPlug\SyliusPayPlugPlugin\Gateway\OneyGatewayFactory;
use PayPlug\SyliusPayPlugPlugin\Gateway\PayPlugGatewayFactory;
use PayPlug\SyliusPayPlugPlugin\Gateway\ScalapayGatewayFactory;
+use PayPlug\SyliusPayPlugPlugin\Gateway\Validator\Constraints\HasNoGatewayChannelConflict;
use PayPlug\SyliusPayPlugPlugin\Gateway\Validator\Constraints\IsCanSavePaymentMethod;
use PayPlug\SyliusPayPlugPlugin\Gateway\Validator\Constraints\IsOneyEnabled;
+use PayPlug\SyliusPayPlugPlugin\Gateway\Validator\Constraints\IsScalapayAmountRangeValid;
use PayPlug\SyliusPayPlugPlugin\Gateway\Validator\Constraints\PayplugPermission;
use PayPlug\SyliusPayPlugPlugin\Gateway\WeroGatewayFactory;
use Sylius\Component\Core\Model\PaymentMethodInterface;
@@ -45,11 +47,11 @@ public function process(PaymentMethodInterface $paymentMethod): void
$errors = match ($paymentMethod->getGatewayConfig()->getFactoryName()) {
PayPlugGatewayFactory::FACTORY_NAME => $this->processPayplug($paymentMethod),
OneyGatewayFactory::FACTORY_NAME => $this->processOney($paymentMethod),
- BancontactGatewayFactory::FACTORY_NAME => $this->processBancontact($paymentMethod),
- AmericanExpressGatewayFactory::FACTORY_NAME => $this->processAmex($paymentMethod),
- ApplePayGatewayFactory::FACTORY_NAME => $this->processApplePay($paymentMethod),
+ BancontactGatewayFactory::FACTORY_NAME => $this->processDefault($paymentMethod),
+ AmericanExpressGatewayFactory::FACTORY_NAME => $this->processDefault($paymentMethod),
+ ApplePayGatewayFactory::FACTORY_NAME => $this->processDefault($paymentMethod),
ScalapayGatewayFactory::FACTORY_NAME => $this->processScalapay($paymentMethod),
- WeroGatewayFactory::FACTORY_NAME => $this->processWero($paymentMethod),
+ WeroGatewayFactory::FACTORY_NAME => $this->processDefault($paymentMethod),
default => throw new \InvalidArgumentException('Unsupported payment method'),
};
@@ -68,57 +70,38 @@ private function processPayplug(PaymentMethodInterface $paymentMethod): Constrai
$config = $paymentMethod->getGatewayConfig()?->getConfig() ?? [];
$constraintList = [new IsCanSavePaymentMethod()];
- if (true === $config[PayPlugGatewayFactory::ONE_CLICK]) {
+ if (true === ($config[PayPlugGatewayFactory::ONE_CLICK] ?? false)) {
$constraintList[] = new PayplugPermission(Permission::CAN_SAVE_CARD);
}
- if (true === $config[PayPlugGatewayFactory::DEFERRED_CAPTURE]) {
+ if (true === ($config[PayPlugGatewayFactory::DEFERRED_CAPTURE] ?? false)) {
$constraintList[] = new PayplugPermission(Permission::CAN_CREATE_DEFERRED_PAYMENT);
}
- if (true === $config[PayPlugGatewayFactory::INTEGRATED_PAYMENT]) {
+ if (true === ($config[PayPlugGatewayFactory::INTEGRATED_PAYMENT] ?? false)) {
$constraintList[] = new PayplugPermission(Permission::CAN_USE_INTEGRATED_PAYMENTS);
}
- return $this->validator->validate($paymentMethod, $constraintList, self::VALIDATION_GROUPS);
- }
-
- private function processOney(PaymentMethodInterface $paymentMethod): ConstraintViolationListInterface
- {
- $constraintList = [new IsOneyEnabled()];
-
- return $this->validator->validate($paymentMethod, $constraintList, self::VALIDATION_GROUPS);
- }
-
- private function processBancontact(PaymentMethodInterface $paymentMethod): ConstraintViolationListInterface
- {
- $constraintList = [new IsCanSavePaymentMethod()];
+ $constraintList[] = new HasNoGatewayChannelConflict();
return $this->validator->validate($paymentMethod, $constraintList, self::VALIDATION_GROUPS);
}
- private function processAmex(PaymentMethodInterface $paymentMethod): ConstraintViolationListInterface
+ private function processOney(PaymentMethodInterface $paymentMethod): ConstraintViolationListInterface
{
- $constraintList = [new IsCanSavePaymentMethod()];
+ $constraintList = [new IsOneyEnabled(), new HasNoGatewayChannelConflict()];
return $this->validator->validate($paymentMethod, $constraintList, self::VALIDATION_GROUPS);
}
- private function processApplePay(PaymentMethodInterface $paymentMethod): ConstraintViolationListInterface
+ private function processDefault(PaymentMethodInterface $paymentMethod): ConstraintViolationListInterface
{
- $constraintList = [new IsCanSavePaymentMethod()];
+ $constraintList = [new IsCanSavePaymentMethod(), new HasNoGatewayChannelConflict()];
return $this->validator->validate($paymentMethod, $constraintList, self::VALIDATION_GROUPS);
}
private function processScalapay(PaymentMethodInterface $paymentMethod): ConstraintViolationListInterface
{
- $constraintList = [new IsCanSavePaymentMethod()];
-
- return $this->validator->validate($paymentMethod, $constraintList, self::VALIDATION_GROUPS);
- }
-
- private function processWero(PaymentMethodInterface $paymentMethod): ConstraintViolationListInterface
- {
- $constraintList = [new IsCanSavePaymentMethod()];
+ $constraintList = [new IsCanSavePaymentMethod(), new IsScalapayAmountRangeValid(), new HasNoGatewayChannelConflict()];
return $this->validator->validate($paymentMethod, $constraintList, self::VALIDATION_GROUPS);
}
diff --git a/templates/admin/order/show/authorization.html.twig b/templates/admin/order/show/authorization.html.twig
new file mode 100644
index 00000000..1af73ed3
--- /dev/null
+++ b/templates/admin/order/show/authorization.html.twig
@@ -0,0 +1,110 @@
+{% import '@SyliusAdmin/shared/helper/money.html.twig' as money %}
+
+{# One card per deferred-capture Hosted Fields payment of the order; nothing at all for any other
+ payment. Which actions are offered comes from payplug_authorization() — the same rules
+ AuthorizedPaymentController enforces — so a form is only ever shown when it can succeed as
+ far as Sylius knows; the Unified API stays the final judge, and its refusal comes back as a
+ flash message. #}
+{% set order = hookable_metadata.context.resource %}
+
+{% for payment in order.payments %}
+ {% set authorization = payplug_authorization(payment) %}
+ {% if authorization is not null %}
+ {% set currency = payment.currencyCode %}
+ {% set csrf_token_id = constant('PayPlug\\SyliusPayPlugPlugin\\Action\\Admin\\AuthorizedPaymentController::CSRF_TOKEN_ID_PREFIX') ~ payment.id %}
+ {% set csrf_token_value = sylius_csrf_protection_enabled() ? csrf_token(csrf_token_id) : null %}
+ {# Disables the button on the first submit, so a double click sends one request; the server
+ side (lock + version) is what actually guarantees it. #}
+ {% set single_submit = "this.querySelector('button[type=submit]').disabled = true;" %}
+
+
+
+
+
+ - {{ 'payplug_sylius_payplug_plugin.admin.authorization.authorized_amount'|trans }}
+ - {{ money.format(authorization.authorized_amount, currency) }}
+ - {{ 'payplug_sylius_payplug_plugin.admin.authorization.captured_amount'|trans }}
+ - {{ money.format(authorization.captured_amount, currency) }}
+ {% if authorization.cancelled_amount > 0 %}
+ - {{ 'payplug_sylius_payplug_plugin.admin.authorization.cancelled_amount'|trans }}
+ - {{ money.format(authorization.cancelled_amount, currency) }}
+ {% endif %}
+ - {{ 'payplug_sylius_payplug_plugin.admin.authorization.remaining_amount'|trans }}
+ -
+ {{ money.format(authorization.remaining_amount, currency) }}
+
+ - {{ 'payplug_sylius_payplug_plugin.admin.authorization.max_capture_date'|trans }}
+ -
+ {% if authorization.max_capture_date is not null %}
+ {{ authorization.max_capture_date|date('d/m/Y H:i') }}
+ {% else %}
+ {{ 'payplug_sylius_payplug_plugin.admin.authorization.max_capture_date_unknown'|trans }}
+ {% endif %}
+
+
+
+ {% if authorization.expired %}
+
{{ 'payplug_sylius_payplug_plugin.admin.authorization.expired'|trans }}
+ {% elseif authorization.expiring_soon and authorization.can_capture %}
+
{{ 'payplug_sylius_payplug_plugin.admin.authorization.expiring_soon'|trans }}
+ {% endif %}
+
+ {% if authorization.can_capture %}
+
+ {% endif %}
+
+ {% if authorization.can_cancel %}
+
+ {% endif %}
+
+ {% if not authorization.can_capture and not authorization.expired %}
+
+ {% if authorization.remaining_amount == 0 %}
+ {{ 'payplug_sylius_payplug_plugin.admin.authorization.nothing_remaining'|trans }}
+ {% else %}
+ {{ 'payplug_sylius_payplug_plugin.admin.authorization.not_authorized'|trans }}
+ {% endif %}
+
+ {% endif %}
+
+
+ {% endif %}
+{% endfor %}
diff --git a/templates/admin/order/show/payment_complete.html.twig b/templates/admin/order/show/payment_complete.html.twig
new file mode 100644
index 00000000..174dbcd1
--- /dev/null
+++ b/templates/admin/order/show/payment_complete.html.twig
@@ -0,0 +1,7 @@
+{# Replaces Sylius's own "Complete" payment action only for a deferred-capture Hosted Fields
+ payment, whose capture is driven from the PayPlug authorization block instead (partial amounts,
+ remaining amount, deadline, explicit refusal messages). Every other payment keeps Sylius's
+ action untouched. #}
+{% if payplug_authorization(hookable_metadata.context.payment) is null %}
+ {% include '@SyliusAdmin/order/show/content/sections/payments/item/actions/complete.html.twig' %}
+{% endif %}
diff --git a/templates/admin/payment_method/form/connected_account.html.twig b/templates/admin/payment_method/form/connected_account.html.twig
new file mode 100644
index 00000000..870ba964
--- /dev/null
+++ b/templates/admin/payment_method/form/connected_account.html.twig
@@ -0,0 +1,47 @@
+{# Read-only: the email is written by the OAuth callback, never edited here, so this is plain text
+ rather than a form field. Sourced from `form.vars.data` — the same path the surrounding Sylius
+ section template uses to read `gatewayConfig.factoryName` — since `account_email` is stored on
+ the gateway config without a form child of its own. #}
+{% set payment_method = hookable_metadata.context.form.vars.data %}
+{% set gateway_config = payment_method.gatewayConfig %}
+{% set account_email = gateway_config ? gateway_config.config['account_email']|default(null) : null %}
+{# What logout would actually clear. Absent on a gateway saved but never authorized, where the
+ button would be a no-op that disables a payment method for nothing. #}
+{% set is_connected = gateway_config and (gateway_config.config['live_client']|default(null) or gateway_config.config['test_client']|default(null)) %}
+
+
+
{{ 'payplug_sylius_payplug_plugin.ui.connected_account'|trans }}
+ {# Three states, not two: a gateway that was never authorized has no email *and* no credentials,
+ and telling that merchant to "re-authenticate" implies a connection they never had. The
+ "unknown" wording is for a gateway connected before PRE-3631, which has credentials but no
+ account_email until the merchant reconnects. #}
+ {% if not is_connected %}
+
{{ 'payplug_sylius_payplug_plugin.ui.not_connected'|trans }}
+ {% elseif account_email %}
+
{{ account_email }}
+ {% else %}
+
{{ 'payplug_sylius_payplug_plugin.ui.connected_account_unknown'|trans }}
+ {% endif %}
+
+ {# A link rather than a submit button: this block is rendered inside the payment-method form,
+ and a nested
diff --git a/templates/admin/payment_method/form/hf_identifier.html.twig b/templates/admin/payment_method/form/hf_identifier.html.twig
new file mode 100644
index 00000000..0942b7be
--- /dev/null
+++ b/templates/admin/payment_method/form/hf_identifier.html.twig
@@ -0,0 +1,5 @@
+{% set form = hookable_metadata.context.form.gatewayConfig.config.hfIdentifier %}
+
+
+ {{ form_row(form) }}
+
diff --git a/templates/admin/payment_method/form/integrated_payment.html.twig b/templates/admin/payment_method/form/hosted_fields_mode.html.twig
similarity index 80%
rename from templates/admin/payment_method/form/integrated_payment.html.twig
rename to templates/admin/payment_method/form/hosted_fields_mode.html.twig
index 26c8c373..f63517fd 100644
--- a/templates/admin/payment_method/form/integrated_payment.html.twig
+++ b/templates/admin/payment_method/form/hosted_fields_mode.html.twig
@@ -1,5 +1,5 @@
-{% set form = hookable_metadata.context.form.gatewayConfig.config.integratedPayment %}
+{% set form = hookable_metadata.context.form.gatewayConfig.config.hostedFieldsMode %}
{{ form_row(form) }}
-
\ No newline at end of file
+
diff --git a/templates/admin/payment_method/form/scalapay_amount_range.html.twig b/templates/admin/payment_method/form/scalapay_amount_range.html.twig
new file mode 100644
index 00000000..6a0c0b3b
--- /dev/null
+++ b/templates/admin/payment_method/form/scalapay_amount_range.html.twig
@@ -0,0 +1,9 @@
+{% set min_amount_form = hookable_metadata.context.form.gatewayConfig.config.min_amount %}
+{% set max_amount_form = hookable_metadata.context.form.gatewayConfig.config.max_amount %}
+
+