Skip to content

Feature/pre 3469#305

Draft
jhoaraupp wants to merge 11 commits into
developfrom
feature/PRE-3469
Draft

Feature/pre 3469#305
jhoaraupp wants to merge 11 commits into
developfrom
feature/PRE-3469

Conversation

@jhoaraupp

Copy link
Copy Markdown
Contributor

Description

Spike/proof-of-concept validating that unified-plugin-core's 6 core contracts (Contracts/) are actually implementable against Sylius before they get frozen. Produces throwaway skeleton implementations of the 4 highest-risk contracts under src/Spike/, backed by real PHPUnit coverage at two levels — not production code, and not meant to be merged as-is into the plugin's real service wiring.

What was built (src/Spike/):

  • SyliusOrderStateMutator — implements IOrderStateMutator, maps PaymentOutcome::* to Sylius PaymentTransitions::* on the sylius_payment Symfony Workflow graph via StateMachineInterface::can()/apply(), guarded exactly like the plugin's existing PaymentStateResolver. THREE_DS_PENDING is a confirmed no-op (order stays new until the webhook resolves it).
  • SyliusTokenCache — implements ITokenCache directly against Psr\Cache\CacheItemPoolInterface (getItem/save/deleteItem), matching the interface's own docblock sketch.
  • SyliusConfigurationRepository — implements IConfigurationRepository over Sylius's GatewayConfigInterface (live_client/test_client scoping), with no credential value ever interpolated into a log or exception message.
  • SyliusPaymentRepository + Entity/PayplugOperation — implements IPaymentRepository backed by a new, normalized Doctrine entity for OperationData (no dependency on the payplug/payplug-php SDK), since the plugin's current storage (Payment::details JSON blob, LIKE-searched) can't support markTreated()/isTreated() idempotency.

Verdict: no blocking friction. All 4 contracts are satisfiable as-is. Full findings, including a couple of adaptation notes that don't require interface changes, are in src/Spike/VALIDATION.md.

Testing, two levels:

  • 38 unit tests (tests/PHPUnit/Spike/*Test.php) — native PHPUnit mocks on every collaborator.
  • 5 integration tests (SpikeIntegrationTest.php) against a really booted Sylius kernel: real Doctrine EntityManager, real Symfony Workflow state machine, real PSR-6 cache pool, real MariaDB — no mocks.

The integration pass caught a real bug the unit tests couldn't see: SyliusOrderStateMutator never flushed the EntityManager after the state machine transition (Symfony Workflow only mutates the state property in memory), so the transition silently never persisted. Fixed by injecting EntityManagerInterface and flushing after every applied transition — same pattern the plugin's existing PaymentStateResolver already uses.

Two unrelated pre-existing issues were also found and fixed along the way since they blocked running this work at all:

  • unified-plugin-core's composer.json pinned symfony/polyfill-mbstring to an exact version, which conflicted with sylius/sylius's own requirement — made it impossible to composer install this library alongside Sylius. Relaxed to a caret constraint (separate fix in unified-plugin-core).
  • phpunit.xml.dist set KERNEL_CLASS_PATH instead of KERNEL_CLASS (the variab KernelTestCase) — meant no kernel/functional test could ever run in thisrepo. Fixed.

Motivation: UHF (Unified Hosted Fields) targets Sylius as its first CMS inte6 UPC contracts we need proof they're implementable without friction,particularly the ones flagged as risky (Symfony Workflow constraints, PSR-6 dential storage).

Precision: The src/Spike/ files are deliberately kept outside the usual directories (src/Entity, etc.) and documented as proof-of-concept skeletons, not production code — worth re-stating this in the reviewer notes to avoid any confusion during review.

Related issue(s): Closes PRE-3469


Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue) [ ]
  • ✨ New feature (non-breaking change that adds functionality) [ ]
  • 💥 Breaking change (fix or feature that causes existing functionality to change and that could impact other libs) [ ]
  • 🔧 Refactor (no functional changes, code improvement only) [x]
  • 📦 Dependency update [ ]
  • 🔒 Security fix [ ]
  • 📝 Documentation update [x]

Checklist

Code Quality

  • Code is linted and formatted
  • No unnecessary commented-out code or debug logs
  • No hardcoded values (use env variables or config)

Testing

  • Unit tests added / updated

Security & Ops

  • No sensitive data or secrets introduced
  • Logging and error handling are appropriate

Copilot AI review requested due to automatic review settings July 20, 2026 07:22

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

Claude Code Review is paused for this repository. To reconnect it, an admin of this repository's GitHub organization (or the account owner, for personal repositories) who can also manage your Claude organization's Code Review settings needs to re-link GitHub in Code Review settings. This is a one-time step.

Tip: disable this comment in your organization's Code Review settings.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Spike / proof-of-concept to validate that unified-plugin-core’s key contracts can be implemented cleanly on top of Sylius (Workflow state machine, PSR-6 cache, Sylius gateway config, Doctrine persistence), backed by unit + kernel-level integration coverage.

Changes:

  • Add src/Spike/ skeleton implementations for 4 UPC contracts plus a normalized PayplugOperation Doctrine entity.
  • Add PHPUnit unit tests for each spike class and a kernel-booted SpikeIntegrationTest.
  • Adjust test/kernel wiring (phpunit.xml.dist) and add local Composer wiring to pull unified-plugin-core.

1. What's Good

  • All new PHP files are strict_types=1 and the spike classes are clearly labeled as non-production.
  • Good defensive handling around PSR-6’s mixed return type (SyliusTokenCache::get()).
  • Workflow transition application is correctly guarded with can() and includes a flush (and the unit tests explicitly assert flush behavior).

2. Summary table

Dimension Rating
Security Fine
Correctness Medium (integration test + schema setup will fail by default)
Performance Fine
Maintainability Medium (spike wiring bleeds into non-test env; Composer path repo)
Operational implications High (composer install / CI and test suite stability)

3. Closing one-liner

Before merge, the spike needs to be made CI-safe (Composer dependency strategy + integration test/schema setup) and the Doctrine mapping prepend must not affect non-test environments.


4. Individual findings (one section per issue)

Heading: Operational implications — High
Subtitle (bold): Composer path repository makes installs non-reproducible (composer.json:40-68)
Code block:

+        "payplug/unified-plugin-core": "@dev",
...
+    "repositories": [
+        {
+            "type": "path",
+            "url": "../../unified-plugin-core",
+            "options": { "symlink": true }
+        }
+    ],

Explanation paragraph: A path repository outside the repo will fail on CI and for contributors who don’t have that sibling directory, so composer install becomes non-deterministic and the plugin is effectively non-installable by default.
Fix: Use a CI-friendly dependency source (tagged release or VCS URL) or keep the spike dependency local-only (documented), rather than committing a relative path repo outside this repository.

Heading: Operational implications — Medium
Subtitle (bold): Spike Doctrine mapping is registered in all environments (src/DependencyInjection/PayPlugSyliusPayPlugExtension.php:31-37)
Code block:

 public function prepend(ContainerBuilder $container): void
 {
     $this->prependTwigExtension($container);
     $this->prependDoctrineMigrations($container);
     $this->prependMonologExtension($container);
+    $this->prependSpikeDoctrineMapping($container);
 }

Explanation paragraph: This unconditionally registers the spike entity mapping in production/dev too, which can affect Doctrine metadata and schema expectations outside tests, despite the spike being explicitly “not shipped code.”
Fix: Only prepend the spike mapping in the test environment (or behind an explicit opt-in flag).

Heading: Correctness — Medium
Subtitle (bold): Integration test runs by default but requires manual DB + table setup (tests/PHPUnit/Spike/SpikeIntegrationTest.php:34-38)
Code block:

 protected function setUp(): void
 {
     self::bootKernel();
     $this->entityManager = self::getContainer()->get(EntityManagerInterface::class);
 }

Explanation paragraph: phpunit.xml.dist includes tests/PHPUnit, so this kernel test will run in the default suite; however it depends on fixtures + a manually created payplug_operation table (no migration included). That will break fresh CI runs and local runs unless the user has performed extra manual steps.
Fix: Make the integration test opt-in (env var) and/or create/update the PayplugOperation schema in setUp() (e.g., Doctrine SchemaTool) so the test suite is self-contained.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/Spike/SyliusOrderStateMutator.php Spike adapter mapping UPC outcomes to Sylius payment workflow transitions (with flush).
src/Spike/SyliusTokenCache.php Spike PSR-6 implementation of UPC token cache contract.
src/Spike/SyliusConfigurationRepository.php Spike adapter reading/writing Sylius gateway config with live/test scoping and non-leaky error messages.
src/Spike/SyliusPaymentRepository.php Spike Doctrine-backed persistence for operation data + idempotency flag.
src/Spike/Entity/PayplugOperation.php New normalized entity representing operation data, separate from Payment::details.
tests/PHPUnit/Spike/*.php Unit coverage for spike adapters and a kernel integration test.
src/DependencyInjection/PayPlugSyliusPayPlugExtension.php Adds Doctrine mapping prepend for spike entity directory (currently unconditional).
phpunit.xml.dist Fixes kernel boot config by using KERNEL_CLASS.
composer.json Adds unified-plugin-core dev dependency plus a local path repository to resolve it.

Comment thread tests/PHPUnit/Spike/SpikeIntegrationTest.php
Comment thread src/DependencyInjection/PayPlugSyliusPayPlugExtension.php
Comment thread composer.json Outdated
Copilot AI review requested due to automatic review settings July 20, 2026 10:09

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 5 comments.

Comments suppressed due to low confidence (1)

composer.json:40

  • Depending on a moving dev branch ("dev-develop") makes installs non-reproducible and can break CI unexpectedly when that branch changes. If this must stay, consider pinning to a specific commit hash or tagged release (or using a local path repository during spike work).
        "payplug/unified-plugin-core": "dev-develop",

Comment thread migrations/Version20260720100000.php
Comment thread migrations/Version20260720100000.php
Comment thread composer.json
Comment thread src/Spike/VALIDATION.md Outdated
Comment thread src/Spike/VALIDATION.md Outdated
Copilot AI review requested due to automatic review settings July 20, 2026 13:57

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

composer.json:45

  • payplug/unified-plugin-core is required as dev-develop, which is a moving target and can break this spike (and its test expectations) unpredictably as the branch evolves. To keep the spike reproducible for reviewers/CI reruns, pin this to a specific commit hash (or a tagged/pre-release version) rather than a live branch name.
        "payplug/unified-plugin-core": "dev-develop",
        "php-parallel-lint/php-parallel-lint": "1.4.0",
        "phpmd/phpmd": "^2.15.0",
        "phpro/grumphp": "^2.12",
        "phpstan/extension-installer": "1.4.3",
        "phpstan/phpstan": "2.0.4",

@adumont-payplug
adumont-payplug marked this pull request as draft July 20, 2026 14:43
jhoaraupp and others added 10 commits July 21, 2026 08:49
NotifyPaymentRequestHandler (task 5) will reference UPC contract types
directly; leaving the package require-dev-only would fatal-error the
container the moment a merchant's production composer install --no-dev
tries to autowire the new dependency.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The spec doc was staged from an earlier, unrelated session step and got
swept into the unified-plugin-core dependency commit by mistake. Pulling
it back out of git tracking (still present on disk, uncommitted) — it
should only be committed when explicitly requested.
Moves the IOrderStateMutator skeleton out of src/Spike/ into a real
production namespace, ahead of wiring it into NotifyPaymentRequestHandler.
Moves the ITokenCache skeleton out of src/Spike/ into a real production
namespace. Stays validated by a real-infrastructure integration test only
(see SpikeIntegrationTest) — no production call site, since its one real
analog (PayPlugApiClientFactory's OAuth token cache) gates authentication
for every gateway and isn't worth the regression risk of replacing here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…onRepository

Moves the IConfigurationRepository skeleton out of src/Spike/ into a real
production namespace. getPublicKeyId()/getPublicKeyValue() now default to
an empty string instead of throwing, since no production code writes
public_key_id/public_key_value yet (Hosted Fields isn't built) — documented
as friction rather than blocked on.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Additive real call site: runs after the existing PaymentTransitionApplier
transition, guarded by the mutator's own can() check (safe no-op once the
transition's already applied) and wrapped in try/catch so any failure is
logged, never allowed to break webhook notification handling.
…call

Cannot cast mixed to string at (string) $order->getId() — Sylius's
ResourceInterface::getId() returns mixed. Matches the existing
@phpstan-ignore-line convention already used elsewhere in this same file
for getDetails() (mixed return), rather than adding a new baseline entry.
Documents the require-dev blocking friction found and fixed, and the
production wiring decisions for each promoted contract.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Symfony does not auto-alias a singly-implemented interface for plain
autowiring — confirmed by directly booting the container, which failed
to compile with "Cannot autowire service NotifyPaymentRequestHandler:
... references interface IOrderStateMutator but no such service exists."
Since NotifyPaymentRequestHandler is an #[AsMessageHandler] service
instantiated on every webhook, this broke all PayPlug notification
handling. Adds the explicit alias, matching the convention already used
for other interfaces in this file.
…rders

PayplugOrderStateMutator::apply() resolves the order's *last* payment
internally (its contract is keyed by order ID, not payment ID). On an
order with more than one payment (e.g. a failed attempt followed by a
retry), that could be a different payment than the one this specific
webhook is about — skip the additive call rather than risk transitioning
the wrong payment.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants