From 15afc54cd39f747efd7b7150a6622da7b7913f52 Mon Sep 17 00:00:00 2001 From: hschoenenberger Date: Fri, 17 Jul 2026 15:26:44 +0200 Subject: [PATCH 1/2] fix(deprecations): PHP 8.2 ${var} and 8.4 implicit-nullable params (#648) Two families of PHP deprecations reported on PS 9.1.4 / PHP 8.3+: 1. "${var}" string interpolation in segmentio/analytics-php (deprecated PHP 8.2, FATAL in PHP 9.0). The lib is a non-scoped psr0 dependency shipped as-is and regenerated by composer, so it is patched at build time: new Makefile target vendor-patch-segmentio rewrites ${var} into {$var}, wired into the php-scoper chain. 2. Implicitly-nullable parameters (Type $x = null, deprecated PHP 8.4) in our own src/. The issue's suggested ?Type fix is rejected: it breaks the PHP 5.6 floor enforced in CI. Instead we drop the native type hint and keep the @param Type|null docblock (ConfigurationRepository, Adapter\Link, StatusManager). ShopSession::getValidToken is an override, so rather than widening the parameter type (which warns pre-7.2) its signature is aligned with the parent (array $scope = []) using an empty() sentinel; verified no caller passes an explicit scope/audience, so behavior is unchanged. Out of scope: 8.4 implicit-nullable patches for lcobucci/jwt and ramsey/uuid (beyond the 8.3 support boundary; would need a vendor patch mechanism). Co-Authored-By: Claude Opus 4.8 --- Makefile | 10 +++++++++- src/Account/Session/ShopSession.php | 10 +++++----- src/Account/StatusManager.php | 4 ++-- src/Adapter/Link.php | 2 +- src/Repository/ConfigurationRepository.php | 2 +- 5 files changed, 18 insertions(+), 10 deletions(-) diff --git a/Makefile b/Makefile index 180f0496c..797e7fc48 100644 --- a/Makefile +++ b/Makefile @@ -282,8 +282,16 @@ php-scoper-dump-autoload: php-scoper-fix-autoload: php fix-autoload.php +# segmentio/analytics-php is a non-scoped (psr0) lib shipped as-is: rewrite the +# deprecated "${var}" string interpolation (deprecated PHP 8.2, fatal PHP 9.0) +# into "{$var}". Runs on the freshly installed vendor, before bundling. +vendor-patch-segmentio: + @echo "patching segmentio interpolation deprecations (PHP 8.2+/9.0)..." + find ./vendor/segmentio/analytics-php/lib -type f -name '*.php' -exec \ + sed -i -E 's/\$$\{([a-zA-Z_][a-zA-Z0-9_]*)\}/{$$\1}/g' {} \; + #php-scoper: php-scoper-add-prefix php-scoper-update-prefix php-scoper-dump-autoload php-scoper-fix-autoload -php-scoper: php-scoper-add-prefix php-scoper-dump-autoload php-scoper-fix-autoload +php-scoper: php-scoper-add-prefix vendor-patch-segmentio php-scoper-dump-autoload php-scoper-fix-autoload ########## # BUNDLING diff --git a/src/Account/Session/ShopSession.php b/src/Account/Session/ShopSession.php index cd13c635c..1c99e2bd7 100644 --- a/src/Account/Session/ShopSession.php +++ b/src/Account/Session/ShopSession.php @@ -76,22 +76,22 @@ public function __construct( /** * @param bool $forceRefresh * @param bool $throw - * @param array|null $scope - * @param array|null $audience + * @param array $scope + * @param array $audience * * @return Token * * @throws RefreshTokenException */ - public function getValidToken($forceRefresh = false, $throw = true, array $scope = null, array $audience = null) + public function getValidToken($forceRefresh = false, $throw = true, array $scope = [], array $audience = []) { - if ($scope === null) { + if (empty($scope)) { $scope = ($this->getStatusManager()->identityVerified() ? [ 'shop.verified', ] : []); } - if ($audience === null) { + if (empty($audience)) { $audience = [ 'store/' . $this->getStatusManager()->getCloudShopId(), $this->tokenAudience, diff --git a/src/Account/StatusManager.php b/src/Account/StatusManager.php index 436fbde73..5cc0923b8 100644 --- a/src/Account/StatusManager.php +++ b/src/Account/StatusManager.php @@ -188,7 +188,7 @@ public function invalidateCache() * * @return bool */ - public function cacheInvalidated(CachedShopStatus $cachedStatus = null) + public function cacheInvalidated($cachedStatus = null) { try { $cachedStatus = $cachedStatus ?: $this->getCachedStatus(); @@ -206,7 +206,7 @@ public function cacheInvalidated(CachedShopStatus $cachedStatus = null) * * @return bool */ - public function cacheExpired(CachedShopStatus $cachedStatus = null, $cacheTtl = self::CACHE_TTL) + public function cacheExpired($cachedStatus = null, $cacheTtl = self::CACHE_TTL) { try { //$dateUpd = $this->getCacheDateUpd(); diff --git a/src/Adapter/Link.php b/src/Adapter/Link.php index e88625e16..23a3fa021 100644 --- a/src/Adapter/Link.php +++ b/src/Adapter/Link.php @@ -45,7 +45,7 @@ class Link */ public function __construct( ShopContext $shopContext, - \Link $link = null + $link = null ) { if (null === $link) { $link = new \Link(); diff --git a/src/Repository/ConfigurationRepository.php b/src/Repository/ConfigurationRepository.php index a2c45824a..0de71e5af 100644 --- a/src/Repository/ConfigurationRepository.php +++ b/src/Repository/ConfigurationRepository.php @@ -37,7 +37,7 @@ class ConfigurationRepository * * @throws \Exception */ - public function __construct(Configuration $configuration = null) + public function __construct($configuration = null) { $this->configuration = $configuration; } From 01517f58a15778d91d9b0a3ab06fbd7a251fd995 Mon Sep 17 00:00:00 2001 From: hschoenenberger Date: Mon, 20 Jul 2026 12:04:33 +0200 Subject: [PATCH 2/2] test(session): cover getValidToken scope/audience default resolution (#648) Add ShopSession::getValidToken() coverage for the empty() sentinel introduced when aligning the signature on the parent: - empty scope/audience => defaults resolved (shop.verified + store audience) - explicit non-empty scope/audience => forwarded unchanged Addresses the minor review point on PR #649 (no test for the modified restricted-area behaviour). Test-only, no change to src/Account/Session/. Co-Authored-By: Claude Opus 4.8 --- .../Session/ShopSession/GetValidTokenTest.php | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/tests/src/Unit/Account/Session/ShopSession/GetValidTokenTest.php b/tests/src/Unit/Account/Session/ShopSession/GetValidTokenTest.php index 59cf994f1..ca2e73f77 100644 --- a/tests/src/Unit/Account/Session/ShopSession/GetValidTokenTest.php +++ b/tests/src/Unit/Account/Session/ShopSession/GetValidTokenTest.php @@ -222,6 +222,101 @@ public function itShouldRefreshInvalidVerifiedShopToken(Token $invalidAccessToke $this->assertEquals((string) $this->validAccessToken, (string) $this->shopSession->getValidToken()); } + /** + * @test + */ + public function itShouldApplyDefaultScopeAndAudienceWhenNoneProvided() + { + $this->configurationRepository->updateCachedShopStatus(json_encode((new CachedShopStatus([ + 'isValid' => true, + 'updatedAt' => (new \DateTime())->format(\DateTime::ATOM), + 'shopStatus' => new ShopStatus([ + 'cloudShopId' => $this->cloudShopId, + 'isVerified' => true, + ]) + ]))->toArray())); + + list($shopSession, $tokenAudience, $capture) = $this->makeCapturingShopSession(); + + // no scope/audience provided + forced refresh => defaults must be resolved + $shopSession->getValidToken(true); + + $this->assertEquals(['shop.verified'], $capture->scope); + $this->assertEquals([ + 'store/' . $this->cloudShopId, + $tokenAudience, + ], $capture->audience); + + $shopSession->cleanup(); + } + + /** + * @test + */ + public function itShouldForwardExplicitScopeAndAudienceUnchanged() + { + $this->configurationRepository->updateCachedShopStatus(json_encode((new CachedShopStatus([ + 'isValid' => true, + 'updatedAt' => (new \DateTime())->format(\DateTime::ATOM), + 'shopStatus' => new ShopStatus([ + 'cloudShopId' => $this->cloudShopId, + 'isVerified' => true, + ]) + ]))->toArray())); + + list($shopSession, $tokenAudience, $capture) = $this->makeCapturingShopSession(); + + $scope = ['custom.scope']; + $audience = ['store/custom-audience']; + + // explicit non-empty scope/audience must be forwarded as-is (no default override) + $shopSession->getValidToken(true, true, $scope, $audience); + + $this->assertEquals($scope, $capture->scope); + $this->assertEquals($audience, $capture->audience); + + $shopSession->cleanup(); + } + + /** + * Builds a ShopSession whose OAuth2Service captures the scope/audience + * actually forwarded to getAccessTokenByClientCredentials(). + * + * @return array [ShopSession, string $tokenAudience, object $capture] + */ + private function makeCapturingShopSession() + { + $capture = new \stdClass(); + $capture->scope = null; + $capture->audience = null; + + $accessToken = new AccessToken([ + 'access_token' => (string) $this->validAccessToken, + ]); + + $oAuth2Service = $this->createMock(OAuth2Service::class); + $oAuth2Service->method('getOAuth2Client') + ->willReturn($this->oauth2Client); + $oAuth2Service->method('getAccessTokenByClientCredentials') + ->willReturnCallback(function ($scope, $audience) use ($capture, $accessToken) { + $capture->scope = $scope; + $capture->audience = $audience; + + return $accessToken; + }); + + $tokenAudience = $this->module->getParameter('ps_accounts.accounts_api_url'); + + $shopSession = new ShopSession( + $this->configurationRepository, + $oAuth2Service, + $tokenAudience + ); + $shopSession->cleanup(); + + return [$shopSession, $tokenAudience, $capture]; + } + /** * @test */