diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..632d155 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,15 @@ +# Changelog + +## [2.3.0] - 2026-05-11 + +### Added +- PHP 8.4 and PHP 8.5 compatibility. +- Explicit `php` constraint in `composer.json` (`~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0`). + +### Changed +- Added `declare(strict_types=1)` to all PHP classes. +- Added explicit return types on all methods (including `Setup\Patch\Data\UpdateSpeculationRulesConfigPathPatch::apply()`, `getAliases()` and `getDependencies()`). +- Replaced `strpos($s, $needle) === 0` with `str_starts_with()` for clarity. +- Typed nullable parameters explicitly (e.g. `int|string|null $store`) to avoid the PHP 8.4 implicit nullable deprecation. +- Made constructor-promoted properties and helper methods `protected` instead of `private` for easier extension by downstream modules. +- Minor refactor: `unset` of loop reference variable after foreach-by-reference. diff --git a/Plugin/Framework/App/Response/Http.php b/Plugin/Framework/App/Response/Http.php index 8515eb5..10acd93 100644 --- a/Plugin/Framework/App/Response/Http.php +++ b/Plugin/Framework/App/Response/Http.php @@ -4,6 +4,7 @@ use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\App\Response\Http as ResponseHttp; use Magento\PageCache\Model\Config; use Magento\Store\Model\ScopeInterface; @@ -12,34 +13,25 @@ */ class Http { - /** @var string */ public const XML_PATH_ENABLE = 'system/bfcache/general/enable'; - - /** @var string */ public const XML_PATH_EXCLUDE_URL_PATTERNS = 'system/bfcache/scope/exclude_url_patterns'; - /** @var bool */ - private $isRequestCacheable = false; + protected bool $isRequestCacheable = false; - /** - * @param Config $config - * @param ScopeConfigInterface $scopeConfig - * @param HttpRequest $request - */ public function __construct( - private Config $config, - private ScopeConfigInterface $scopeConfig, - private HttpRequest $request + protected Config $config, + protected ScopeConfigInterface $scopeConfig, + protected HttpRequest $request ) { } /** * Intercept before setting no-cache headers to determine if request is cacheable * - * @param \Magento\Framework\App\Response\Http $subject + * @param ResponseHttp $subject * @return void */ - public function beforeSetNoCacheHeaders(\Magento\Framework\App\Response\Http $subject): void + public function beforeSetNoCacheHeaders(ResponseHttp $subject): void { if ($this->config->getType() !== Config::BUILT_IN || !$this->isEnabled()) { return; @@ -61,11 +53,11 @@ public function beforeSetNoCacheHeaders(\Magento\Framework\App\Response\Http $su /** * Update cache headers after setting no-cache headers * - * @param \Magento\Framework\App\Response\Http $subject + * @param ResponseHttp $subject * @param mixed $result * @return mixed */ - public function afterSetNoCacheHeaders(\Magento\Framework\App\Response\Http $subject, $result) + public function afterSetNoCacheHeaders(ResponseHttp $subject, mixed $result): mixed { if ($this->config->getType() !== Config::BUILT_IN || !$this->isEnabled()) { return $result; @@ -77,7 +69,6 @@ public function afterSetNoCacheHeaders(\Magento\Framework\App\Response\Http $sub } if ($this->isRequestCacheable === true) { - $cacheControlHeader = $subject->getHeader('Cache-Control'); $cacheControlHeader->removeDirective('no-store'); } $this->isRequestCacheable = false; @@ -91,7 +82,7 @@ public function afterSetNoCacheHeaders(\Magento\Framework\App\Response\Http $sub * @param string $cacheControl * @return bool */ - private function isRequestCacheable(string $cacheControl): bool + protected function isRequestCacheable(string $cacheControl): bool { // FPC hits will not have public or private cache control directives -- already processed if (!str_contains($cacheControl, 'public') @@ -101,7 +92,7 @@ private function isRequestCacheable(string $cacheControl): bool } // FPC misses will be cacheable if they have a public directive - return (bool) preg_match('/public.*s-maxage=(\d+)/', $cacheControl); + return (bool)preg_match('/public.*s-maxage=(\d+)/', $cacheControl); } /** @@ -110,11 +101,11 @@ private function isRequestCacheable(string $cacheControl): bool * @param string $requestURI * @return bool */ - private function isRequestInExcludePatterns(string $requestURI): bool + protected function isRequestInExcludePatterns(string $requestURI): bool { $patterns = $this->getConfig(self::XML_PATH_EXCLUDE_URL_PATTERNS); - if (empty($patterns)) { + if ($patterns === '') { return false; } @@ -133,9 +124,9 @@ private function isRequestInExcludePatterns(string $requestURI): bool * @param string $patterns * @return array */ - private function parseExcludePatterns(string $patterns): array + protected function parseExcludePatterns(string $patterns): array { - return array_filter(array_map('trim', explode("\n", $patterns))); + return array_values(array_filter(array_map('trim', explode("\n", $patterns)))); } /** @@ -143,7 +134,7 @@ private function parseExcludePatterns(string $patterns): array * * @return bool */ - private function isEnabled(): bool + protected function isEnabled(): bool { return $this->scopeConfig->isSetFlag( self::XML_PATH_ENABLE, @@ -158,7 +149,7 @@ private function isEnabled(): bool * @param int|string|null $store * @return string */ - private function getConfig(string $configPath, $store = null): string + protected function getConfig(string $configPath, int|string|null $store = null): string { return (string)$this->scopeConfig->getValue( $configPath, diff --git a/Setup/Patch/Data/UpdateSpeculationRulesConfigPathPatch.php b/Setup/Patch/Data/UpdateSpeculationRulesConfigPathPatch.php index f1bf0a7..e598d13 100644 --- a/Setup/Patch/Data/UpdateSpeculationRulesConfigPathPatch.php +++ b/Setup/Patch/Data/UpdateSpeculationRulesConfigPathPatch.php @@ -1,4 +1,4 @@ -moduleDataSetup = $moduleDataSetup; + protected ModuleDataSetupInterface $moduleDataSetup + ) { } /** * Do Upgrade. * - * @return void + * @return self */ - public function apply() + public function apply(): self { - $this->moduleDataSetup->getConnection()->startSetup(); + $connection = $this->moduleDataSetup->getConnection(); + $connection->startSetup(); // Change the core_config_data path for any 'dev/speculation_rules/*' values to 'system/speculation_rules/*' - $connection = $this->moduleDataSetup->getConnection(); $connection->update( $this->moduleDataSetup->getTable('core_config_data'), [ @@ -47,7 +37,9 @@ public function apply() ['path LIKE ?' => 'dev/speculation_rules/%'] ); - $this->moduleDataSetup->getConnection()->endSetup(); + $connection->endSetup(); + + return $this; } /** @@ -55,7 +47,7 @@ public function apply() * * @return string[] */ - public function getAliases() + public function getAliases(): array { return []; } @@ -72,7 +64,7 @@ public function getAliases() * * @return string[] */ - public static function getDependencies() + public static function getDependencies(): array { return []; } diff --git a/Test/Unit/ViewModel/SpeculationRulesTest.php b/Test/Unit/ViewModel/SpeculationRulesTest.php index e6d66b8..f03fb45 100644 --- a/Test/Unit/ViewModel/SpeculationRulesTest.php +++ b/Test/Unit/ViewModel/SpeculationRulesTest.php @@ -9,6 +9,7 @@ use Magento\Framework\View\Element\Block\ArgumentInterface; use Magento\Store\Model\ScopeInterface; use MageOS\ThemeOptimization\ViewModel\SpeculationRules; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; class SpeculationRulesTest extends TestCase @@ -67,10 +68,8 @@ public function testImplementsArgumentInterface(): void // Test Category #2: Configuration Tests - isEnabled() method - /** - * @dataProvider enabledDataProvider - */ - public function testIsEnabled($configValue, bool $expected): void + #[DataProvider('enabledDataProvider')] + public function testIsEnabled(mixed $configValue, bool $expected): void { $this->scopeConfigMock->expects($this->once()) ->method('getValue') @@ -81,7 +80,7 @@ public function testIsEnabled($configValue, bool $expected): void $this->assertEquals($expected, $result); } - public function enabledDataProvider(): array + public static function enabledDataProvider(): array { return [ 'string true' => ['1', true], @@ -98,10 +97,8 @@ public function enabledDataProvider(): array // Test Category #3: Eagerness Mode Tests - /** - * @dataProvider eagernessDataProvider - */ - public function testGetEagerness($configValue, string $expected): void + #[DataProvider('eagernessDataProvider')] + public function testGetEagerness(mixed $configValue, string $expected): void { $this->scopeConfigMock->expects($this->once()) ->method('getValue') @@ -112,7 +109,7 @@ public function testGetEagerness($configValue, string $expected): void $this->assertEquals($expected, $result); } - public function eagernessDataProvider(): array + public static function eagernessDataProvider(): array { return [ 'conservative' => ['conservative', 'conservative'], diff --git a/ViewModel/BfCache.php b/ViewModel/BfCache.php index 53f1a55..380945d 100644 --- a/ViewModel/BfCache.php +++ b/ViewModel/BfCache.php @@ -15,21 +15,15 @@ */ class BfCache implements ArgumentInterface { - /** @var string */ - private const XML_PATH_ENABLE_USER_INTERACTION_RELOAD_MINICART = + protected const XML_PATH_ENABLE_USER_INTERACTION_RELOAD_MINICART = 'system/bfcache/general/enable_user_interaction_reload_minicart'; - - /** @var string */ - private const XML_PATH_AUTO_CLOSE_MENU_MOBILE = + + protected const XML_PATH_AUTO_CLOSE_MENU_MOBILE = 'system/bfcache/general/auto_close_menu_mobile'; - /** - * @param ScopeConfigInterface $scopeConfig - * @param Context $httpContext - */ public function __construct( - private ScopeConfigInterface $scopeConfig, - private Context $httpContext + protected ScopeConfigInterface $scopeConfig, + protected Context $httpContext ) { } @@ -66,6 +60,6 @@ public function autoCloseMenuMobile(): bool */ public function isCustomerLoggedIn(): bool { - return (bool) $this->httpContext->getValue(CustomerContext::CONTEXT_AUTH); + return (bool)$this->httpContext->getValue(CustomerContext::CONTEXT_AUTH); } } diff --git a/ViewModel/SpeculationRules.php b/ViewModel/SpeculationRules.php index 29567da..ecaec46 100644 --- a/ViewModel/SpeculationRules.php +++ b/ViewModel/SpeculationRules.php @@ -1,36 +1,43 @@ -getConfigValue('enable'); } + /** + * @return string + */ public function getMode(): string { $mode = $this->getConfigValue('mode'); @@ -42,6 +49,9 @@ public function getMode(): string return self::MODE_PREFETCH; } + /** + * @return string + */ public function getEagerness(): string { $eagerness = $this->getConfigValue('eagerness'); @@ -50,7 +60,7 @@ public function getEagerness(): string return $eagerness; } - return 'moderate'; + return self::EAGERNESS_DEFAULT; } /** @@ -94,6 +104,9 @@ public function getPrerenderingScript(): string JS; } + /** + * @return array + */ public function getSpeculationRules(): array { // Possible future development: add support for multiple modes and rulesets at once. @@ -108,13 +121,18 @@ public function getSpeculationRules(): array ]; } + /** + * @return string + * @throws InvalidArgumentException + */ public function getSpeculationRulesJson(): string { - $rules = $this->getSpeculationRules(); - - return $this->serializer->serialize($rules); + return $this->serializer->serialize($this->getSpeculationRules()); } + /** + * @return array + */ protected function buildRules(): array { // Include all URLs by default @@ -144,14 +162,23 @@ protected function buildRules(): array return $rules; } + /** + * @param string $key + * @return string|null + */ protected function getConfigValue(string $key): ?string { - return $this->scopeConfig->getValue( + $value = $this->scopeConfig->getValue( self::CONFIG_PATH . $key, ScopeInterface::SCOPE_STORE ); + + return $value === null ? null : (string)$value; } + /** + * @return array + */ public function getExcludedPaths(): array { $paths = explode("\n", (string)$this->getConfigValue('exclude_paths')); @@ -159,6 +186,7 @@ public function getExcludedPaths(): array foreach ($paths as &$pattern) { $pattern = trim(trim($pattern), '/'); } + unset($pattern); $paths = array_filter($paths); if (empty($paths)) { @@ -168,6 +196,9 @@ public function getExcludedPaths(): array return ['not' => ['href_matches' => '/*(' . implode('|', $paths) . ')/*']]; } + /** + * @return array + */ public function getExcludedExtensions(): array { $rules = []; @@ -181,6 +212,9 @@ public function getExcludedExtensions(): array return $rules; } + /** + * @return array + */ public function getExcludedSelectors(): array { $rules = []; @@ -199,11 +233,11 @@ public function getExcludedSelectors(): array * * @return bool */ - private function isHyva(): bool + protected function isHyva(): bool { $theme = $this->viewDesign->getDesignTheme(); while ($theme) { - if (strpos($theme->getCode(), 'Hyva/') === 0) { + if (str_starts_with((string)$theme->getCode(), 'Hyva/')) { return true; } $theme = $theme->getParentTheme(); diff --git a/ViewModel/ViewTransitions.php b/ViewModel/ViewTransitions.php index fd65974..ae3a3c4 100644 --- a/ViewModel/ViewTransitions.php +++ b/ViewModel/ViewTransitions.php @@ -1,4 +1,4 @@ -scopeConfig->getValue( + $value = $this->scopeConfig->getValue( self::CONFIG_PATH . $key, ScopeInterface::SCOPE_STORE ); + + return $value === null ? null : (string)$value; } + /** + * @return bool + */ public function isEnabled(): bool { return (bool)$this->getConfigValue('enable'); } + /** + * @return bool + */ public function isEnabledForBfcache(): bool { return (bool)$this->getConfigValue('enable_for_bfcache'); diff --git a/composer.json b/composer.json index 1446bde..57a5a55 100644 --- a/composer.json +++ b/composer.json @@ -3,6 +3,7 @@ "description": "Page transitions and speculative loading rules for Magento", "type": "magento2-module", "require": { + "php": "~8.1.0||~8.2.0||~8.3.0||~8.4.0||~8.5.0", "magento/framework": "^103.0", "magento/module-store": "*" }, diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 34ac61a..818daf1 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,15 +1,13 @@ - - - - Test/Unit - - - - - - - + + + + Test/Unit + + + + + + +