From 2298fb2f54b7b008d2b6dae7884f6c1415a8a803 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Kr=C3=A4mer?= Date: Mon, 6 Oct 2025 23:52:40 +0200 Subject: [PATCH 01/17] Initial Cache Experiment --- .gitignore | 1 + composer.json | 1 + composer.lock | 1709 +++++++++++------ config.yml | 7 +- src/Application.php | 11 +- src/Business/Cache/CacheItem.php | 72 + .../Cache/Exception/CacheException.php | 14 + src/Business/Cache/FileCacheService.php | 239 +++ .../Cognitive/CognitiveMetricsCollector.php | 139 +- src/Business/MetricsFacade.php | 30 + src/Command/CognitiveMetricsCommand.php | 43 + src/Config/CacheConfig.php | 18 + src/Config/CognitiveConfig.php | 1 + src/Config/ConfigFactory.php | 12 +- src/Config/ConfigLoader.php | 13 + 15 files changed, 1701 insertions(+), 609 deletions(-) create mode 100644 src/Business/Cache/CacheItem.php create mode 100644 src/Business/Cache/Exception/CacheException.php create mode 100644 src/Business/Cache/FileCacheService.php create mode 100644 src/Config/CacheConfig.php diff --git a/.gitignore b/.gitignore index de8c042..15a5510 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ !/bin/phpcca.bat /.phive/ /.phpunit.cache/ +/.phpcca.cache/ /tmp/ /tools/ /benchmarks/storage/ diff --git a/composer.json b/composer.json index 694266e..a6aaad8 100644 --- a/composer.json +++ b/composer.json @@ -5,6 +5,7 @@ "require": { "php": "^8.1", "nikic/php-parser": "^5.1", + "psr/cache": "^3.0", "symfony/console": "^6.0||^7.0", "symfony/config": "^6.0||^7.0", "symfony/yaml": "^6.0||^7.0", diff --git a/composer.lock b/composer.lock index 5de4bb4..d3761ab 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "822640c462f0d98b33c92e0f7b9231ad", + "content-hash": "a98c505b11eb0ea3c5a774cbe975dbca", "packages": [ { "name": "nikic/php-parser", - "version": "v5.1.0", + "version": "v5.6.1", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "683130c2ff8c2739f4822ff7ac5c873ec529abd1" + "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/683130c2ff8c2739f4822ff7ac5c873ec529abd1", - "reference": "683130c2ff8c2739f4822ff7ac5c873ec529abd1", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2", + "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2", "shasum": "" }, "require": { @@ -36,7 +36,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.x-dev" } }, "autoload": { @@ -60,9 +60,106 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.1.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.1" }, - "time": "2024-07-01T20:03:41+00:00" + "time": "2025-08-13T20:13:15+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", @@ -119,30 +216,30 @@ }, { "name": "psr/log", - "version": "1.1.4", + "version": "3.0.2", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "d49695b909c3b7628b6289db5479a1c204601f11" + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", - "reference": "d49695b909c3b7628b6289db5479a1c204601f11", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": ">=8.0.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.1.x-dev" + "dev-master": "3.x-dev" } }, "autoload": { "psr-4": { - "Psr\\Log\\": "Psr/Log/" + "Psr\\Log\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -163,43 +260,114 @@ "psr-3" ], "support": { - "source": "https://github.com/php-fig/log/tree/1.1.4" + "source": "https://github.com/php-fig/log/tree/3.0.2" }, - "time": "2021-05-03T11:20:27+00:00" + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "symfony/clock", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/clock.git", + "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/clock/zipball/b81435fbd6648ea425d1ee96a2d8e68f4ceacd24", + "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "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/v7.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" }, { "name": "symfony/config", - "version": "v6.0.0", + "version": "v7.3.4", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "df4871981fd37f953c117b55feac03462be5a2d6" + "reference": "8a09223170046d2cfda3d2e11af01df2c641e961" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/df4871981fd37f953c117b55feac03462be5a2d6", - "reference": "df4871981fd37f953c117b55feac03462be5a2d6", + "url": "https://api.github.com/repos/symfony/config/zipball/8a09223170046d2cfda3d2e11af01df2c641e961", + "reference": "8a09223170046d2cfda3d2e11af01df2c641e961", "shasum": "" }, "require": { - "php": ">=8.0.2", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/filesystem": "^5.4|^6.0", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-php81": "^1.22" + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/filesystem": "^7.1", + "symfony/polyfill-ctype": "~1.8" }, "conflict": { - "symfony/finder": "<4.4" + "symfony/finder": "<6.4", + "symfony/service-contracts": "<2.5" }, "require-dev": { - "symfony/event-dispatcher": "^5.4|^6.0", - "symfony/finder": "^5.4|^6.0", - "symfony/messenger": "^5.4|^6.0", - "symfony/service-contracts": "^1.1|^2|^3", - "symfony/yaml": "^5.4|^6.0" - }, - "suggest": { - "symfony/yaml": "To use the yaml reference dumper" + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -227,7 +395,7 @@ "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.0.0" + "source": "https://github.com/symfony/config/tree/v7.3.4" }, "funding": [ { @@ -238,56 +406,60 @@ "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": "2021-11-23T19:05:29+00:00" + "time": "2025-09-22T12:46:16+00:00" }, { "name": "symfony/console", - "version": "v6.4.25", + "version": "v7.3.4", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "273fd29ff30ba0a88ca5fb83f7cf1ab69306adae" + "reference": "2b9c5fafbac0399a20a2e82429e2bd735dcfb7db" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/273fd29ff30ba0a88ca5fb83f7cf1ab69306adae", - "reference": "273fd29ff30ba0a88ca5fb83f7cf1ab69306adae", + "url": "https://api.github.com/repos/symfony/console/zipball/2b9c5fafbac0399a20a2e82429e2bd735dcfb7db", + "reference": "2b9c5fafbac0399a20a2e82429e2bd735dcfb7db", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "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" + "symfony/string": "^7.2" }, "conflict": { - "symfony/dependency-injection": "<5.4", - "symfony/dotenv": "<5.4", - "symfony/event-dispatcher": "<5.4", - "symfony/lock": "<5.4", - "symfony/process": "<5.4" + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.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/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^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" + "symfony/lock": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -321,7 +493,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.4.25" + "source": "https://github.com/symfony/console/tree/v7.3.4" }, "funding": [ { @@ -341,51 +513,43 @@ "type": "tidelift" } ], - "time": "2025-08-22T10:21:53+00:00" + "time": "2025-09-22T15:31:00+00:00" }, { "name": "symfony/dependency-injection", - "version": "v6.0.0", + "version": "v7.3.4", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "4dae086ee9bda1e2d0fdbc19a4a30ea49b0e3c7c" + "reference": "82119812ab0bf3425c1234d413efd1b19bb92ae4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/4dae086ee9bda1e2d0fdbc19a4a30ea49b0e3c7c", - "reference": "4dae086ee9bda1e2d0fdbc19a4a30ea49b0e3c7c", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/82119812ab0bf3425c1234d413efd1b19bb92ae4", + "reference": "82119812ab0bf3425c1234d413efd1b19bb92ae4", "shasum": "" }, "require": { - "php": ">=8.0.2", + "php": ">=8.2", "psr/container": "^1.1|^2.0", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/polyfill-php81": "^1.22", - "symfony/service-contracts": "^1.1.6|^2.0|^3.0" + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/service-contracts": "^3.5", + "symfony/var-exporter": "^6.4.20|^7.2.5" }, "conflict": { "ext-psr": "<1.1|>=2", - "symfony/config": "<5.4", - "symfony/finder": "<5.4", - "symfony/proxy-manager-bridge": "<5.4", - "symfony/yaml": "<5.4" + "symfony/config": "<6.4", + "symfony/finder": "<6.4", + "symfony/yaml": "<6.4" }, "provide": { - "psr/container-implementation": "1.1|2.0|3.0", + "psr/container-implementation": "1.1|2.0", "symfony/service-implementation": "1.1|2.0|3.0" }, "require-dev": { - "symfony/config": "^5.4|^6.0", - "symfony/expression-language": "^5.4|^6.0", - "symfony/yaml": "^5.4|^6.0" - }, - "suggest": { - "symfony/config": "", - "symfony/expression-language": "For using expressions in service container configuration", - "symfony/finder": "For using double-star glob patterns or when GLOB_BRACE portability is required", - "symfony/proxy-manager-bridge": "Generate service proxies to lazy load them", - "symfony/yaml": "" + "symfony/config": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/yaml": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -413,7 +577,7 @@ "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.0.0" + "source": "https://github.com/symfony/dependency-injection/tree/v7.3.4" }, "funding": [ { @@ -424,12 +588,16 @@ "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": "2021-11-29T15:32:57+00:00" + "time": "2025-09-11T10:12:26+00:00" }, { "name": "symfony/deprecation-contracts", @@ -500,25 +668,25 @@ }, { "name": "symfony/filesystem", - "version": "v6.4.24", + "version": "v7.3.2", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "75ae2edb7cdcc0c53766c30b0a2512b8df574bd8" + "reference": "edcbb768a186b5c3f25d0643159a787d3e63b7fd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/75ae2edb7cdcc0c53766c30b0a2512b8df574bd8", - "reference": "75ae2edb7cdcc0c53766c30b0a2512b8df574bd8", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/edcbb768a186b5c3f25d0643159a787d3e63b7fd", + "reference": "edcbb768a186b5c3f25d0643159a787d3e63b7fd", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-mbstring": "~1.8" }, "require-dev": { - "symfony/process": "^5.4|^6.4|^7.0" + "symfony/process": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -546,7 +714,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v6.4.24" + "source": "https://github.com/symfony/filesystem/tree/v7.3.2" }, "funding": [ { @@ -566,48 +734,52 @@ "type": "tidelift" } ], - "time": "2025-07-10T08:14:14+00:00" + "time": "2025-07-07T08:17:47+00:00" }, { "name": "symfony/messenger", - "version": "v6.0.0", + "version": "v7.3.3", "source": { "type": "git", "url": "https://github.com/symfony/messenger.git", - "reference": "3553a51e7309e76f77cbd8bb949c67d18a2291ac" + "reference": "d9e04339404ba2dcd04c24172125516dc0e06c35" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/messenger/zipball/3553a51e7309e76f77cbd8bb949c67d18a2291ac", - "reference": "3553a51e7309e76f77cbd8bb949c67d18a2291ac", + "url": "https://api.github.com/repos/symfony/messenger/zipball/d9e04339404ba2dcd04c24172125516dc0e06c35", + "reference": "d9e04339404ba2dcd04c24172125516dc0e06c35", "shasum": "" }, "require": { - "php": ">=8.0.2", - "psr/log": "^1|^2|^3" + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/clock": "^6.4|^7.0", + "symfony/deprecation-contracts": "^2.5|^3" }, "conflict": { - "symfony/event-dispatcher": "<5.4", - "symfony/framework-bundle": "<5.4", - "symfony/http-kernel": "<5.4", - "symfony/serializer": "<5.4" + "symfony/console": "<7.2", + "symfony/event-dispatcher": "<6.4", + "symfony/event-dispatcher-contracts": "<2.5", + "symfony/framework-bundle": "<6.4", + "symfony/http-kernel": "<6.4", + "symfony/lock": "<6.4", + "symfony/serializer": "<6.4" }, "require-dev": { "psr/cache": "^1.0|^2.0|^3.0", - "symfony/console": "^5.4|^6.0", - "symfony/dependency-injection": "^5.4|^6.0", - "symfony/event-dispatcher": "^5.4|^6.0", - "symfony/http-kernel": "^5.4|^6.0", - "symfony/process": "^5.4|^6.0", - "symfony/property-access": "^5.4|^6.0", - "symfony/routing": "^5.4|^6.0", - "symfony/serializer": "^5.4|^6.0", - "symfony/service-contracts": "^1.1|^2|^3", - "symfony/stopwatch": "^5.4|^6.0", - "symfony/validator": "^5.4|^6.0" - }, - "suggest": { - "enqueue/messenger-adapter": "For using the php-enqueue library as a transport." + "symfony/console": "^7.2", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/property-access": "^6.4|^7.0", + "symfony/rate-limiter": "^6.4|^7.0", + "symfony/routing": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/validator": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -635,7 +807,7 @@ "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.0.0" + "source": "https://github.com/symfony/messenger/tree/v7.3.3" }, "funding": [ { @@ -646,12 +818,16 @@ "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": "2021-11-26T13:06:04+00:00" + "time": "2025-08-13T11:49:31+00:00" }, { "name": "symfony/polyfill-ctype", @@ -989,30 +1165,27 @@ "time": "2024-12-23T08:48:59+00:00" }, { - "name": "symfony/polyfill-php81", - "version": "v1.22.0", + "name": "symfony/polyfill-php83", + "version": "v1.33.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php81.git", - "reference": "00dedc6d362a1b863dda3f8243516da9fdfbe657" + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/00dedc6d362a1b863dda3f8243516da9fdfbe657", - "reference": "00dedc6d362a1b863dda3f8243516da9fdfbe657", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "type": "library", "extra": { "thanks": { "url": "https://github.com/symfony/polyfill", "name": "symfony/polyfill" - }, - "branch-alias": { - "dev-main": "1.22-dev" } }, "autoload": { @@ -1020,8 +1193,11 @@ "bootstrap.php" ], "psr-4": { - "Symfony\\Polyfill\\Php81\\": "" - } + "Symfony\\Polyfill\\Php83\\": "" + }, + "classmap": [ + "Resources/stubs" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1037,7 +1213,7 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ "compatibility", @@ -1046,7 +1222,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.22.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0" }, "funding": [ { @@ -1057,12 +1233,16 @@ "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": "2021-01-07T16:49:33+00:00" + "time": "2025-07-08T02:45:35+00:00" }, { "name": "symfony/service-contracts", @@ -1149,16 +1329,16 @@ }, { "name": "symfony/string", - "version": "v7.3.3", + "version": "v7.3.4", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "17a426cce5fd1f0901fefa9b2a490d0038fd3c9c" + "reference": "f96476035142921000338bad71e5247fbc138872" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/17a426cce5fd1f0901fefa9b2a490d0038fd3c9c", - "reference": "17a426cce5fd1f0901fefa9b2a490d0038fd3c9c", + "url": "https://api.github.com/repos/symfony/string/zipball/f96476035142921000338bad71e5247fbc138872", + "reference": "f96476035142921000338bad71e5247fbc138872", "shasum": "" }, "require": { @@ -1173,7 +1353,6 @@ }, "require-dev": { "symfony/emoji": "^7.1", - "symfony/error-handler": "^6.4|^7.0", "symfony/http-client": "^6.4|^7.0", "symfony/intl": "^6.4|^7.0", "symfony/translation-contracts": "^2.5|^3.0", @@ -1216,7 +1395,88 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.3.3" + "source": "https://github.com/symfony/string/tree/v7.3.4" + }, + "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-09-11T14:36:48+00:00" + }, + { + "name": "symfony/var-exporter", + "version": "v7.3.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-exporter.git", + "reference": "0f020b544a30a7fe8ba972e53ee48a74c0bc87f4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/0f020b544a30a7fe8ba972e53ee48a74c0bc87f4", + "reference": "0f020b544a30a7fe8ba972e53ee48a74c0bc87f4", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "require-dev": { + "symfony/property-access": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^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/v7.3.4" }, "funding": [ { @@ -1236,34 +1496,32 @@ "type": "tidelift" } ], - "time": "2025-08-25T06:35:40+00:00" + "time": "2025-09-11T10:12:26+00:00" }, { "name": "symfony/yaml", - "version": "v6.0.0", + "version": "v7.3.3", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "f3064a2e0b5eabaeaf92db0a5913a77098b3b91e" + "reference": "d4f4a66866fe2451f61296924767280ab5732d9d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/f3064a2e0b5eabaeaf92db0a5913a77098b3b91e", - "reference": "f3064a2e0b5eabaeaf92db0a5913a77098b3b91e", + "url": "https://api.github.com/repos/symfony/yaml/zipball/d4f4a66866fe2451f61296924767280ab5732d9d", + "reference": "d4f4a66866fe2451f61296924767280ab5732d9d", "shasum": "" }, "require": { - "php": ">=8.0.2", + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", "symfony/polyfill-ctype": "^1.8" }, "conflict": { - "symfony/console": "<5.4" + "symfony/console": "<6.4" }, "require-dev": { - "symfony/console": "^5.4|^6.0" - }, - "suggest": { - "symfony/console": "For validating YAML files using the lint command" + "symfony/console": "^6.4|^7.0" }, "bin": [ "Resources/bin/yaml-lint" @@ -1294,7 +1552,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v6.0.0" + "source": "https://github.com/symfony/yaml/tree/v7.3.3" }, "funding": [ { @@ -1305,44 +1563,45 @@ "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": "2021-11-28T15:34:37+00:00" + "time": "2025-08-27T11:34:33+00:00" } ], "packages-dev": [ { "name": "colinodell/json5", - "version": "v2.2.0", + "version": "v3.0.0", "source": { "type": "git", "url": "https://github.com/colinodell/json5.git", - "reference": "dd7f788c5de3837d1483a216dc9b30e5d9c8c00a" + "reference": "5724d21bc5c910c2560af1b8915f0cc0163579c8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/colinodell/json5/zipball/dd7f788c5de3837d1483a216dc9b30e5d9c8c00a", - "reference": "dd7f788c5de3837d1483a216dc9b30e5d9c8c00a", + "url": "https://api.github.com/repos/colinodell/json5/zipball/5724d21bc5c910c2560af1b8915f0cc0163579c8", + "reference": "5724d21bc5c910c2560af1b8915f0cc0163579c8", "shasum": "" }, "require": { "ext-json": "*", "ext-mbstring": "*", - "php": "^7.1.3|^8.0" - }, - "conflict": { - "scrutinizer/ocular": "1.7.*" + "php": "^8.0" }, "require-dev": { - "mikehaertl/php-shellcommand": "^1.2.5", - "phpstan/phpstan": "^0.12.58", - "scrutinizer/ocular": "^1.6", - "squizlabs/php_codesniffer": "^2.3", - "symfony/finder": "^4.4|^5.2", - "symfony/phpunit-bridge": "^5.1" + "mikehaertl/php-shellcommand": "^1.7.0", + "phpstan/phpstan": "^1.10.57", + "scrutinizer/ocular": "^1.9", + "squizlabs/php_codesniffer": "^3.8.1", + "symfony/finder": "^6.0|^7.0", + "symfony/phpunit-bridge": "^7.0.3" }, "bin": [ "bin/json5" @@ -1350,7 +1609,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-main": "4.0-dev" } }, "autoload": { @@ -1383,7 +1642,7 @@ ], "support": { "issues": "https://github.com/colinodell/json5/issues", - "source": "https://github.com/colinodell/json5/tree/v2.2.0" + "source": "https://github.com/colinodell/json5/tree/v3.0.0" }, "funding": [ { @@ -1403,29 +1662,110 @@ "type": "patreon" } ], - "time": "2020-11-29T14:52:27+00:00" + "time": "2024-02-09T13:06:12+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": "2.0.0", + "version": "3.0.5", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "31d57697eb1971712a08031cfaff5a846d10bdf5" + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/31d57697eb1971712a08031cfaff5a846d10bdf5", - "reference": "31d57697eb1971712a08031cfaff5a846d10bdf5", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", "shasum": "" }, "require": { - "php": "^5.3.2 || ^7.0 || ^8.0", - "psr/log": "^1.0" + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1 || ^2 || ^3" }, "require-dev": { - "phpstan/phpstan": "^0.12.55", - "symfony/phpunit-bridge": "^4.2 || ^5" + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" }, "type": "library", "autoload": { @@ -1449,9 +1789,9 @@ "performance" ], "support": { - "irc": "irc://irc.freenode.org/composer", + "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/xdebug-handler/issues", - "source": "https://github.com/composer/xdebug-handler/tree/2.0.0" + "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" }, "funding": [ { @@ -1467,7 +1807,7 @@ "type": "tidelift" } ], - "time": "2021-04-09T19:40:06+00:00" + "time": "2024-05-06T16:37:16+00:00" }, { "name": "doctrine/annotations", @@ -1624,16 +1964,16 @@ }, { "name": "fidry/cpu-core-counter", - "version": "0.4.0", + "version": "1.3.0", "source": { "type": "git", "url": "https://github.com/theofidry/cpu-core-counter.git", - "reference": "666cb04a02f2801f3b19955fc23c824f9018bf64" + "reference": "db9508f7b1474469d9d3c53b86f817e344732678" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/666cb04a02f2801f3b19955fc23c824f9018bf64", - "reference": "666cb04a02f2801f3b19955fc23c824f9018bf64", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/db9508f7b1474469d9d3c53b86f817e344732678", + "reference": "db9508f7b1474469d9d3c53b86f817e344732678", "shasum": "" }, "require": { @@ -1641,13 +1981,13 @@ }, "require-dev": { "fidry/makefile": "^0.2.0", + "fidry/php-cs-fixer-config": "^1.1.2", "phpstan/extension-installer": "^1.2.0", - "phpstan/phpstan": "^1.9.2", - "phpstan/phpstan-deprecation-rules": "^1.0.0", - "phpstan/phpstan-phpunit": "^1.2.2", - "phpstan/phpstan-strict-rules": "^1.4.4", - "phpunit/phpunit": "^9.5.26 || ^8.5.31", - "theofidry/php-cs-fixer-config": "^1.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-deprecation-rules": "^2.0.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^8.5.31 || ^9.5.26", "webmozarts/strict-phpunit": "^7.5" }, "type": "library", @@ -1673,7 +2013,7 @@ ], "support": { "issues": "https://github.com/theofidry/cpu-core-counter/issues", - "source": "https://github.com/theofidry/cpu-core-counter/tree/0.4.0" + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.3.0" }, "funding": [ { @@ -1681,7 +2021,7 @@ "type": "github" } ], - "time": "2022-12-10T21:26:31+00:00" + "time": "2025-08-14T07:29:31+00:00" }, { "name": "infection/abstract-testframework-adapter", @@ -1740,33 +2080,33 @@ }, { "name": "infection/extension-installer", - "version": "0.1.1", + "version": "0.1.2", "source": { "type": "git", "url": "https://github.com/infection/extension-installer.git", - "reference": "ff30c0adffcdbc747c96adf92382ccbe271d0afd" + "reference": "9b351d2910b9a23ab4815542e93d541e0ca0cdcf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/infection/extension-installer/zipball/ff30c0adffcdbc747c96adf92382ccbe271d0afd", - "reference": "ff30c0adffcdbc747c96adf92382ccbe271d0afd", + "url": "https://api.github.com/repos/infection/extension-installer/zipball/9b351d2910b9a23ab4815542e93d541e0ca0cdcf", + "reference": "9b351d2910b9a23ab4815542e93d541e0ca0cdcf", "shasum": "" }, "require": { "composer-plugin-api": "^1.1 || ^2.0" }, "require-dev": { - "composer/composer": "^1.9", - "friendsofphp/php-cs-fixer": "^2.16", + "composer/composer": "^1.9 || ^2.0", + "friendsofphp/php-cs-fixer": "^2.18, <2.19", "infection/infection": "^0.15.2", - "php-coveralls/php-coveralls": "^2.2", + "php-coveralls/php-coveralls": "^2.4", "phpstan/extension-installer": "^1.0", "phpstan/phpstan": "^0.12.10", "phpstan/phpstan-phpunit": "^0.12.6", "phpstan/phpstan-strict-rules": "^0.12.2", "phpstan/phpstan-webmozart-assert": "^0.12.2", - "phpunit/phpunit": "^8.5", - "vimeo/psalm": "^3.8" + "phpunit/phpunit": "^9.5", + "vimeo/psalm": "^4.8" }, "type": "composer-plugin", "extra": { @@ -1783,16 +2123,26 @@ ], "authors": [ { - "name": "Maks Rafalko", - "email": "maks.rafalko@gmail.com" + "name": "Maks Rafalko", + "email": "maks.rafalko@gmail.com" + } + ], + "description": "Infection Extension Installer", + "support": { + "issues": "https://github.com/infection/extension-installer/issues", + "source": "https://github.com/infection/extension-installer/tree/0.1.2" + }, + "funding": [ + { + "url": "https://github.com/infection", + "type": "github" + }, + { + "url": "https://opencollective.com/infection", + "type": "open_collective" } ], - "description": "Infection Extension Installer", - "support": { - "issues": "https://github.com/infection/extension-installer/issues", - "source": "https://github.com/infection/extension-installer/tree/0.1.1" - }, - "time": "2020-04-25T22:40:05+00:00" + "time": "2021-10-20T22:08:34+00:00" }, { "name": "infection/include-interceptor", @@ -1852,16 +2202,16 @@ }, { "name": "infection/infection", - "version": "0.29.6", + "version": "0.29.14", "source": { "type": "git", "url": "https://github.com/infection/infection.git", - "reference": "a8510c1d472892dda2ae32e2c4b2e795533db810" + "reference": "feea2a48a8aeedd3a4d2105167b41a46f0e568a3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/infection/infection/zipball/a8510c1d472892dda2ae32e2c4b2e795533db810", - "reference": "a8510c1d472892dda2ae32e2c4b2e795533db810", + "url": "https://api.github.com/repos/infection/infection/zipball/feea2a48a8aeedd3a4d2105167b41a46f0e568a3", + "reference": "feea2a48a8aeedd3a4d2105167b41a46f0e568a3", "shasum": "" }, "require": { @@ -1877,18 +2227,18 @@ "infection/extension-installer": "^0.1.0", "infection/include-interceptor": "^0.2.5", "infection/mutator": "^0.4", - "justinrainbow/json-schema": "^5.2.10", - "nikic/php-parser": "^5.0", + "justinrainbow/json-schema": "^5.3 || ^6.0", + "nikic/php-parser": "^5.3", "ondram/ci-detector": "^4.1.0", - "php": "^8.1", + "php": "^8.2", "sanmai/later": "^0.1.1", "sanmai/pipeline": "^5.1 || ^6", - "sebastian/diff": "^3.0.2 || ^4.0 || ^5.0 || ^6.0", - "symfony/console": "^5.4 || ^6.0 || ^7.0", - "symfony/filesystem": "^5.4 || ^6.0 || ^7.0", - "symfony/finder": "^5.4 || ^6.0 || ^7.0", - "symfony/process": "^5.4 || ^6.0 || ^7.0", - "thecodingmachine/safe": "^2.1.2", + "sebastian/diff": "^3.0.2 || ^4.0 || ^5.0 || ^6.0 || ^7.0", + "symfony/console": "^6.4 || ^7.0", + "symfony/filesystem": "^6.4 || ^7.0", + "symfony/finder": "^6.4 || ^7.0", + "symfony/process": "^6.4 || ^7.0", + "thecodingmachine/safe": "^v3.0", "webmozart/assert": "^1.11" }, "conflict": { @@ -1899,19 +2249,16 @@ "require-dev": { "ext-simplexml": "*", "fidry/makefile": "^1.0", - "helmich/phpunit-json-assert": "^3.0", - "phpspec/prophecy": "^1.15", - "phpspec/prophecy-phpunit": "^2.0", - "phpstan/extension-installer": "^1.1.0", - "phpstan/phpstan": "^1.10.15", - "phpstan/phpstan-phpunit": "^1.0.0", - "phpstan/phpstan-strict-rules": "^1.1.0", - "phpstan/phpstan-webmozart-assert": "^1.0.2", - "phpunit/phpunit": "^10.5", - "rector/rector": "^1.0", - "sidz/phpstan-rules": "^0.4", - "symfony/yaml": "^5.4 || ^6.0 || ^7.0", - "thecodingmachine/phpstan-safe-rule": "^1.2.0" + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpstan/phpstan-webmozart-assert": "^2.0", + "phpunit/phpunit": "^11.5", + "rector/rector": "^2.0", + "sidz/phpstan-rules": "^0.5.1", + "symfony/yaml": "^6.4 || ^7.0", + "thecodingmachine/phpstan-safe-rule": "^1.4" }, "bin": [ "bin/infection" @@ -1967,7 +2314,7 @@ ], "support": { "issues": "https://github.com/infection/infection/issues", - "source": "https://github.com/infection/infection/tree/0.29.6" + "source": "https://github.com/infection/infection/tree/0.29.14" }, "funding": [ { @@ -1979,20 +2326,20 @@ "type": "open_collective" } ], - "time": "2024-06-21T10:21:05+00:00" + "time": "2025-03-02T18:49:12+00:00" }, { "name": "infection/mutator", - "version": "0.4.0", + "version": "0.4.1", "source": { "type": "git", "url": "https://github.com/infection/mutator.git", - "reference": "51d6d01a2357102030aee9d603063c4bad86b144" + "reference": "3c976d721b02b32f851ee4e15d553ef1e9186d1d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/infection/mutator/zipball/51d6d01a2357102030aee9d603063c4bad86b144", - "reference": "51d6d01a2357102030aee9d603063c4bad86b144", + "url": "https://api.github.com/repos/infection/mutator/zipball/3c976d721b02b32f851ee4e15d553ef1e9186d1d", + "reference": "3c976d721b02b32f851ee4e15d553ef1e9186d1d", "shasum": "" }, "require": { @@ -2020,7 +2367,7 @@ "description": "Mutator interface to implement custom mutators (mutation operators) for Infection", "support": { "issues": "https://github.com/infection/mutator/issues", - "source": "https://github.com/infection/mutator/tree/0.4.0" + "source": "https://github.com/infection/mutator/tree/0.4.1" }, "funding": [ { @@ -2032,29 +2379,34 @@ "type": "open_collective" } ], - "time": "2024-05-14T22:39:59+00:00" + "time": "2025-04-29T08:19:52+00:00" }, { "name": "justinrainbow/json-schema", - "version": "5.2.10", + "version": "6.5.2", "source": { "type": "git", "url": "https://github.com/jsonrainbow/json-schema.git", - "reference": "2ba9c8c862ecd5510ed16c6340aa9f6eadb4f31b" + "reference": "ac0d369c09653cf7af561f6d91a705bc617a87b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/2ba9c8c862ecd5510ed16c6340aa9f6eadb4f31b", - "reference": "2ba9c8c862ecd5510ed16c6340aa9f6eadb4f31b", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/ac0d369c09653cf7af561f6d91a705bc617a87b8", + "reference": "ac0d369c09653cf7af561f6d91a705bc617a87b8", "shasum": "" }, "require": { - "php": ">=5.3.3" + "ext-json": "*", + "marc-mabe/php-enum": "^4.0", + "php": "^7.2 || ^8.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "~2.2.20||~2.15.1", - "json-schema/json-schema-test-suite": "1.2.0", - "phpunit/phpunit": "^4.8.35" + "friendsofphp/php-cs-fixer": "3.3.0", + "json-schema/json-schema-test-suite": "^23.2", + "marc-mabe/php-enum-phpstan": "^2.0", + "phpspec/prophecy": "^1.19", + "phpstan/phpstan": "^1.12", + "phpunit/phpunit": "^8.5" }, "bin": [ "bin/validate-json" @@ -2062,7 +2414,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0.x-dev" + "dev-master": "6.x-dev" } }, "autoload": { @@ -2093,29 +2445,102 @@ } ], "description": "A library to validate a json schema.", - "homepage": "https://github.com/justinrainbow/json-schema", + "homepage": "https://github.com/jsonrainbow/json-schema", "keywords": [ "json", "schema" ], "support": { "issues": "https://github.com/jsonrainbow/json-schema/issues", - "source": "https://github.com/jsonrainbow/json-schema/tree/5.2.10" + "source": "https://github.com/jsonrainbow/json-schema/tree/6.5.2" + }, + "time": "2025-09-09T09:42:27+00:00" + }, + { + "name": "marc-mabe/php-enum", + "version": "v4.7.2", + "source": { + "type": "git", + "url": "https://github.com/marc-mabe/php-enum.git", + "reference": "bb426fcdd65c60fb3638ef741e8782508fda7eef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/marc-mabe/php-enum/zipball/bb426fcdd65c60fb3638ef741e8782508fda7eef", + "reference": "bb426fcdd65c60fb3638ef741e8782508fda7eef", + "shasum": "" + }, + "require": { + "ext-reflection": "*", + "php": "^7.1 | ^8.0" + }, + "require-dev": { + "phpbench/phpbench": "^0.16.10 || ^1.0.4", + "phpstan/phpstan": "^1.3.1", + "phpunit/phpunit": "^7.5.20 | ^8.5.22 | ^9.5.11", + "vimeo/psalm": "^4.17.0 | ^5.26.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-3.x": "3.2-dev", + "dev-master": "4.7-dev" + } + }, + "autoload": { + "psr-4": { + "MabeEnum\\": "src/" + }, + "classmap": [ + "stubs/Stringable.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Marc Bennewitz", + "email": "dev@mabe.berlin", + "homepage": "https://mabe.berlin/", + "role": "Lead" + } + ], + "description": "Simple and fast implementation of enumerations with native PHP", + "homepage": "https://github.com/marc-mabe/php-enum", + "keywords": [ + "enum", + "enum-map", + "enum-set", + "enumeration", + "enumerator", + "enummap", + "enumset", + "map", + "set", + "type", + "type-hint", + "typehint" + ], + "support": { + "issues": "https://github.com/marc-mabe/php-enum/issues", + "source": "https://github.com/marc-mabe/php-enum/tree/v4.7.2" }, - "time": "2020-05-27T16:41:55+00:00" + "time": "2025-09-14T11:18:39+00:00" }, { "name": "myclabs/deep-copy", - "version": "1.12.0", + "version": "1.13.4", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c" + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", - "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", "shasum": "" }, "require": { @@ -2154,7 +2579,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.12.0" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" }, "funding": [ { @@ -2162,33 +2587,33 @@ "type": "tidelift" } ], - "time": "2024-06-12T14:39:25+00:00" + "time": "2025-08-01T08:46:24+00:00" }, { "name": "ondram/ci-detector", - "version": "4.1.0", + "version": "4.2.0", "source": { "type": "git", "url": "https://github.com/OndraM/ci-detector.git", - "reference": "8a4b664e916df82ff26a44709942dfd593fa6f30" + "reference": "8b0223b5ed235fd377c75fdd1bfcad05c0f168b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/OndraM/ci-detector/zipball/8a4b664e916df82ff26a44709942dfd593fa6f30", - "reference": "8a4b664e916df82ff26a44709942dfd593fa6f30", + "url": "https://api.github.com/repos/OndraM/ci-detector/zipball/8b0223b5ed235fd377c75fdd1bfcad05c0f168b8", + "reference": "8b0223b5ed235fd377c75fdd1bfcad05c0f168b8", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" + "php": "^7.4 || ^8.0" }, "require-dev": { - "ergebnis/composer-normalize": "^2.2", - "lmc/coding-standard": "^1.3 || ^2.1", + "ergebnis/composer-normalize": "^2.13.2", + "lmc/coding-standard": "^3.0.0", "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/extension-installer": "^1.0.5", - "phpstan/phpstan": "^0.12.58", - "phpstan/phpstan-phpunit": "^0.12.16", - "phpunit/phpunit": "^7.1 || ^8.0 || ^9.0" + "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": { @@ -2238,22 +2663,22 @@ ], "support": { "issues": "https://github.com/OndraM/ci-detector/issues", - "source": "https://github.com/OndraM/ci-detector/tree/4.1.0" + "source": "https://github.com/OndraM/ci-detector/tree/4.2.0" }, - "time": "2021-04-14T09:16:52+00:00" + "time": "2024-03-12T13:22:30+00:00" }, { "name": "pdepend/pdepend", - "version": "2.16.1", + "version": "2.16.2", "source": { "type": "git", "url": "https://github.com/pdepend/pdepend.git", - "reference": "66ceb05eaa8bf358574143c974b04463911bc700" + "reference": "f942b208dc2a0868454d01b29f0c75bbcfc6ed58" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pdepend/pdepend/zipball/66ceb05eaa8bf358574143c974b04463911bc700", - "reference": "66ceb05eaa8bf358574143c974b04463911bc700", + "url": "https://api.github.com/repos/pdepend/pdepend/zipball/f942b208dc2a0868454d01b29f0c75bbcfc6ed58", + "reference": "f942b208dc2a0868454d01b29f0c75bbcfc6ed58", "shasum": "" }, "require": { @@ -2295,7 +2720,7 @@ ], "support": { "issues": "https://github.com/pdepend/pdepend/issues", - "source": "https://github.com/pdepend/pdepend/tree/2.16.1" + "source": "https://github.com/pdepend/pdepend/tree/2.16.2" }, "funding": [ { @@ -2303,7 +2728,7 @@ "type": "tidelift" } ], - "time": "2023-12-10T18:38:19+00:00" + "time": "2023-12-17T18:09:59+00:00" }, { "name": "phar-io/manifest", @@ -2657,16 +3082,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.0", - "source": { - "type": "git", - "url": "https://github.com/phpstan/phpstan.git", - "reference": "2392d360fdf54ea253aa6c68cad1d4ba2e54e927" - }, + "version": "2.1.30", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/2392d360fdf54ea253aa6c68cad1d4ba2e54e927", - "reference": "2392d360fdf54ea253aa6c68cad1d4ba2e54e927", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/a4a7f159927983dd4f7c8020ed227d80b7f39d7d", + "reference": "a4a7f159927983dd4f7c8020ed227d80b7f39d7d", "shasum": "" }, "require": { @@ -2711,39 +3131,39 @@ "type": "github" } ], - "time": "2024-12-31T07:30:03+00:00" + "time": "2025-10-02T16:07:52+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "11.0.5", + "version": "11.0.11", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "19b6365ab8b59a64438c0c3f4241feeb480c9861" + "reference": "4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/19b6365ab8b59a64438c0c3f4241feeb480c9861", - "reference": "19b6365ab8b59a64438c0c3f4241feeb480c9861", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4", + "reference": "4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^5.0", + "nikic/php-parser": "^5.4.0", "php": ">=8.2", - "phpunit/php-file-iterator": "^5.0", - "phpunit/php-text-template": "^4.0", - "sebastian/code-unit-reverse-lookup": "^4.0", - "sebastian/complexity": "^4.0", - "sebastian/environment": "^7.0", - "sebastian/lines-of-code": "^3.0", - "sebastian/version": "^5.0", - "theseer/tokenizer": "^1.2.0" + "phpunit/php-file-iterator": "^5.1.0", + "phpunit/php-text-template": "^4.0.1", + "sebastian/code-unit-reverse-lookup": "^4.0.1", + "sebastian/complexity": "^4.0.1", + "sebastian/environment": "^7.2.0", + "sebastian/lines-of-code": "^3.0.1", + "sebastian/version": "^5.0.2", + "theseer/tokenizer": "^1.2.3" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^11.5.2" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -2752,7 +3172,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "11.0-dev" + "dev-main": "11.0.x-dev" } }, "autoload": { @@ -2781,28 +3201,40 @@ "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/11.0.5" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.11" }, "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/phpunit/php-code-coverage", + "type": "tidelift" } ], - "time": "2024-07-03T05:05:37+00:00" + "time": "2025-08-27T14:37:49+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "5.0.1", + "version": "5.1.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "6ed896bf50bbbfe4d504a33ed5886278c78e4a26" + "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/6ed896bf50bbbfe4d504a33ed5886278c78e4a26", - "reference": "6ed896bf50bbbfe4d504a33ed5886278c78e4a26", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/118cfaaa8bc5aef3287bf315b6060b1174754af6", + "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6", "shasum": "" }, "require": { @@ -2842,7 +3274,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.0.1" + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.0" }, "funding": [ { @@ -2850,7 +3282,7 @@ "type": "github" } ], - "time": "2024-07-03T05:06:37+00:00" + "time": "2024-08-27T05:02:59+00:00" }, { "name": "phpunit/php-invoker", @@ -3038,16 +3470,16 @@ }, { "name": "phpunit/phpunit", - "version": "11.3.0", + "version": "11.5.42", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "a8dce73a8938dfec7ac0daa91bdbcaae7d7188a3" + "reference": "1c6cb5dfe412af3d0dfd414cfd110e3b9cfdbc3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a8dce73a8938dfec7ac0daa91bdbcaae7d7188a3", - "reference": "a8dce73a8938dfec7ac0daa91bdbcaae7d7188a3", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/1c6cb5dfe412af3d0dfd414cfd110e3b9cfdbc3c", + "reference": "1c6cb5dfe412af3d0dfd414cfd110e3b9cfdbc3c", "shasum": "" }, "require": { @@ -3057,25 +3489,26 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.12.0", + "myclabs/deep-copy": "^1.13.4", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.2", - "phpunit/php-code-coverage": "^11.0.5", - "phpunit/php-file-iterator": "^5.0.1", + "phpunit/php-code-coverage": "^11.0.11", + "phpunit/php-file-iterator": "^5.1.0", "phpunit/php-invoker": "^5.0.1", "phpunit/php-text-template": "^4.0.1", "phpunit/php-timer": "^7.0.1", "sebastian/cli-parser": "^3.0.2", - "sebastian/code-unit": "^3.0.1", - "sebastian/comparator": "^6.0.1", + "sebastian/code-unit": "^3.0.3", + "sebastian/comparator": "^6.3.2", "sebastian/diff": "^6.0.2", - "sebastian/environment": "^7.2.0", - "sebastian/exporter": "^6.1.3", + "sebastian/environment": "^7.2.1", + "sebastian/exporter": "^6.3.2", "sebastian/global-state": "^7.0.2", "sebastian/object-enumerator": "^6.0.1", - "sebastian/type": "^5.0.1", - "sebastian/version": "^5.0.1" + "sebastian/type": "^5.1.3", + "sebastian/version": "^5.0.2", + "staabm/side-effects-detector": "^1.0.5" }, "suggest": { "ext-soap": "To be able to generate mocks based on WSDL files" @@ -3086,7 +3519,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "11.3-dev" + "dev-main": "11.5-dev" } }, "autoload": { @@ -3118,7 +3551,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/11.3.0" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.42" }, "funding": [ { @@ -3129,61 +3562,20 @@ "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/phpunit/phpunit", "type": "tidelift" } ], - "time": "2024-08-02T03:56:43+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" + "time": "2025-09-28T12:09:13+00:00" }, { "name": "roave/security-advisories", @@ -3191,19 +3583,19 @@ "source": { "type": "git", "url": "https://github.com/Roave/SecurityAdvisories.git", - "reference": "d9e8845908f658ef04aebe52283cfc63b192d421" + "reference": "60ac6a710b7b0527786041ba96200bd49aa8de7e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/d9e8845908f658ef04aebe52283cfc63b192d421", - "reference": "d9e8845908f658ef04aebe52283cfc63b192d421", + "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/60ac6a710b7b0527786041ba96200bd49aa8de7e", + "reference": "60ac6a710b7b0527786041ba96200bd49aa8de7e", "shasum": "" }, "conflict": { "3f/pygmentize": "<1.2", "adaptcms/adaptcms": "<=1.3", "admidio/admidio": "<4.3.12", - "adodb/adodb-php": "<=5.22.8", + "adodb/adodb-php": "<=5.22.9", "aheinze/cockpit": "<2.2", "aimeos/ai-admin-graphql": ">=2022.04.1,<2022.10.10|>=2023.04.1,<2023.10.6|>=2024.04.1,<2024.07.2", "aimeos/ai-admin-jsonadm": "<2020.10.13|>=2021.04.1,<2021.10.6|>=2022.04.1,<2022.10.3|>=2023.04.1,<2023.10.4|==2024.04.1", @@ -3214,7 +3606,7 @@ "airesvsg/acf-to-rest-api": "<=3.1", "akaunting/akaunting": "<2.1.13", "akeneo/pim-community-dev": "<5.0.119|>=6,<6.0.53", - "alextselegidis/easyappointments": "<=1.5.1", + "alextselegidis/easyappointments": "<1.5.2.0-beta1", "alterphp/easyadmin-extension-bundle": ">=1.2,<1.2.11|>=1.3,<1.3.1", "amazing/media2click": ">=1,<1.3.3", "ameos/ameos_tarteaucitron": "<1.2.23", @@ -3227,8 +3619,8 @@ "aoe/restler": "<1.7.1", "apache-solr-for-typo3/solr": "<2.8.3", "apereo/phpcas": "<1.6", - "api-platform/core": "<3.4.17|>=4.0.0.0-alpha1,<4.0.22", - "api-platform/graphql": "<3.4.17|>=4.0.0.0-alpha1,<4.0.22", + "api-platform/core": "<3.4.17|>=4,<4.0.22|>=4.1,<4.1.5", + "api-platform/graphql": "<3.4.17|>=4,<4.0.22|>=4.1,<4.1.5", "appwrite/server-ce": "<=1.2.1", "arc/web": "<3", "area17/twill": "<1.2.5|>=2,<2.5.3", @@ -3238,10 +3630,10 @@ "athlon1600/php-proxy-app": "<=3", "athlon1600/youtube-downloader": "<=4", "austintoddj/canvas": "<=3.4.2", - "auth0/auth0-php": ">=8.0.0.0-beta1,<8.14", - "auth0/login": "<7.17", - "auth0/symfony": "<5.4", - "auth0/wordpress": "<5.3", + "auth0/auth0-php": ">=3.3,<=8.16", + "auth0/login": "<=7.18", + "auth0/symfony": "<=5.4.1", + "auth0/wordpress": "<=5.3", "automad/automad": "<2.0.0.0-alpha5", "automattic/jetpack": "<9.8", "awesome-support/awesome-support": "<=6.0.7", @@ -3251,8 +3643,8 @@ "backdrop/backdrop": "<1.27.3|>=1.28,<1.28.2", "backpack/crud": "<3.4.9", "backpack/filemanager": "<2.0.2|>=3,<3.0.9", - "bacula-web/bacula-web": "<8.0.0.0-RC2-dev", - "badaso/core": "<2.7", + "bacula-web/bacula-web": "<9.7.1", + "badaso/core": "<=2.9.11", "bagisto/bagisto": "<2.1", "barrelstrength/sprout-base-email": "<1.2.7", "barrelstrength/sprout-forms": "<3.9", @@ -3306,26 +3698,26 @@ "cockpit-hq/cockpit": "<2.11.4", "codeception/codeception": "<3.1.3|>=4,<4.1.22", "codeigniter/framework": "<3.1.10", - "codeigniter4/framework": "<4.5.8", + "codeigniter4/framework": "<4.6.2", "codeigniter4/shield": "<1.0.0.0-beta8", "codiad/codiad": "<=2.8.4", "codingms/additional-tca": ">=1.7,<1.15.17|>=1.16,<1.16.9", "commerceteam/commerce": ">=0.9.6,<0.9.9", "components/jquery": ">=1.0.3,<3.5", "composer/composer": "<1.10.27|>=2,<2.2.24|>=2.3,<2.7.7", - "concrete5/concrete5": "<9.4.0.0-RC2-dev", + "concrete5/concrete5": "<9.4.3", "concrete5/core": "<8.5.8|>=9,<9.1", "contao-components/mediaelement": ">=2.14.2,<2.21.1", "contao/comments-bundle": ">=2,<4.13.40|>=5.0.0.0-RC1-dev,<5.3.4", - "contao/contao": ">=3,<3.5.37|>=4,<4.4.56|>=4.5,<4.9.40|>=4.10,<4.11.7|>=4.13,<4.13.21|>=5.1,<5.1.4", + "contao/contao": ">=3,<3.5.37|>=4,<4.4.56|>=4.5,<4.13.56|>=5,<5.3.38|>=5.4.0.0-RC1-dev,<5.6.1", "contao/core": "<3.5.39", - "contao/core-bundle": "<4.13.54|>=5,<5.3.30|>=5.4,<5.5.6", + "contao/core-bundle": "<4.13.56|>=5,<5.3.38|>=5.4,<5.6.1", "contao/listing-bundle": ">=3,<=3.5.30|>=4,<4.4.8", "contao/managed-edition": "<=1.5", "corveda/phpsandbox": "<1.3.5", "cosenary/instagram": "<=2.3", "couleurcitron/tarteaucitron-wp": "<0.3", - "craftcms/cms": "<4.15.3|>=5,<5.7.5", + "craftcms/cms": "<=4.16.5|>=5,<=5.8.6", "croogo/croogo": "<4", "cuyz/valinor": "<0.12", "czim/file-handling": "<1.5|>=2,<2.3", @@ -3334,6 +3726,7 @@ "dapphp/securimage": "<3.6.6", "darylldoyle/safe-svg": "<1.9.10", "datadog/dd-trace": ">=0.30,<0.30.2", + "datahihi1/tiny-env": "<1.0.3|>=1.0.9,<1.0.11", "datatables/datatables": "<1.10.10", "david-garcia/phpwhois": "<=4.3.1", "dbrisinajumi/d2files": "<1", @@ -3357,7 +3750,7 @@ "doctrine/mongodb-odm": "<1.0.2", "doctrine/mongodb-odm-bundle": "<3.0.1", "doctrine/orm": ">=1,<1.2.4|>=2,<2.4.8|>=2.5,<2.5.1|>=2.8.3,<2.8.4", - "dolibarr/dolibarr": "<19.0.2|==21.0.0.0-beta", + "dolibarr/dolibarr": "<21.0.3", "dompdf/dompdf": "<2.0.4", "doublethreedigital/guest-entries": "<3.1.2", "drupal/admin_audit_trail": "<1.0.5", @@ -3393,11 +3786,11 @@ "elefant/cms": "<2.0.7", "elgg/elgg": "<3.3.24|>=4,<4.0.5", "elijaa/phpmemcacheadmin": "<=1.3", - "elmsln/haxcms": "<11", + "elmsln/haxcms": "<11.0.14", "encore/laravel-admin": "<=1.8.19", "endroid/qr-code-bundle": "<3.4.2", "enhavo/enhavo-app": "<=0.13.1", - "enshrined/svg-sanitize": "<0.15", + "enshrined/svg-sanitize": "<0.22", "erusev/parsedown": "<1.7.2", "ether/logs": "<3.0.4", "evolutioncms/evolution": "<=3.2.3", @@ -3481,8 +3874,9 @@ "globalpayments/php-sdk": "<2", "goalgorilla/open_social": "<12.3.11|>=12.4,<12.4.10|>=13.0.0.0-alpha1,<13.0.0.0-alpha11", "gogentooss/samlbase": "<1.2.7", - "google/protobuf": "<3.15", + "google/protobuf": "<3.4", "gos/web-socket-bundle": "<1.10.4|>=2,<2.6.1|>=3,<3.3", + "gp247/core": "<1.1.24", "gree/jose": "<2.2.1", "gregwar/rst": "<1.0.3", "grumpydictator/firefly-iii": "<6.1.17", @@ -3521,10 +3915,10 @@ "imdbphp/imdbphp": "<=5.1.1", "impresscms/impresscms": "<=1.4.5", "impresspages/impresspages": "<1.0.13", - "in2code/femanager": "<5.5.5|>=6,<6.4.1|>=7,<7.4.2|>=8,<8.2.2", + "in2code/femanager": "<6.4.2|>=7,<7.5.3|>=8,<8.3.1", "in2code/ipandlanguageredirect": "<5.1.2", "in2code/lux": "<17.6.1|>=18,<24.0.2", - "in2code/powermail": "<7.5.1|>=8,<8.5.1|>=9,<10.9.1|>=11,<12.4.1", + "in2code/powermail": "<7.5.1|>=8,<8.5.1|>=9,<10.9.1|>=11,<12.5.3|==13", "innologi/typo3-appointments": "<2.0.6", "intelliants/subrion": "<4.2.2", "inter-mediator/inter-mediator": "==5.5", @@ -3540,12 +3934,12 @@ "jbartels/wec-map": "<3.0.3", "jcbrand/converse.js": "<3.3.3", "joelbutcher/socialstream": "<5.6|>=6,<6.2", - "johnbillion/wp-crontrol": "<1.16.2", + "johnbillion/wp-crontrol": "<1.16.2|>=1.17,<1.19.2", "joomla/application": "<1.0.13", "joomla/archive": "<1.1.12|>=2,<2.0.1", "joomla/database": ">=1,<2.2|>=3,<3.4", "joomla/filesystem": "<1.6.2|>=2,<2.0.1", - "joomla/filter": "<1.4.4|>=2,<2.0.1", + "joomla/filter": "<2.0.6|>=3,<3.0.5|==4", "joomla/framework": "<1.5.7|>=2.5.4,<=3.8.12", "joomla/input": ">=2,<2.0.2", "joomla/joomla-cms": "<3.9.12|>=4,<4.4.13|>=5,<5.2.6", @@ -3584,6 +3978,7 @@ "laravel/socialite": ">=1,<2.0.10", "latte/latte": "<2.10.8", "lavalite/cms": "<=9|==10.1", + "lavitto/typo3-form-to-database": "<2.2.5|>=3,<3.2.2|>=4,<4.2.3|>=5,<5.0.2", "lcobucci/jwt": ">=3.4,<3.4.6|>=4,<4.0.4|>=4.1,<4.1.5", "league/commonmark": "<2.7", "league/flysystem": "<1.1.4|>=2,<2.1.1", @@ -3596,7 +3991,7 @@ "lightsaml/lightsaml": "<1.3.5", "limesurvey/limesurvey": "<6.5.12", "livehelperchat/livehelperchat": "<=3.91", - "livewire/livewire": "<2.12.7|>=3.0.0.0-beta1,<3.5.2", + "livewire/livewire": "<2.12.7|>=3.0.0.0-beta1,<3.6.4", "livewire/volt": "<1.7", "lms/routes": "<2.1.1", "localizationteam/l10nmgr": "<7.4|>=8,<8.7|>=9,<9.2", @@ -3605,20 +4000,23 @@ "luyadev/yii-helpers": "<1.2.1", "macropay-solutions/laravel-crud-wizard-free": "<3.4.17", "maestroerror/php-heic-to-jpg": "<1.0.5", - "magento/community-edition": "<2.4.5.0-patch13|==2.4.6|>=2.4.6.0-patch1,<2.4.6.0-patch11|>=2.4.7.0-beta1,<2.4.7.0-patch6|>=2.4.8.0-beta1,<2.4.8.0-patch1", + "magento/community-edition": "<=2.4.5.0-patch14|==2.4.6|>=2.4.6.0-patch1,<=2.4.6.0-patch12|>=2.4.7.0-beta1,<=2.4.7.0-patch7|>=2.4.8.0-beta1,<=2.4.8.0-patch2|>=2.4.9.0-alpha1,<=2.4.9.0-alpha2|==2.4.9", "magento/core": "<=1.9.4.5", "magento/magento1ce": "<1.9.4.3-dev", "magento/magento1ee": ">=1,<1.14.4.3-dev", "magento/product-community-edition": "<2.4.4.0-patch9|>=2.4.5,<2.4.5.0-patch8|>=2.4.6,<2.4.6.0-patch6|>=2.4.7,<2.4.7.0-patch1", "magento/project-community-edition": "<=2.0.2", "magneto/core": "<1.9.4.4-dev", + "mahocommerce/maho": "<25.9", "maikuolan/phpmussel": ">=1,<1.6", "mainwp/mainwp": "<=4.4.3.3", + "manogi/nova-tiptap": "<=3.2.6", "mantisbt/mantisbt": "<=2.26.3", "marcwillmann/turn": "<0.3.3", + "marshmallow/nova-tiptap": "<5.7", "matomo/matomo": "<1.11", "matyhtf/framework": "<3.0.6", - "mautic/core": "<5.2.6|>=6.0.0.0-alpha,<6.0.2", + "mautic/core": "<5.2.8|>=6.0.0.0-alpha,<6.0.5", "mautic/core-lib": ">=1.0.0.0-beta,<4.4.13|>=5.0.0.0-alpha,<5.1.1", "maximebf/debugbar": "<1.19", "mdanter/ecc": "<2", @@ -3647,6 +4045,7 @@ "mongodb/mongodb": ">=1,<1.9.2", "monolog/monolog": ">=1.8,<1.12", "moodle/moodle": "<4.3.12|>=4.4,<4.4.8|>=4.5.0.0-beta,<4.5.4", + "moonshine/moonshine": "<=3.12.5", "mos/cimage": "<0.7.19", "movim/moxl": ">=0.8,<=0.10", "movingbytes/social-network": "<=1.2.1", @@ -3680,6 +4079,7 @@ "notrinos/notrinos-erp": "<=0.7", "noumo/easyii": "<=0.9", "novaksolutions/infusionsoft-php-sdk": "<1", + "novosga/novosga": "<=2.2.9", "nukeviet/nukeviet": "<4.5.02", "nyholm/psr7": "<1.6.1", "nystudio107/craft-seomatic": "<3.4.12", @@ -3742,7 +4142,7 @@ "phpoffice/common": "<0.2.9", "phpoffice/math": "<=0.2", "phpoffice/phpexcel": "<=1.8.2", - "phpoffice/phpspreadsheet": "<1.29.9|>=2,<2.1.8|>=2.2,<2.3.7|>=3,<3.9", + "phpoffice/phpspreadsheet": "<1.30|>=2,<2.1.12|>=2.2,<2.4|>=3,<3.10|>=4,<5", "phpseclib/phpseclib": "<2.0.47|>=3,<3.0.36", "phpservermon/phpservermon": "<3.6", "phpsysinfo/phpsysinfo": "<3.4.3", @@ -3763,7 +4163,7 @@ "pixelfed/pixelfed": "<0.12.5", "plotly/plotly.js": "<2.25.2", "pocketmine/bedrock-protocol": "<8.0.2", - "pocketmine/pocketmine-mp": "<5.25.2", + "pocketmine/pocketmine-mp": "<5.32.1", "pocketmine/raklib": ">=0.14,<0.14.6|>=0.15,<0.15.1", "pressbooks/pressbooks": "<5.18", "prestashop/autoupgrade": ">=4,<4.10.1", @@ -3771,7 +4171,7 @@ "prestashop/blockwishlist": ">=2,<2.1.1", "prestashop/contactform": ">=1.0.1,<4.3", "prestashop/gamification": "<2.3.2", - "prestashop/prestashop": "<8.1.6", + "prestashop/prestashop": "<8.2.3", "prestashop/productcomments": "<5.0.2", "prestashop/ps_contactinfo": "<=3.3.2", "prestashop/ps_emailsubscription": "<2.6.1", @@ -3810,7 +4210,7 @@ "roundcube/roundcubemail": "<1.5.10|>=1.6,<1.6.11", "rudloff/alltube": "<3.0.3", "rudloff/rtmpdump-bin": "<=2.3.1", - "s-cart/core": "<6.9", + "s-cart/core": "<=9.0.5", "s-cart/s-cart": "<6.9", "sabberworm/php-css-parser": ">=1,<1.0.1|>=2,<2.0.1|>=3,<3.0.1|>=4,<4.0.1|>=5,<5.0.9|>=5.1,<5.1.3|>=5.2,<5.2.1|>=6,<6.0.2|>=7,<7.0.4|>=8,<8.0.1|>=8.1,<8.1.1|>=8.2,<8.2.1|>=8.3,<8.3.1", "sabre/dav": ">=1.6,<1.7.11|>=1.8,<1.8.9", @@ -3818,12 +4218,13 @@ "scheb/two-factor-bundle": "<3.26|>=4,<4.11", "sensiolabs/connect": "<4.2.3", "serluck/phpwhois": "<=4.2.6", + "setasign/fpdi": "<2.6.4", "sfroemken/url_redirect": "<=1.2.1", "sheng/yiicms": "<1.2.1", - "shopware/core": "<6.5.8.18-dev|>=6.6,<6.6.10.3-dev|>=6.7.0.0-RC1-dev,<6.7.0.0-RC2-dev", - "shopware/platform": "<6.5.8.18-dev|>=6.6,<6.6.10.3-dev|>=6.7.0.0-RC1-dev,<6.7.0.0-RC2-dev", + "shopware/core": "<6.5.8.18-dev|>=6.6,<6.6.10.3-dev|>=6.7,<6.7.2.1-dev", + "shopware/platform": "<=6.6.10.4|>=6.7.0.0-RC1-dev,<6.7.0.0-RC2-dev", "shopware/production": "<=6.3.5.2", - "shopware/shopware": "<=5.7.17", + "shopware/shopware": "<=5.7.17|>=6.7,<6.7.2.1-dev", "shopware/storefront": "<=6.4.8.1|>=6.5.8,<6.5.8.7-dev", "shopxo/shopxo": "<=6.4", "showdoc/showdoc": "<2.10.4", @@ -3846,6 +4247,7 @@ "silverstripe/taxonomy": ">=1.3,<1.3.1|>=2,<2.0.1", "silverstripe/userforms": "<3|>=5,<5.4.2", "silverstripe/versioned-admin": ">=1,<1.11.1", + "simogeo/filemanager": "<=2.5", "simple-updates/phpwhois": "<=1", "simplesamlphp/saml2": "<=4.16.15|>=5.0.0.0-alpha1,<=5.0.0.0-alpha19", "simplesamlphp/saml2-legacy": "<=4.16.15", @@ -3864,9 +4266,11 @@ "slim/slim": "<2.6", "slub/slub-events": "<3.0.3", "smarty/smarty": "<4.5.3|>=5,<5.1.1", - "snipe/snipe-it": "<8.1", + "snipe/snipe-it": "<8.1.18", "socalnick/scn-social-auth": "<1.15.2", "socialiteproviders/steam": "<1.1", + "solspace/craft-freeform": ">=5,<5.10.16", + "soosyze/soosyze": "<=2", "spatie/browsershot": "<5.0.5", "spatie/image-optimizer": "<1.7.3", "spencer14420/sp-php-email-handler": "<1", @@ -3878,6 +4282,7 @@ "starcitizentools/citizen-skin": ">=1.9.4,<3.4", "starcitizentools/short-description": ">=4,<4.0.1", "starcitizentools/tabber-neue": ">=1.9.1,<2.7.2|>=3,<3.1.1", + "starcitizenwiki/embedvideo": "<=4", "statamic/cms": "<=5.16", "stormpath/sdk": "<9.9.99", "studio-42/elfinder": "<=2.1.64", @@ -3952,7 +4357,7 @@ "thelia/thelia": ">=2.1,<2.1.3", "theonedemon/phpwhois": "<=4.2.5", "thinkcmf/thinkcmf": "<6.0.8", - "thorsten/phpmyfaq": "<=4.0.1", + "thorsten/phpmyfaq": "<=4.0.1|>=4.0.7,<4.0.13", "tikiwiki/tiki-manager": "<=17.1", "timber/timber": ">=0.16.6,<1.23.1|>=1.24,<1.24.1|>=2,<2.1", "tinymce/tinymce": "<7.2", @@ -3968,14 +4373,14 @@ "tribalsystems/zenario": "<=9.7.61188", "truckersmp/phpwhois": "<=4.3.1", "ttskch/pagination-service-provider": "<1", - "twbs/bootstrap": "<=3.4.1|>=4,<=4.6.2", + "twbs/bootstrap": "<3.4.1|>=4,<=4.6.2", "twig/twig": "<3.11.2|>=3.12,<3.14.1|>=3.16,<3.19", "typo3/cms": "<9.5.29|>=10,<10.4.35|>=11,<11.5.23|>=12,<12.2", - "typo3/cms-backend": "<4.1.14|>=4.2,<4.2.15|>=4.3,<4.3.7|>=4.4,<4.4.4|>=7,<=7.6.50|>=8,<=8.7.39|>=9,<=9.5.24|>=10,<10.4.46|>=11,<11.5.40|>=12,<=12.4.30|>=13,<=13.4.11", + "typo3/cms-backend": "<4.1.14|>=4.2,<4.2.15|>=4.3,<4.3.7|>=4.4,<4.4.4|>=7,<=7.6.50|>=8,<=8.7.39|>=9,<9.5.55|>=10,<10.4.54|>=11,<11.5.48|>=12,<12.4.37|>=13,<13.4.18", "typo3/cms-belog": ">=10,<=10.4.47|>=11,<=11.5.41|>=12,<=12.4.24|>=13,<=13.4.2", - "typo3/cms-beuser": ">=10,<=10.4.47|>=11,<=11.5.41|>=12,<=12.4.24|>=13,<=13.4.2", - "typo3/cms-core": "<=8.7.56|>=9,<=9.5.50|>=10,<=10.4.49|>=11,<=11.5.43|>=12,<=12.4.30|>=13,<=13.4.11", - "typo3/cms-dashboard": ">=10,<=10.4.47|>=11,<=11.5.41|>=12,<=12.4.24|>=13,<=13.4.2", + "typo3/cms-beuser": ">=9,<9.5.55|>=10,<10.4.54|>=11,<11.5.48|>=12,<12.4.37|>=13,<13.4.18", + "typo3/cms-core": "<=8.7.56|>=9,<9.5.55|>=10,<10.4.54|>=11,<11.5.48|>=12,<12.4.37|>=13,<13.4.18", + "typo3/cms-dashboard": ">=10,<10.4.54|>=11,<11.5.48|>=12,<12.4.37|>=13,<13.4.18", "typo3/cms-extbase": "<6.2.24|>=7,<7.6.8|==8.1.1", "typo3/cms-extensionmanager": ">=10,<=10.4.47|>=11,<=11.5.41|>=12,<=12.4.24|>=13,<=13.4.2", "typo3/cms-felogin": ">=4.2,<4.2.3", @@ -3985,10 +4390,13 @@ "typo3/cms-indexed-search": ">=10,<=10.4.47|>=11,<=11.5.41|>=12,<=12.4.24|>=13,<=13.4.2", "typo3/cms-install": "<4.1.14|>=4.2,<4.2.16|>=4.3,<4.3.9|>=4.4,<4.4.5|>=12.2,<12.4.8|==13.4.2", "typo3/cms-lowlevel": ">=11,<=11.5.41", + "typo3/cms-recordlist": ">=11,<11.5.48", + "typo3/cms-recycler": ">=9,<9.5.55|>=10,<10.4.54|>=11,<11.5.48|>=12,<12.4.37|>=13,<13.4.18", "typo3/cms-rte-ckeditor": ">=9.5,<9.5.42|>=10,<10.4.39|>=11,<11.5.30", "typo3/cms-scheduler": ">=11,<=11.5.41", "typo3/cms-setup": ">=9,<=9.5.50|>=10,<=10.4.49|>=11,<=11.5.43|>=12,<=12.4.30|>=13,<=13.4.11", "typo3/cms-webhooks": ">=12,<=12.4.30|>=13,<=13.4.11", + "typo3/cms-workspaces": ">=9,<9.5.55|>=10,<10.4.54|>=11,<11.5.48|>=12,<12.4.37|>=13,<13.4.18", "typo3/flow": ">=1,<1.0.4|>=1.1,<1.1.1|>=2,<2.0.1|>=2.3,<2.3.16|>=3,<3.0.12|>=3.1,<3.1.10|>=3.2,<3.2.13|>=3.3,<3.3.13|>=4,<4.0.6", "typo3/html-sanitizer": ">=1,<=1.5.2|>=2,<=2.1.3", "typo3/neos": ">=1.1,<1.1.3|>=1.2,<1.2.13|>=2,<2.0.4|>=2.3,<2.3.99|>=3,<3.0.20|>=3.1,<3.1.18|>=3.2,<3.2.14|>=3.3,<3.3.23|>=4,<4.0.17|>=4.1,<4.1.16|>=4.2,<4.2.12|>=4.3,<4.3.3", @@ -3999,7 +4407,7 @@ "uasoft-indonesia/badaso": "<=2.9.7", "unisharp/laravel-filemanager": "<2.9.1", "universal-omega/dynamic-page-list3": "<3.6.4", - "unopim/unopim": "<0.1.5", + "unopim/unopim": "<=0.3", "userfrosting/userfrosting": ">=0.3.1,<4.6.3", "usmanhalalit/pixie": "<1.0.3|>=2,<2.0.2", "uvdesk/community-skeleton": "<=1.1.1", @@ -4013,7 +4421,7 @@ "vertexvaar/falsftp": "<0.2.6", "villagedefrance/opencart-overclocked": "<=1.11.1", "vova07/yii2-fileapi-widget": "<0.1.9", - "vrana/adminer": "<4.8.1", + "vrana/adminer": "<=4.8.1", "vufind/vufind": ">=2,<9.1.1", "waldhacker/hcaptcha": "<2.1.2", "wallabag/tcpdf": "<6.2.22", @@ -4049,7 +4457,7 @@ "xataface/xataface": "<3", "xpressengine/xpressengine": "<3.0.15", "yab/quarx": "<2.4.5", - "yeswiki/yeswiki": "<4.5.4", + "yeswiki/yeswiki": "<=4.5.4", "yetiforce/yetiforce-crm": "<6.5", "yidashi/yii2cmf": "<=2", "yii2mod/yii2-cms": "<1.9.2", @@ -4066,6 +4474,7 @@ "yoast-seo-for-typo3/yoast_seo": "<7.2.3", "yourls/yourls": "<=1.8.2", "yuan1994/tpadmin": "<=1.3.12", + "z-push/z-push-dev": "<2.7.6", "zencart/zencart": "<=1.5.7.0-beta", "zendesk/zendesk_api_client_php": "<2.2.11", "zendframework/zend-cache": ">=2.4,<2.4.8|>=2.5,<2.5.3", @@ -4140,35 +4549,41 @@ "type": "tidelift" } ], - "time": "2025-07-14T22:05:34+00:00" + "time": "2025-10-03T15:05:30+00:00" }, { "name": "sanmai/later", - "version": "0.1.1", + "version": "0.1.7", "source": { "type": "git", "url": "https://github.com/sanmai/later.git", - "reference": "d42e29e587fbac5a7a64e4081348206c2fb0987c" + "reference": "72a82d783864bca90412d8a26c1878f8981fee97" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sanmai/later/zipball/d42e29e587fbac5a7a64e4081348206c2fb0987c", - "reference": "d42e29e587fbac5a7a64e4081348206c2fb0987c", + "url": "https://api.github.com/repos/sanmai/later/zipball/72a82d783864bca90412d8a26c1878f8981fee97", + "reference": "72a82d783864bca90412d8a26c1878f8981fee97", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" + "php": ">=8.2" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^2.13", - "infection/infection": ">=0.10.5", - "phan/phan": "^2.0", + "ergebnis/composer-normalize": "^2.8", + "friendsofphp/php-cs-fixer": "^3.35.1", + "infection/infection": ">=0.27.6", + "phan/phan": ">=2", "php-coveralls/php-coveralls": "^2.0", - "phpstan/phpstan": ">=0.10", - "phpunit/phpunit": "^7.4 || ^8.1 || ^9.3", - "vimeo/psalm": "^2.0 || ^3.0" + "phpstan/phpstan": ">=1.4.5", + "phpunit/phpunit": ">=9.5 <10", + "vimeo/psalm": ">=2" }, "type": "library", + "extra": { + "branch-alias": { + "dev-main": "0.1.x-dev" + } + }, "autoload": { "files": [ "src/functions.php" @@ -4190,7 +4605,7 @@ "description": "Later: deferred wrapper object", "support": { "issues": "https://github.com/sanmai/later/issues", - "source": "https://github.com/sanmai/later/tree/0.1.1" + "source": "https://github.com/sanmai/later/tree/0.1.7" }, "funding": [ { @@ -4198,40 +4613,43 @@ "type": "github" } ], - "time": "2020-09-14T14:07:41+00:00" + "time": "2025-05-11T01:48:00+00:00" }, { "name": "sanmai/pipeline", - "version": "v5.1.0", + "version": "6.22", "source": { "type": "git", "url": "https://github.com/sanmai/pipeline.git", - "reference": "f935e10ddcb758c89829e7b69cfb1dc2b2638518" + "reference": "fb8d0c23b4ef085315a36d397fafa052203020ce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sanmai/pipeline/zipball/f935e10ddcb758c89829e7b69cfb1dc2b2638518", - "reference": "f935e10ddcb758c89829e7b69cfb1dc2b2638518", + "url": "https://api.github.com/repos/sanmai/pipeline/zipball/fb8d0c23b4ef085315a36d397fafa052203020ce", + "reference": "fb8d0c23b4ef085315a36d397fafa052203020ce", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" + "php": ">=8.2" }, "require-dev": { "ergebnis/composer-normalize": "^2.8", - "friendsofphp/php-cs-fixer": "^2.16", - "infection/infection": ">=0.10.5", - "league/pipeline": "^1.0 || ^0.3", - "phan/phan": "^1.1 || ^2.0 || ^3.0", + "esi/phpunit-coverage-check": ">2", + "friendsofphp/php-cs-fixer": "^3.17", + "infection/infection": ">=0.30.3", + "league/pipeline": "^0.3 || ^1.0", "php-coveralls/php-coveralls": "^2.4.1", - "phpstan/phpstan": ">=0.10", - "phpunit/phpunit": "^7.4 || ^8.1 || ^9.4", - "vimeo/psalm": "^2.0 || ^3.0 || ^4.0" + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2", + "phpunit/phpunit": ">=9.4 <12", + "sanmai/phpstan-rules": "^0.3.0", + "sanmai/phpunit-double-colon-syntax": "^0.1.1", + "vimeo/psalm": ">=2" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "v5.x-dev" + "dev-main": "v6.x-dev" } }, "autoload": { @@ -4255,7 +4673,7 @@ "description": "General-purpose collections pipeline", "support": { "issues": "https://github.com/sanmai/pipeline/issues", - "source": "https://github.com/sanmai/pipeline/tree/v5.1.0" + "source": "https://github.com/sanmai/pipeline/tree/6.22" }, "funding": [ { @@ -4263,7 +4681,7 @@ "type": "github" } ], - "time": "2020-10-25T15:20:56+00:00" + "time": "2025-07-22T09:07:07+00:00" }, { "name": "sebastian/cli-parser", @@ -4324,23 +4742,23 @@ }, { "name": "sebastian/code-unit", - "version": "3.0.1", + "version": "3.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit.git", - "reference": "6bb7d09d6623567178cf54126afa9c2310114268" + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/6bb7d09d6623567178cf54126afa9c2310114268", - "reference": "6bb7d09d6623567178cf54126afa9c2310114268", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64", "shasum": "" }, "require": { "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^11.5" }, "type": "library", "extra": { @@ -4369,7 +4787,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/code-unit/issues", "security": "https://github.com/sebastianbergmann/code-unit/security/policy", - "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.1" + "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.3" }, "funding": [ { @@ -4377,20 +4795,20 @@ "type": "github" } ], - "time": "2024-07-03T04:44:28+00:00" + "time": "2025-03-19T07:56:08+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", - "version": "4.0.0", + "version": "4.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "df80c875d3e459b45c6039e4d9b71d4fbccae25d" + "reference": "183a9b2632194febd219bb9246eee421dad8d45e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/df80c875d3e459b45c6039e4d9b71d4fbccae25d", - "reference": "df80c875d3e459b45c6039e4d9b71d4fbccae25d", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/183a9b2632194febd219bb9246eee421dad8d45e", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e", "shasum": "" }, "require": { @@ -4425,7 +4843,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", "security": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/security/policy", - "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/4.0.0" + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/4.0.1" }, "funding": [ { @@ -4433,20 +4851,20 @@ "type": "github" } ], - "time": "2024-02-02T05:52:17+00:00" + "time": "2024-07-03T04:45:54+00:00" }, { "name": "sebastian/comparator", - "version": "6.0.1", + "version": "6.3.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "131942b86d3587291067a94f295498ab6ac79c20" + "reference": "85c77556683e6eee4323e4c5468641ca0237e2e8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/131942b86d3587291067a94f295498ab6ac79c20", - "reference": "131942b86d3587291067a94f295498ab6ac79c20", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/85c77556683e6eee4323e4c5468641ca0237e2e8", + "reference": "85c77556683e6eee4323e4c5468641ca0237e2e8", "shasum": "" }, "require": { @@ -4457,12 +4875,15 @@ "sebastian/exporter": "^6.0" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^11.4" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "6.0-dev" + "dev-main": "6.3-dev" } }, "autoload": { @@ -4502,28 +4923,40 @@ "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/6.0.1" + "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.2" }, "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": "2024-07-03T04:48:07+00:00" + "time": "2025-08-10T08:07:46+00:00" }, { "name": "sebastian/complexity", - "version": "4.0.0", + "version": "4.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "88a434ad86150e11a606ac4866b09130712671f0" + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/88a434ad86150e11a606ac4866b09130712671f0", - "reference": "88a434ad86150e11a606ac4866b09130712671f0", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/ee41d384ab1906c68852636b6de493846e13e5a0", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0", "shasum": "" }, "require": { @@ -4560,7 +4993,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/complexity/issues", "security": "https://github.com/sebastianbergmann/complexity/security/policy", - "source": "https://github.com/sebastianbergmann/complexity/tree/4.0.0" + "source": "https://github.com/sebastianbergmann/complexity/tree/4.0.1" }, "funding": [ { @@ -4568,7 +5001,7 @@ "type": "github" } ], - "time": "2024-02-02T05:55:19+00:00" + "time": "2024-07-03T04:49:50+00:00" }, { "name": "sebastian/diff", @@ -4639,23 +5072,23 @@ }, { "name": "sebastian/environment", - "version": "7.2.0", + "version": "7.2.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5" + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5", - "reference": "855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/a5c75038693ad2e8d4b6c15ba2403532647830c4", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4", "shasum": "" }, "require": { "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^11.3" }, "suggest": { "ext-posix": "*" @@ -4691,28 +5124,40 @@ "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", "security": "https://github.com/sebastianbergmann/environment/security/policy", - "source": "https://github.com/sebastianbergmann/environment/tree/7.2.0" + "source": "https://github.com/sebastianbergmann/environment/tree/7.2.1" }, "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/environment", + "type": "tidelift" } ], - "time": "2024-07-03T04:54:44+00:00" + "time": "2025-05-21T11:55:47+00:00" }, { "name": "sebastian/exporter", - "version": "6.1.3", + "version": "6.3.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "c414673eee9a8f9d51bbf8d61fc9e3ef1e85b20e" + "reference": "70a298763b40b213ec087c51c739efcaa90bcd74" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/c414673eee9a8f9d51bbf8d61fc9e3ef1e85b20e", - "reference": "c414673eee9a8f9d51bbf8d61fc9e3ef1e85b20e", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/70a298763b40b213ec087c51c739efcaa90bcd74", + "reference": "70a298763b40b213ec087c51c739efcaa90bcd74", "shasum": "" }, "require": { @@ -4721,12 +5166,12 @@ "sebastian/recursion-context": "^6.0" }, "require-dev": { - "phpunit/phpunit": "^11.2" + "phpunit/phpunit": "^11.3" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "6.1-dev" + "dev-main": "6.3-dev" } }, "autoload": { @@ -4769,15 +5214,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", "security": "https://github.com/sebastianbergmann/exporter/security/policy", - "source": "https://github.com/sebastianbergmann/exporter/tree/6.1.3" + "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.2" }, "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": "2024-07-03T04:56:19+00:00" + "time": "2025-09-24T06:12:51+00:00" }, { "name": "sebastian/global-state", @@ -4843,16 +5300,16 @@ }, { "name": "sebastian/lines-of-code", - "version": "3.0.0", + "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "376c5b3f6b43c78fdc049740bca76a7c846706c0" + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/376c5b3f6b43c78fdc049740bca76a7c846706c0", - "reference": "376c5b3f6b43c78fdc049740bca76a7c846706c0", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a", "shasum": "" }, "require": { @@ -4889,7 +5346,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/3.0.0" + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/3.0.1" }, "funding": [ { @@ -4897,7 +5354,7 @@ "type": "github" } ], - "time": "2024-02-02T06:00:36+00:00" + "time": "2024-07-03T04:58:38+00:00" }, { "name": "sebastian/object-enumerator", @@ -4959,16 +5416,16 @@ }, { "name": "sebastian/object-reflector", - "version": "4.0.0", + "version": "4.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "bb2a6255d30853425fd38f032eb64ced9f7f132d" + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/bb2a6255d30853425fd38f032eb64ced9f7f132d", - "reference": "bb2a6255d30853425fd38f032eb64ced9f7f132d", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9", "shasum": "" }, "require": { @@ -5003,7 +5460,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/object-reflector/issues", "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", - "source": "https://github.com/sebastianbergmann/object-reflector/tree/4.0.0" + "source": "https://github.com/sebastianbergmann/object-reflector/tree/4.0.1" }, "funding": [ { @@ -5011,27 +5468,27 @@ "type": "github" } ], - "time": "2024-02-02T06:02:18+00:00" + "time": "2024-07-03T05:01:32+00:00" }, { "name": "sebastian/recursion-context", - "version": "6.0.0", + "version": "6.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "b75224967b5a466925c6d54e68edd0edf8dd4ed4" + "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/b75224967b5a466925c6d54e68edd0edf8dd4ed4", - "reference": "b75224967b5a466925c6d54e68edd0edf8dd4ed4", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/f6458abbf32a6c8174f8f26261475dc133b3d9dc", + "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc", "shasum": "" }, "require": { "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^11.3" }, "type": "library", "extra": { @@ -5067,40 +5524,52 @@ "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.0" + "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.3" }, "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": "2024-02-02T06:08:48+00:00" + "time": "2025-08-13T04:42:22+00:00" }, { "name": "sebastian/type", - "version": "5.0.1", + "version": "5.1.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "fb6a6566f9589e86661291d13eba708cce5eb4aa" + "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/fb6a6566f9589e86661291d13eba708cce5eb4aa", - "reference": "fb6a6566f9589e86661291d13eba708cce5eb4aa", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/f77d2d4e78738c98d9a68d2596fe5e8fa380f449", + "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449", "shasum": "" }, "require": { "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^11.3" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.0-dev" + "dev-main": "5.1-dev" } }, "autoload": { @@ -5124,28 +5593,40 @@ "support": { "issues": "https://github.com/sebastianbergmann/type/issues", "security": "https://github.com/sebastianbergmann/type/security/policy", - "source": "https://github.com/sebastianbergmann/type/tree/5.0.1" + "source": "https://github.com/sebastianbergmann/type/tree/5.1.3" }, "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/type", + "type": "tidelift" } ], - "time": "2024-07-03T05:11:49+00:00" + "time": "2025-08-09T06:55:48+00:00" }, { "name": "sebastian/version", - "version": "5.0.1", + "version": "5.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/version.git", - "reference": "45c9debb7d039ce9b97de2f749c2cf5832a06ac4" + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/45c9debb7d039ce9b97de2f749c2cf5832a06ac4", - "reference": "45c9debb7d039ce9b97de2f749c2cf5832a06ac4", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c687e3387b99f5b03b6caa64c74b63e2936ff874", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874", "shasum": "" }, "require": { @@ -5178,7 +5659,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/version/issues", "security": "https://github.com/sebastianbergmann/version/security/policy", - "source": "https://github.com/sebastianbergmann/version/tree/5.0.1" + "source": "https://github.com/sebastianbergmann/version/tree/5.0.2" }, "funding": [ { @@ -5186,7 +5667,7 @@ "type": "github" } ], - "time": "2024-07-03T05:13:08+00:00" + "time": "2024-10-09T05:16:32+00:00" }, { "name": "seld/jsonlint", @@ -5254,16 +5735,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.10.0", + "version": "3.13.4", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "57e09801c2fbae2d257b8b75bebb3deeb7e9deb2" + "reference": "ad545ea9c1b7d270ce0fc9cbfb884161cd706119" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/57e09801c2fbae2d257b8b75bebb3deeb7e9deb2", - "reference": "57e09801c2fbae2d257b8b75bebb3deeb7e9deb2", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/ad545ea9c1b7d270ce0fc9cbfb884161cd706119", + "reference": "ad545ea9c1b7d270ce0fc9cbfb884161cd706119", "shasum": "" }, "require": { @@ -5328,9 +5809,65 @@ { "url": "https://opencollective.com/php_codesniffer", "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2025-09-05T05:47:09+00:00" + }, + { + "name": "staabm/side-effects-detector", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" } ], - "time": "2024-05-20T08:11:32+00:00" + "time": "2024-10-20T05:08:20+00:00" }, { "name": "symfony/finder", @@ -5473,16 +6010,16 @@ }, { "name": "symfony/process", - "version": "v7.3.3", + "version": "v7.3.4", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "32241012d521e2e8a9d713adb0812bb773b907f1" + "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/32241012d521e2e8a9d713adb0812bb773b907f1", - "reference": "32241012d521e2e8a9d713adb0812bb773b907f1", + "url": "https://api.github.com/repos/symfony/process/zipball/f24f8f316367b30810810d4eb30c543d7003ff3b", + "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b", "shasum": "" }, "require": { @@ -5514,7 +6051,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.3.3" + "source": "https://github.com/symfony/process/tree/v7.3.4" }, "funding": [ { @@ -5534,20 +6071,20 @@ "type": "tidelift" } ], - "time": "2025-08-18T09:42:54+00:00" + "time": "2025-09-11T10:12:26+00:00" }, { "name": "symfony/var-dumper", - "version": "v7.3.0", + "version": "v7.3.4", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "548f6760c54197b1084e1e5c71f6d9d523f2f78e" + "reference": "b8abe7daf2730d07dfd4b2ee1cecbf0dd2fbdabb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/548f6760c54197b1084e1e5c71f6d9d523f2f78e", - "reference": "548f6760c54197b1084e1e5c71f6d9d523f2f78e", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/b8abe7daf2730d07dfd4b2ee1cecbf0dd2fbdabb", + "reference": "b8abe7daf2730d07dfd4b2ee1cecbf0dd2fbdabb", "shasum": "" }, "require": { @@ -5559,7 +6096,6 @@ "symfony/console": "<6.4" }, "require-dev": { - "ext-iconv": "*", "symfony/console": "^6.4|^7.0", "symfony/http-kernel": "^6.4|^7.0", "symfony/process": "^6.4|^7.0", @@ -5602,7 +6138,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.3.0" + "source": "https://github.com/symfony/var-dumper/tree/v7.3.4" }, "funding": [ { @@ -5613,54 +6149,44 @@ "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-04-27T18:39:23+00:00" + "time": "2025-09-11T10:12:26+00:00" }, { "name": "thecodingmachine/safe", - "version": "v2.1.2", + "version": "v3.3.0", "source": { "type": "git", "url": "https://github.com/thecodingmachine/safe.git", - "reference": "e19610c7bb1f829bf0886f5a94a21924141212de" + "reference": "2cdd579eeaa2e78e51c7509b50cc9fb89a956236" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/e19610c7bb1f829bf0886f5a94a21924141212de", - "reference": "e19610c7bb1f829bf0886f5a94a21924141212de", + "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/2cdd579eeaa2e78e51c7509b50cc9fb89a956236", + "reference": "2cdd579eeaa2e78e51c7509b50cc9fb89a956236", "shasum": "" }, "require": { - "php": "^8.0" + "php": "^8.1" }, "require-dev": { - "phpstan/phpstan": "^0.12", - "phpunit/phpunit": "^9.5", - "squizlabs/php_codesniffer": "^3.2", - "thecodingmachine/phpstan-strict-rules": "^0.12" + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpstan/phpstan": "^2", + "phpunit/phpunit": "^10", + "squizlabs/php_codesniffer": "^3.2" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "0.1-dev" - } - }, "autoload": { "files": [ - "deprecated/apc.php", - "deprecated/array.php", - "deprecated/datetime.php", - "deprecated/libevent.php", - "deprecated/password.php", - "deprecated/mssql.php", - "deprecated/stats.php", - "deprecated/strings.php", "lib/special_cases.php", - "deprecated/mysqli.php", "generated/apache.php", "generated/apcu.php", "generated/array.php", @@ -5700,6 +6226,7 @@ "generated/mbstring.php", "generated/misc.php", "generated/mysql.php", + "generated/mysqli.php", "generated/network.php", "generated/oci8.php", "generated/opcache.php", @@ -5712,6 +6239,7 @@ "generated/ps.php", "generated/pspell.php", "generated/readline.php", + "generated/rnp.php", "generated/rpminfo.php", "generated/rrd.php", "generated/sem.php", @@ -5739,13 +6267,12 @@ "generated/zip.php", "generated/zlib.php" ], - "psr-4": { - "Safe\\": [ - "lib/", - "deprecated/", - "generated/" - ] - } + "classmap": [ + "lib/DateTime.php", + "lib/DateTimeImmutable.php", + "lib/Exceptions/", + "generated/Exceptions/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -5754,22 +6281,36 @@ "description": "PHP core functions that throw exceptions instead of returning FALSE on error", "support": { "issues": "https://github.com/thecodingmachine/safe/issues", - "source": "https://github.com/thecodingmachine/safe/tree/v2.1.2" + "source": "https://github.com/thecodingmachine/safe/tree/v3.3.0" }, - "time": "2022-02-01T21:08:44+00:00" + "funding": [ + { + "url": "https://github.com/OskarStark", + "type": "github" + }, + { + "url": "https://github.com/shish", + "type": "github" + }, + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2025-05-14T06:15:44+00:00" }, { "name": "theseer/tokenizer", - "version": "1.2.0", + "version": "1.2.3", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "75a63c33a8577608444246075ea0af0d052e452a" + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/75a63c33a8577608444246075ea0af0d052e452a", - "reference": "75a63c33a8577608444246075ea0af0d052e452a", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", "shasum": "" }, "require": { @@ -5798,7 +6339,7 @@ "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/master" + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" }, "funding": [ { @@ -5806,7 +6347,7 @@ "type": "github" } ], - "time": "2020-07-12T23:59:07+00:00" + "time": "2024-03-03T12:36:25+00:00" }, { "name": "webmozart/assert", @@ -5926,6 +6467,8 @@ "platform": { "php": "^8.1" }, - "platform-dev": {}, + "platform-dev": { + "ext-dom": "*" + }, "plugin-api-version": "2.6.0" } diff --git a/config.yml b/config.yml index 5043680..8e66615 100644 --- a/config.yml +++ b/config.yml @@ -2,7 +2,7 @@ cognitive: excludeFilePatterns: excludePatterns: scoreThreshold: 0.5 - showOnlyMethodsExceedingThreshold: false + showOnlyMethodsExceedingThreshold: true showHalsteadComplexity: false showCyclomaticComplexity: false showDetailedCognitiveMetrics: true @@ -40,3 +40,8 @@ cognitive: threshold: 1 scale: 1.0 enabled: true + +cache: + enabled: true + directory: './.phpcca.cache' + compression: true diff --git a/src/Application.php b/src/Application.php index 980bb4e..5032f2f 100644 --- a/src/Application.php +++ b/src/Application.php @@ -4,6 +4,7 @@ namespace Phauthentic\CognitiveCodeAnalysis; +use Phauthentic\CognitiveCodeAnalysis\Business\Cache\FileCacheService; use Phauthentic\CognitiveCodeAnalysis\Business\Churn\ChangeCounter\ChangeCounterFactory; use Phauthentic\CognitiveCodeAnalysis\Business\Churn\ChurnCalculator; use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Baseline; @@ -44,6 +45,7 @@ use Symfony\Component\Messenger\MessageBus; use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Messenger\Middleware\HandleMessageMiddleware; +use Psr\Cache\CacheItemPoolInterface; /** * @@ -82,6 +84,12 @@ private function registerServices(): void $this->containerBuilder->register(ConfigService::class, ConfigService::class) ->setPublic(true); + $this->containerBuilder->register(CacheItemPoolInterface::class, FileCacheService::class) + ->setArguments([ + './.phpcca.cache' // Default cache directory, can be overridden by config + ]) + ->setPublic(true); + $this->containerBuilder->register(ChurnTextRenderer::class, ChurnTextRenderer::class) ->setArguments([ new Reference(OutputInterface::class) @@ -165,7 +173,8 @@ private function bootstrapMetricsCollectors(): void new Reference(Parser::class), new Reference(DirectoryScanner::class), new Reference(ConfigService::class), - new Reference(MessageBusInterface::class) + new Reference(MessageBusInterface::class), + new Reference(CacheItemPoolInterface::class) ]) ->setPublic(true); } diff --git a/src/Business/Cache/CacheItem.php b/src/Business/Cache/CacheItem.php new file mode 100644 index 0000000..ce680cf --- /dev/null +++ b/src/Business/Cache/CacheItem.php @@ -0,0 +1,72 @@ +key = $key; + $this->value = $value; + $this->isHit = $isHit; + } + + public function getKey(): string + { + return $this->key; + } + + public function get(): mixed + { + return $this->value; + } + + public function set(mixed $value): static + { + $this->value = $value; + return $this; + } + + public function isHit(): bool + { + return $this->isHit; + } + + public function setExpiration(?int $expiration): static + { + // Not used in this file-based cache implementation + // Cache validity is determined by file modification time and config hash + return $this; + } + + public function getExpiration(): ?int + { + // Not used in this file-based cache implementation + return null; + } + + public function expiresAt(?\DateTimeInterface $expiration): static + { + // Not used in this file-based cache implementation + // Cache validity is determined by file modification time and config hash + return $this; + } + + public function expiresAfter(int|\DateInterval|null $time): static + { + // Not used in this file-based cache implementation + // Cache validity is determined by file modification time and config hash + return $this; + } +} diff --git a/src/Business/Cache/Exception/CacheException.php b/src/Business/Cache/Exception/CacheException.php new file mode 100644 index 0000000..50d44c8 --- /dev/null +++ b/src/Business/Cache/Exception/CacheException.php @@ -0,0 +1,14 @@ +cacheDirectory = rtrim($cacheDirectory, '/'); + $this->ensureCacheDirectory(); + } + + public function getItem(string $key): CacheItemInterface + { + $filePath = $this->getCacheFilePath($key); + + if (!file_exists($filePath)) { + return new CacheItem($key, null, false); + } + + $data = $this->loadCacheData($filePath); + if ($data === null) { + return new CacheItem($key, null, false); + } + + return new CacheItem($key, $data, true); + } + + public function getItems(array $keys = []): iterable + { + $items = []; + foreach ($keys as $key) { + $items[$key] = $this->getItem($key); + } + return $items; + } + + public function hasItem(string $key): bool + { + $filePath = $this->getCacheFilePath($key); + return file_exists($filePath) && $this->loadCacheData($filePath) !== null; + } + + public function clear(): bool + { + try { + $this->removeDirectory($this->cacheDirectory); + $this->ensureCacheDirectory(); + return true; + } catch (CacheException $e) { + return false; + } + } + + public function deleteItem(string $key): bool + { + $filePath = $this->getCacheFilePath($key); + + if (file_exists($filePath)) { + return unlink($filePath); + } + + return true; + } + + public function deleteItems(array $keys): bool + { + $success = true; + foreach ($keys as $key) { + if (!$this->deleteItem($key)) { + $success = false; + } + } + return $success; + } + + public function save(CacheItemInterface $item): bool + { + if (!$item instanceof CacheItem) { + return false; + } + + $filePath = $this->getCacheFilePath($item->getKey()); + $data = $item->get(); + + if ($data === null) { + return $this->deleteItem($item->getKey()); + } + + return $this->saveCacheData($filePath, $data); + } + + public function saveDeferred(CacheItemInterface $item): bool + { + if (!$item instanceof CacheItem) { + return false; + } + + $this->deferred[] = $item; + return true; + } + + public function commit(): bool + { + $success = true; + foreach ($this->deferred as $item) { + if (!$this->save($item)) { + $success = false; + } + } + $this->deferred = []; + return $success; + } + + private function ensureCacheDirectory(): void + { + if (!is_dir($this->cacheDirectory)) { + if (!mkdir($this->cacheDirectory, 0755, true)) { + throw new CacheException("Failed to create cache directory: {$this->cacheDirectory}"); + } + } + } + + private function getCacheFilePath(string $key): string + { + // Create subdirectories to avoid too many files in one directory + $hash = md5($key); + $subDir = substr($hash, 0, 2); + $dir = $this->cacheDirectory . '/' . $subDir; + + if (!is_dir($dir)) { + if (!mkdir($dir, 0755, true)) { + throw new CacheException("Failed to create cache subdirectory: {$dir}"); + } + } + + return $dir . '/' . $hash . '.cache'; + } + + private function loadCacheData(string $filePath): ?array + { + $content = file_get_contents($filePath); + if ($content === false) { + return null; + } + + $data = json_decode($content, true); + if ($data === null) { + return null; + } + + // Data is stored without compression for now + + return $data; + } + + private function saveCacheData(string $filePath, array $data): bool + { + // Store data without compression for now (compression can be added later) + // This ensures cache works reliably + + // Sanitize data to ensure valid UTF-8 encoding + $data = $this->sanitizeUtf8($data); + + $json = json_encode($data, JSON_PRETTY_PRINT); + if ($json === false) { + return false; + } + + $dir = dirname($filePath); + if (!is_dir($dir)) { + if (!mkdir($dir, 0755, true)) { + return false; + } + } + + $result = file_put_contents($filePath, $json); + return $result !== false; + } + + /** + * Recursively sanitize UTF-8 data to ensure valid encoding + */ + private function sanitizeUtf8(mixed $data): mixed + { + if (is_string($data)) { + // Remove or replace invalid UTF-8 characters + return mb_convert_encoding($data, 'UTF-8', 'UTF-8'); + } + + if (is_array($data)) { + $sanitized = []; + foreach ($data as $key => $value) { + $sanitizedKey = is_string($key) ? mb_convert_encoding($key, 'UTF-8', 'UTF-8') : $key; + $sanitized[$sanitizedKey] = $this->sanitizeUtf8($value); + } + return $sanitized; + } + + if (is_object($data)) { + // Convert objects to arrays for sanitization + $array = (array) $data; + $sanitized = $this->sanitizeUtf8($array); + return (object) $sanitized; + } + + return $data; + } + + private function removeDirectory(string $dir): void + { + if (!is_dir($dir)) { + return; + } + + $files = array_diff(scandir($dir), ['.', '..']); + foreach ($files as $file) { + $path = $dir . '/' . $file; + if (is_dir($path)) { + $this->removeDirectory($path); + } else { + unlink($path); + } + } + rmdir($dir); + } +} diff --git a/src/Business/Cognitive/CognitiveMetricsCollector.php b/src/Business/Cognitive/CognitiveMetricsCollector.php index ffbf61c..ab59e77 100644 --- a/src/Business/Cognitive/CognitiveMetricsCollector.php +++ b/src/Business/Cognitive/CognitiveMetricsCollector.php @@ -15,6 +15,7 @@ use Symfony\Component\Messenger\Exception\ExceptionInterface; use Symfony\Component\Messenger\MessageBusInterface; use Throwable; +use Psr\Cache\CacheItemPoolInterface; /** * CognitiveMetricsCollector class that collects cognitive metrics from source files @@ -31,6 +32,7 @@ public function __construct( protected readonly DirectoryScanner $directoryScanner, protected readonly ConfigService $configService, protected readonly MessageBusInterface $messageBus, + protected readonly ?CacheItemPoolInterface $cachePool = null, ) { } @@ -94,29 +96,60 @@ private function findMetrics(iterable $files): CognitiveMetricsCollection { $metricsCollection = new CognitiveMetricsCollection(); $fileCount = 0; + $config = $this->configService->getConfig(); + $configHash = $this->generateConfigHash($config); foreach ($files as $file) { - try { - $metrics = $this->parser->parse( - $this->getCodeFromFile($file) - ); - - // Store ignored items from the parser - $this->ignoredItems = $this->parser->getIgnored(); - - $fileCount++; + $metrics = null; + $useCache = $this->cachePool !== null && $config->cache?->enabled === true; + $cacheItem = null; + + + if ($useCache) { + $cacheKey = $this->generateCacheKey($file, $configHash); + $cacheItem = $this->cachePool->getItem($cacheKey); + + if ($cacheItem->isHit()) { + // Use cached result + $cachedData = $cacheItem->get(); + $metrics = $cachedData['analysis_result']; + $this->ignoredItems = $cachedData['ignored_items']; + + $this->messageBus->dispatch(new FileProcessed($file)); + } + } - // Clear memory periodically to prevent memory leaks - if ($fileCount % 50 === 0) { - $this->parser->clearStaticCaches(); - gc_collect_cycles(); + if ($metrics === null) { + // Parse file and cache result + try { + $metrics = $this->parser->parse( + $this->getCodeFromFile($file) + ); + + // Store ignored items from the parser + $this->ignoredItems = $this->parser->getIgnored(); + + $fileCount++; + + // Clear memory periodically to prevent memory leaks + if ($fileCount % 50 === 0) { + $this->parser->clearStaticCaches(); + gc_collect_cycles(); + } + + // Cache the result if caching is enabled + if ($useCache && $cacheItem !== null) { + $this->cacheResult($cacheItem, $file, $metrics, $configHash); + } + } catch (Throwable $exception) { + $this->messageBus->dispatch(new ParserFailed( + $file, + $exception + )); + continue; } - } catch (Throwable $exception) { - $this->messageBus->dispatch(new ParserFailed( - $file, - $exception - )); - continue; + + $this->messageBus->dispatch(new FileProcessed($file)); } $filename = $file->getRealPath(); @@ -133,10 +166,6 @@ private function findMetrics(iterable $files): CognitiveMetricsCollection $metricsCollection, $filename ); - - $this->messageBus->dispatch(new FileProcessed( - $file, - )); } return $metricsCollection; @@ -255,4 +284,68 @@ private function getProjectRoot(): ?string return null; } + + /** + * Generate cache key for a file based on path, modification time, and config hash + */ + private function generateCacheKey(SplFileInfo $file, string $configHash): string + { + $filePath = $file->getRealPath(); + $fileMtime = $file->getMTime(); + + return 'phpcca_' . md5($filePath . '|' . $fileMtime . '|' . $configHash); + } + + /** + * Generate configuration hash for cache invalidation + */ + private function generateConfigHash(CognitiveConfig $config): string + { + return md5(serialize($this->getConfigAsArray($config))); + } + + /** + * Cache the analysis result for a file + */ + private function cacheResult($cacheItem, SplFileInfo $file, array $metrics, string $configHash): void + { + if (!$this->cachePool) { + return; + } + + $data = [ + 'version' => '1.0', + 'file_path' => $file->getRealPath(), + 'file_mtime' => $file->getMTime(), + 'config_hash' => $configHash, + 'analysis_result' => $metrics, + 'ignored_items' => $this->ignoredItems, + 'cached_at' => time() + ]; + + $cacheItem->set($data); + $this->cachePool->save($cacheItem); + } + + /** + * Get configuration as array for serialization + */ + private function getConfigAsArray(CognitiveConfig $config): array + { + return [ + 'excludeFilePatterns' => $config->excludeFilePatterns, + 'excludePatterns' => $config->excludePatterns, + 'scoreThreshold' => $config->scoreThreshold, + 'showOnlyMethodsExceedingThreshold' => $config->showOnlyMethodsExceedingThreshold, + 'showHalsteadComplexity' => $config->showHalsteadComplexity, + 'showCyclomaticComplexity' => $config->showCyclomaticComplexity, + 'groupByClass' => $config->groupByClass, + 'showDetailedCognitiveMetrics' => $config->showDetailedCognitiveMetrics, + 'cache' => $config->cache ? [ + 'enabled' => $config->cache->enabled, + 'directory' => $config->cache->directory, + 'compression' => $config->cache->compression, + ] : null, + ]; + } } diff --git a/src/Business/MetricsFacade.php b/src/Business/MetricsFacade.php index 933b03a..b9c3853 100644 --- a/src/Business/MetricsFacade.php +++ b/src/Business/MetricsFacade.php @@ -188,4 +188,34 @@ public function exportMetricsReport( $exporter = $this->getCognitiveExporterFactory()->create($reportType); $exporter->export($metricsCollection, $filename); } + + /** + * Clear all cached analysis results + */ + public function clearCache(): void + { + // This would need to be implemented to clear the cache + // For now, we'll add a placeholder + throw new \RuntimeException('Cache clearing not yet implemented'); + } + + /** + * Set cache directory override + */ + public function setCacheDirectory(string $cacheDir): void + { + // This would need to be implemented to override cache directory + // For now, we'll add a placeholder + throw new \RuntimeException('Cache directory override not yet implemented'); + } + + /** + * Disable caching for this run + */ + public function disableCache(): void + { + // This would need to be implemented to disable caching + // For now, we'll add a placeholder + throw new \RuntimeException('Cache disabling not yet implemented'); + } } diff --git a/src/Command/CognitiveMetricsCommand.php b/src/Command/CognitiveMetricsCommand.php index d1e9671..2b8b438 100644 --- a/src/Command/CognitiveMetricsCommand.php +++ b/src/Command/CognitiveMetricsCommand.php @@ -15,6 +15,7 @@ use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** @@ -33,6 +34,9 @@ class CognitiveMetricsCommand extends Command public const OPTION_DEBUG = 'debug'; public const OPTION_SORT_BY = 'sort-by'; public const OPTION_SORT_ORDER = 'sort-order'; + public const OPTION_CLEAR_CACHE = 'clear-cache'; + public const OPTION_NO_CACHE = 'no-cache'; + public const OPTION_CACHE_DIR = 'cache-dir'; private const ARGUMENT_PATH = 'path'; public function __construct( @@ -97,6 +101,21 @@ protected function configure(): void mode: InputArgument::OPTIONAL, description: 'Enables debug output', default: false + ) + ->addOption( + name: self::OPTION_CLEAR_CACHE, + mode: InputOption::VALUE_NONE, + description: 'Clear all cached analysis results' + ) + ->addOption( + name: self::OPTION_NO_CACHE, + mode: InputOption::VALUE_NONE, + description: 'Disable caching for this run' + ) + ->addOption( + name: self::OPTION_CACHE_DIR, + mode: InputArgument::OPTIONAL, + description: 'Override default cache directory', ); } @@ -113,11 +132,34 @@ protected function execute(InputInterface $input, OutputInterface $output): int $pathInput = $input->getArgument(self::ARGUMENT_PATH); $paths = $this->parsePaths($pathInput); + // Handle cache options + if ($input->getOption(self::OPTION_CLEAR_CACHE)) { + try { + $this->metricsFacade->clearCache(); + $output->writeln('Cache cleared successfully.'); + } catch (Exception $e) { + $output->writeln('Failed to clear cache: ' . $e->getMessage() . ''); + } + return Command::SUCCESS; + } + $configFile = $input->getOption(self::OPTION_CONFIG_FILE); if ($configFile && !$this->loadConfiguration($configFile, $output)) { return Command::FAILURE; } + // Handle cache directory override + $cacheDir = $input->getOption(self::OPTION_CACHE_DIR); + if ($cacheDir) { + $this->metricsFacade->setCacheDirectory($cacheDir); + } + + // Handle no-cache option + $noCache = $input->getOption(self::OPTION_NO_CACHE); + if ($noCache) { + $this->metricsFacade->disableCache(); + } + $metricsCollection = $this->metricsFacade->getCognitiveMetricsFromPaths($paths); $this->handleBaseLine($input, $metricsCollection); @@ -195,4 +237,5 @@ private function loadConfiguration(string $configFile, OutputInterface $output): return false; } } + } diff --git a/src/Config/CacheConfig.php b/src/Config/CacheConfig.php new file mode 100644 index 0000000..129f1a9 --- /dev/null +++ b/src/Config/CacheConfig.php @@ -0,0 +1,18 @@ +end() ->end() ->end() + ->arrayNode('cache') + ->children() + ->booleanNode('enabled') + ->defaultValue(true) + ->end() + ->scalarNode('directory') + ->defaultValue('./.phpcca.cache') + ->end() + ->booleanNode('compression') + ->defaultValue(true) + ->end() + ->end() + ->end() ->end(); return $treeBuilder; From 3889f20a516c3df14e2a459d680bcfdd3c359d29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Kr=C3=A4mer?= Date: Tue, 7 Oct 2025 00:35:27 +0200 Subject: [PATCH 02/17] Refactoring the Cache --- config.yml | 11 +- src/Application.php | 5 +- src/Business/Cache/FileCache.php | 239 ++++++++++++++++++++++++ src/Business/Cache/FileCacheService.php | 118 ++++++------ src/Business/MetricsFacade.php | 44 ++--- src/Config/CacheConfig.php | 6 +- src/Config/ConfigFactory.php | 8 +- src/Config/ConfigLoader.php | 24 +-- 8 files changed, 345 insertions(+), 110 deletions(-) create mode 100644 src/Business/Cache/FileCache.php diff --git a/config.yml b/config.yml index 8e66615..3c45091 100644 --- a/config.yml +++ b/config.yml @@ -2,7 +2,7 @@ cognitive: excludeFilePatterns: excludePatterns: scoreThreshold: 0.5 - showOnlyMethodsExceedingThreshold: true + showOnlyMethodsExceedingThreshold: false showHalsteadComplexity: false showCyclomaticComplexity: false showDetailedCognitiveMetrics: true @@ -40,8 +40,7 @@ cognitive: threshold: 1 scale: 1.0 enabled: true - -cache: - enabled: true - directory: './.phpcca.cache' - compression: true + cache: + enabled: true + directory: './.phpcca.cache' + compression: true diff --git a/src/Application.php b/src/Application.php index 5032f2f..e44a561 100644 --- a/src/Application.php +++ b/src/Application.php @@ -4,7 +4,7 @@ namespace Phauthentic\CognitiveCodeAnalysis; -use Phauthentic\CognitiveCodeAnalysis\Business\Cache\FileCacheService; +use Phauthentic\CognitiveCodeAnalysis\Business\Cache\FileCache; use Phauthentic\CognitiveCodeAnalysis\Business\Churn\ChangeCounter\ChangeCounterFactory; use Phauthentic\CognitiveCodeAnalysis\Business\Churn\ChurnCalculator; use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Baseline; @@ -84,7 +84,7 @@ private function registerServices(): void $this->containerBuilder->register(ConfigService::class, ConfigService::class) ->setPublic(true); - $this->containerBuilder->register(CacheItemPoolInterface::class, FileCacheService::class) + $this->containerBuilder->register(CacheItemPoolInterface::class, FileCache::class) ->setArguments([ './.phpcca.cache' // Default cache directory, can be overridden by config ]) @@ -218,6 +218,7 @@ private function registerMetricsFacade(): void new Reference(ConfigService::class), new Reference(ChurnCalculator::class), new Reference(ChangeCounterFactory::class), + new Reference(CacheItemPoolInterface::class), ]) ->setPublic(true); } diff --git a/src/Business/Cache/FileCache.php b/src/Business/Cache/FileCache.php new file mode 100644 index 0000000..aafa289 --- /dev/null +++ b/src/Business/Cache/FileCache.php @@ -0,0 +1,239 @@ +cacheDirectory = rtrim($cacheDirectory, '/'); + $this->ensureCacheDirectory(); + } + + public function getItem(string $key): CacheItemInterface + { + $filePath = $this->getCacheFilePath($key); + + if (!file_exists($filePath)) { + return new CacheItem($key, null, false); + } + + $data = $this->loadCacheData($filePath); + if ($data === null) { + return new CacheItem($key, null, false); + } + + return new CacheItem($key, $data, true); + } + + public function getItems(array $keys = []): iterable + { + $items = []; + foreach ($keys as $key) { + $items[$key] = $this->getItem($key); + } + return $items; + } + + public function hasItem(string $key): bool + { + $filePath = $this->getCacheFilePath($key); + return file_exists($filePath) && $this->loadCacheData($filePath) !== null; + } + + public function clear(): bool + { + try { + $this->removeDirectory($this->cacheDirectory); + $this->ensureCacheDirectory(); + return true; + } catch (CacheException $e) { + return false; + } + } + + public function deleteItem(string $key): bool + { + $filePath = $this->getCacheFilePath($key); + + if (file_exists($filePath)) { + return unlink($filePath); + } + + return true; + } + + public function deleteItems(array $keys): bool + { + $success = true; + foreach ($keys as $key) { + if (!$this->deleteItem($key)) { + $success = false; + } + } + return $success; + } + + public function save(CacheItemInterface $item): bool + { + if (!$item instanceof CacheItem) { + return false; + } + + $filePath = $this->getCacheFilePath($item->getKey()); + $data = $item->get(); + + if ($data === null) { + return $this->deleteItem($item->getKey()); + } + + return $this->saveCacheData($filePath, $data); + } + + public function saveDeferred(CacheItemInterface $item): bool + { + if (!$item instanceof CacheItem) { + return false; + } + + $this->deferred[] = $item; + return true; + } + + public function commit(): bool + { + $success = true; + foreach ($this->deferred as $item) { + if (!$this->save($item)) { + $success = false; + } + } + $this->deferred = []; + return $success; + } + + private function ensureCacheDirectory(): void + { + if (!is_dir($this->cacheDirectory)) { + if (!mkdir($this->cacheDirectory, 0755, true)) { + throw new CacheException("Failed to create cache directory: {$this->cacheDirectory}"); + } + } + } + + private function getCacheFilePath(string $key): string + { + // Create subdirectories to avoid too many files in one directory + $hash = md5($key); + $subDir = substr($hash, 0, 2); + $dir = $this->cacheDirectory . '/' . $subDir; + + if (!is_dir($dir)) { + if (!mkdir($dir, 0755, true)) { + throw new CacheException("Failed to create cache subdirectory: {$dir}"); + } + } + + return $dir . '/' . $hash . '.cache'; + } + + private function loadCacheData(string $filePath): ?array + { + $content = file_get_contents($filePath); + if ($content === false) { + return null; + } + + $data = json_decode($content, true); + if ($data === null) { + return null; + } + + // Data is stored without compression for now + + return $data; + } + + private function saveCacheData(string $filePath, array $data): bool + { + // Store data without compression for now (compression can be added later) + // This ensures cache works reliably + + // Sanitize data to ensure valid UTF-8 encoding + $data = $this->sanitizeUtf8($data); + + $json = json_encode($data, JSON_PRETTY_PRINT); + if ($json === false) { + return false; + } + + $dir = dirname($filePath); + if (!is_dir($dir)) { + if (!mkdir($dir, 0755, true)) { + return false; + } + } + + $result = file_put_contents($filePath, $json); + return $result !== false; + } + + /** + * Recursively sanitize UTF-8 data to ensure valid encoding + */ + private function sanitizeUtf8(mixed $data): mixed + { + if (is_string($data)) { + // Remove or replace invalid UTF-8 characters + return mb_convert_encoding($data, 'UTF-8', 'UTF-8'); + } + + if (is_array($data)) { + $sanitized = []; + foreach ($data as $key => $value) { + $sanitizedKey = is_string($key) ? mb_convert_encoding($key, 'UTF-8', 'UTF-8') : $key; + $sanitized[$sanitizedKey] = $this->sanitizeUtf8($value); + } + return $sanitized; + } + + if (is_object($data)) { + // Convert objects to arrays for sanitization + $array = (array) $data; + $sanitized = $this->sanitizeUtf8($array); + return (object) $sanitized; + } + + return $data; + } + + private function removeDirectory(string $dir): void + { + if (!is_dir($dir)) { + return; + } + + $files = array_diff(scandir($dir), ['.', '..']); + foreach ($files as $file) { + $path = $dir . '/' . $file; + if (is_dir($path)) { + $this->removeDirectory($path); + } else { + unlink($path); + } + } + rmdir($dir); + } +} diff --git a/src/Business/Cache/FileCacheService.php b/src/Business/Cache/FileCacheService.php index c32b773..cb0deb4 100644 --- a/src/Business/Cache/FileCacheService.php +++ b/src/Business/Cache/FileCacheService.php @@ -4,13 +4,10 @@ namespace Phauthentic\CognitiveCodeAnalysis\Business\Cache; -use Phauthentic\CognitiveCodeAnalysis\Business\Cache\Exception\CacheException; -use Psr\Cache\CacheItemInterface; use Psr\Cache\CacheItemPoolInterface; +use Psr\Cache\CacheItemInterface; +use Phauthentic\CognitiveCodeAnalysis\Business\Cache\Exception\CacheException; -/** - * PSR-6 File-based Cache implementation with compression support - */ class FileCacheService implements CacheItemPoolInterface { private string $cacheDirectory; @@ -29,12 +26,18 @@ public function getItem(string $key): CacheItemInterface if (!file_exists($filePath)) { return new CacheItem($key, null, false); } - + $data = $this->loadCacheData($filePath); if ($data === null) { return new CacheItem($key, null, false); } - + + // Check if cache is still valid (file hasn't changed and config matches) + if (!$this->isCacheValid($data, $filePath)) { + unlink($filePath); + return new CacheItem($key, null, false); + } + return new CacheItem($key, $data, true); } @@ -49,29 +52,38 @@ public function getItems(array $keys = []): iterable public function hasItem(string $key): bool { - $filePath = $this->getCacheFilePath($key); - return file_exists($filePath) && $this->loadCacheData($filePath) !== null; + return $this->getItem($key)->isHit(); } public function clear(): bool { - try { - $this->removeDirectory($this->cacheDirectory); - $this->ensureCacheDirectory(); + if (!is_dir($this->cacheDirectory)) { return true; - } catch (CacheException $e) { - return false; } + + $success = true; + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($this->cacheDirectory, \RecursiveDirectoryIterator::SKIP_DOTS), + \RecursiveIteratorIterator::CHILD_FIRST + ); + + foreach ($iterator as $file) { + if ($file->isDir()) { + $success = rmdir($file->getRealPath()) && $success; + } else { + $success = unlink($file->getRealPath()) && $success; + } + } + + return $success; } public function deleteItem(string $key): bool { $filePath = $this->getCacheFilePath($key); - if (file_exists($filePath)) { return unlink($filePath); } - return true; } @@ -79,35 +91,25 @@ public function deleteItems(array $keys): bool { $success = true; foreach ($keys as $key) { - if (!$this->deleteItem($key)) { - $success = false; - } + $success = $this->deleteItem($key) && $success; } return $success; } public function save(CacheItemInterface $item): bool { - if (!$item instanceof CacheItem) { - return false; - } - $filePath = $this->getCacheFilePath($item->getKey()); $data = $item->get(); if ($data === null) { - return $this->deleteItem($item->getKey()); + return true; } - + return $this->saveCacheData($filePath, $data); } public function saveDeferred(CacheItemInterface $item): bool { - if (!$item instanceof CacheItem) { - return false; - } - $this->deferred[] = $item; return true; } @@ -116,9 +118,7 @@ public function commit(): bool { $success = true; foreach ($this->deferred as $item) { - if (!$this->save($item)) { - $success = false; - } + $success = $this->save($item) && $success; } $this->deferred = []; return $success; @@ -141,9 +141,7 @@ private function getCacheFilePath(string $key): string $dir = $this->cacheDirectory . '/' . $subDir; if (!is_dir($dir)) { - if (!mkdir($dir, 0755, true)) { - throw new CacheException("Failed to create cache subdirectory: {$dir}"); - } + mkdir($dir, 0755, true); } return $dir . '/' . $hash . '.cache'; @@ -162,7 +160,6 @@ private function loadCacheData(string $filePath): ?array } // Data is stored without compression for now - return $data; } @@ -190,6 +187,28 @@ private function saveCacheData(string $filePath, array $data): bool return $result !== false; } + /** + * Check if cache data is still valid + */ + private function isCacheValid(array $data, string $filePath): bool + { + // Check if file modification time matches + if (isset($data['file_mtime']) && file_exists($data['file_path'])) { + $currentMtime = filemtime($data['file_path']); + if ($currentMtime !== $data['file_mtime']) { + return false; + } + } + + // Check if config hash matches (if present) + if (isset($data['config_hash'])) { + // This would need to be implemented to check current config hash + // For now, we'll assume it's valid + } + + return true; + } + /** * Recursively sanitize UTF-8 data to ensure valid encoding */ @@ -211,29 +230,14 @@ private function sanitizeUtf8(mixed $data): mixed if (is_object($data)) { // Convert objects to arrays for sanitization - $array = (array) $data; - $sanitized = $this->sanitizeUtf8($array); - return (object) $sanitized; + $sanitized = []; + foreach ((array) $data as $key => $value) { + $sanitizedKey = is_string($key) ? mb_convert_encoding($key, 'UTF-8', 'UTF-8') : $key; + $sanitized[$sanitizedKey] = $this->sanitizeUtf8($value); + } + return $sanitized; } return $data; } - - private function removeDirectory(string $dir): void - { - if (!is_dir($dir)) { - return; - } - - $files = array_diff(scandir($dir), ['.', '..']); - foreach ($files as $file) { - $path = $dir . '/' . $file; - if (is_dir($path)) { - $this->removeDirectory($path); - } else { - unlink($path); - } - } - rmdir($dir); - } } diff --git a/src/Business/MetricsFacade.php b/src/Business/MetricsFacade.php index b9c3853..7db0512 100644 --- a/src/Business/MetricsFacade.php +++ b/src/Business/MetricsFacade.php @@ -4,7 +4,6 @@ namespace Phauthentic\CognitiveCodeAnalysis\Business; -use JsonException; use Phauthentic\CognitiveCodeAnalysis\Business\Churn\ChangeCounter\ChangeCounterFactory; use Phauthentic\CognitiveCodeAnalysis\Business\Churn\ChurnCalculator; use Phauthentic\CognitiveCodeAnalysis\Business\Churn\Exporter\ChurnExporterFactory; @@ -13,9 +12,9 @@ use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\CognitiveMetricsCollector; use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Exporter\CognitiveExporterFactory; use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\ScoreCalculator; -use Phauthentic\CognitiveCodeAnalysis\CognitiveAnalysisException; use Phauthentic\CognitiveCodeAnalysis\Config\CognitiveConfig; use Phauthentic\CognitiveCodeAnalysis\Config\ConfigService; +use Psr\Cache\CacheItemPoolInterface; /** * Facade class for collecting and managing code quality metrics. @@ -33,7 +32,8 @@ public function __construct( private readonly ScoreCalculator $scoreCalculator, private readonly ConfigService $configService, private readonly ChurnCalculator $churnCalculator, - private readonly ChangeCounterFactory $changeCounterFactory + private readonly ChangeCounterFactory $changeCounterFactory, + private readonly ?CacheItemPoolInterface $cachePool = null ) { $this->loadConfig(__DIR__ . '/../../config.yml'); } @@ -85,10 +85,12 @@ public function getCognitiveMetrics(string $path): CognitiveMetricsCollection */ public function getCognitiveMetricsFromPaths(array $paths): CognitiveMetricsCollection { - $metricsCollection = $this->cognitiveMetricsCollector->collectFromPaths($paths, $this->configService->getConfig()); + $config = $this->configService->getConfig(); + + $metricsCollection = $this->cognitiveMetricsCollector->collectFromPaths($paths, $config); foreach ($metricsCollection as $metric) { - $this->scoreCalculator->calculate($metric, $this->configService->getConfig()); + $this->scoreCalculator->calculate($metric, $config); } return $metricsCollection; @@ -156,16 +158,6 @@ public function getIgnoredClasses(): array return $this->cognitiveMetricsCollector->getIgnoredClasses(); } - /** - * Get ignored methods from the last metrics collection. - * - * @return array Array of ignored method keys (ClassName::methodName) - */ - public function getIgnoredMethods(): array - { - return $this->cognitiveMetricsCollector->getIgnoredMethods(); - } - /** * @param array> $classes */ @@ -174,8 +166,9 @@ public function exportChurnReport( string $reportType, string $filename ): void { - $exporter = $this->getChurnExporterFactory()->create($reportType); - $exporter->export($classes, $filename); + $this->getChurnExporterFactory() + ->create($reportType) + ->export($classes, $filename); } /** @@ -194,9 +187,12 @@ public function exportMetricsReport( */ public function clearCache(): void { - // This would need to be implemented to clear the cache - // For now, we'll add a placeholder - throw new \RuntimeException('Cache clearing not yet implemented'); + if (!$this->cachePool) { + throw new \RuntimeException('Cache is not available'); + } + + // Clear all cache items + $this->cachePool->clear(); } /** @@ -204,9 +200,7 @@ public function clearCache(): void */ public function setCacheDirectory(string $cacheDir): void { - // This would need to be implemented to override cache directory - // For now, we'll add a placeholder - throw new \RuntimeException('Cache directory override not yet implemented'); + $this->configService->getConfig()->cache->directory = $cacheDir; } /** @@ -214,8 +208,6 @@ public function setCacheDirectory(string $cacheDir): void */ public function disableCache(): void { - // This would need to be implemented to disable caching - // For now, we'll add a placeholder - throw new \RuntimeException('Cache disabling not yet implemented'); + $this->configService->getConfig()->cache->enabled = false; } } diff --git a/src/Config/CacheConfig.php b/src/Config/CacheConfig.php index 129f1a9..cce2815 100644 --- a/src/Config/CacheConfig.php +++ b/src/Config/CacheConfig.php @@ -10,9 +10,9 @@ class CacheConfig { public function __construct( - public readonly bool $enabled, - public readonly string $directory, - public readonly bool $compression, + public bool $enabled, + public string $directory, + public bool $compression, ) { } } diff --git a/src/Config/ConfigFactory.php b/src/Config/ConfigFactory.php index 865e064..6ecfead 100644 --- a/src/Config/ConfigFactory.php +++ b/src/Config/ConfigFactory.php @@ -24,11 +24,11 @@ public function fromArray(array $config): CognitiveConfig }, $config['cognitive']['metrics']); $cacheConfig = null; - if (isset($config['cache'])) { + if (isset($config['cognitive']['cache'])) { $cacheConfig = new CacheConfig( - enabled: $config['cache']['enabled'] ?? true, - directory: $config['cache']['directory'] ?? './.phpcca.cache', - compression: $config['cache']['compression'] ?? true, + enabled: $config['cognitive']['cache']['enabled'] ?? true, + directory: $config['cognitive']['cache']['directory'] ?? './.phpcca.cache', + compression: $config['cognitive']['cache']['compression'] ?? true, ); } diff --git a/src/Config/ConfigLoader.php b/src/Config/ConfigLoader.php index 14e4234..da0fd52 100644 --- a/src/Config/ConfigLoader.php +++ b/src/Config/ConfigLoader.php @@ -129,18 +129,18 @@ public function getConfigTreeBuilder(): TreeBuilder }) ->end() ->end() - ->end() - ->end() - ->arrayNode('cache') - ->children() - ->booleanNode('enabled') - ->defaultValue(true) - ->end() - ->scalarNode('directory') - ->defaultValue('./.phpcca.cache') - ->end() - ->booleanNode('compression') - ->defaultValue(true) + ->arrayNode('cache') + ->children() + ->booleanNode('enabled') + ->defaultValue(true) + ->end() + ->scalarNode('directory') + ->defaultValue('./.phpcca.cache') + ->end() + ->booleanNode('compression') + ->defaultValue(true) + ->end() + ->end() ->end() ->end() ->end() From 0fb3ee4316eb82d11da25688cc9a8b2fec1dcc75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Kr=C3=A4mer?= Date: Tue, 7 Oct 2025 00:42:57 +0200 Subject: [PATCH 03/17] Refactoring the Cache --- src/Business/MetricsFacade.php | 26 +------------------------ src/Command/CognitiveMetricsCommand.php | 4 ++-- 2 files changed, 3 insertions(+), 27 deletions(-) diff --git a/src/Business/MetricsFacade.php b/src/Business/MetricsFacade.php index 7db0512..3a10f23 100644 --- a/src/Business/MetricsFacade.php +++ b/src/Business/MetricsFacade.php @@ -33,7 +33,7 @@ public function __construct( private readonly ConfigService $configService, private readonly ChurnCalculator $churnCalculator, private readonly ChangeCounterFactory $changeCounterFactory, - private readonly ?CacheItemPoolInterface $cachePool = null + private readonly CacheItemPoolInterface $cachePool ) { $this->loadConfig(__DIR__ . '/../../config.yml'); } @@ -182,32 +182,8 @@ public function exportMetricsReport( $exporter->export($metricsCollection, $filename); } - /** - * Clear all cached analysis results - */ public function clearCache(): void { - if (!$this->cachePool) { - throw new \RuntimeException('Cache is not available'); - } - - // Clear all cache items $this->cachePool->clear(); } - - /** - * Set cache directory override - */ - public function setCacheDirectory(string $cacheDir): void - { - $this->configService->getConfig()->cache->directory = $cacheDir; - } - - /** - * Disable caching for this run - */ - public function disableCache(): void - { - $this->configService->getConfig()->cache->enabled = false; - } } diff --git a/src/Command/CognitiveMetricsCommand.php b/src/Command/CognitiveMetricsCommand.php index 2b8b438..b0eda26 100644 --- a/src/Command/CognitiveMetricsCommand.php +++ b/src/Command/CognitiveMetricsCommand.php @@ -151,13 +151,13 @@ protected function execute(InputInterface $input, OutputInterface $output): int // Handle cache directory override $cacheDir = $input->getOption(self::OPTION_CACHE_DIR); if ($cacheDir) { - $this->metricsFacade->setCacheDirectory($cacheDir); + $this->metricsFacade->getConfig()->cache->director = $cacheDir; } // Handle no-cache option $noCache = $input->getOption(self::OPTION_NO_CACHE); if ($noCache) { - $this->metricsFacade->disableCache(); + $this->metricsFacade->getConfig()->cache->enabled = false; } $metricsCollection = $this->metricsFacade->getCognitiveMetricsFromPaths($paths); From 912a7559bbc37321459f057c0ac91d51480cd464 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Kr=C3=A4mer?= Date: Tue, 7 Oct 2025 00:49:52 +0200 Subject: [PATCH 04/17] Refactoring the Cache --- src/Business/Cache/FileCacheService.php | 243 ------------------------ 1 file changed, 243 deletions(-) delete mode 100644 src/Business/Cache/FileCacheService.php diff --git a/src/Business/Cache/FileCacheService.php b/src/Business/Cache/FileCacheService.php deleted file mode 100644 index cb0deb4..0000000 --- a/src/Business/Cache/FileCacheService.php +++ /dev/null @@ -1,243 +0,0 @@ -cacheDirectory = rtrim($cacheDirectory, '/'); - $this->ensureCacheDirectory(); - } - - public function getItem(string $key): CacheItemInterface - { - $filePath = $this->getCacheFilePath($key); - - if (!file_exists($filePath)) { - return new CacheItem($key, null, false); - } - - $data = $this->loadCacheData($filePath); - if ($data === null) { - return new CacheItem($key, null, false); - } - - // Check if cache is still valid (file hasn't changed and config matches) - if (!$this->isCacheValid($data, $filePath)) { - unlink($filePath); - return new CacheItem($key, null, false); - } - - return new CacheItem($key, $data, true); - } - - public function getItems(array $keys = []): iterable - { - $items = []; - foreach ($keys as $key) { - $items[$key] = $this->getItem($key); - } - return $items; - } - - public function hasItem(string $key): bool - { - return $this->getItem($key)->isHit(); - } - - public function clear(): bool - { - if (!is_dir($this->cacheDirectory)) { - return true; - } - - $success = true; - $iterator = new \RecursiveIteratorIterator( - new \RecursiveDirectoryIterator($this->cacheDirectory, \RecursiveDirectoryIterator::SKIP_DOTS), - \RecursiveIteratorIterator::CHILD_FIRST - ); - - foreach ($iterator as $file) { - if ($file->isDir()) { - $success = rmdir($file->getRealPath()) && $success; - } else { - $success = unlink($file->getRealPath()) && $success; - } - } - - return $success; - } - - public function deleteItem(string $key): bool - { - $filePath = $this->getCacheFilePath($key); - if (file_exists($filePath)) { - return unlink($filePath); - } - return true; - } - - public function deleteItems(array $keys): bool - { - $success = true; - foreach ($keys as $key) { - $success = $this->deleteItem($key) && $success; - } - return $success; - } - - public function save(CacheItemInterface $item): bool - { - $filePath = $this->getCacheFilePath($item->getKey()); - $data = $item->get(); - - if ($data === null) { - return true; - } - - return $this->saveCacheData($filePath, $data); - } - - public function saveDeferred(CacheItemInterface $item): bool - { - $this->deferred[] = $item; - return true; - } - - public function commit(): bool - { - $success = true; - foreach ($this->deferred as $item) { - $success = $this->save($item) && $success; - } - $this->deferred = []; - return $success; - } - - private function ensureCacheDirectory(): void - { - if (!is_dir($this->cacheDirectory)) { - if (!mkdir($this->cacheDirectory, 0755, true)) { - throw new CacheException("Failed to create cache directory: {$this->cacheDirectory}"); - } - } - } - - private function getCacheFilePath(string $key): string - { - // Create subdirectories to avoid too many files in one directory - $hash = md5($key); - $subDir = substr($hash, 0, 2); - $dir = $this->cacheDirectory . '/' . $subDir; - - if (!is_dir($dir)) { - mkdir($dir, 0755, true); - } - - return $dir . '/' . $hash . '.cache'; - } - - private function loadCacheData(string $filePath): ?array - { - $content = file_get_contents($filePath); - if ($content === false) { - return null; - } - - $data = json_decode($content, true); - if ($data === null) { - return null; - } - - // Data is stored without compression for now - return $data; - } - - private function saveCacheData(string $filePath, array $data): bool - { - // Store data without compression for now (compression can be added later) - // This ensures cache works reliably - - // Sanitize data to ensure valid UTF-8 encoding - $data = $this->sanitizeUtf8($data); - - $json = json_encode($data, JSON_PRETTY_PRINT); - if ($json === false) { - return false; - } - - $dir = dirname($filePath); - if (!is_dir($dir)) { - if (!mkdir($dir, 0755, true)) { - return false; - } - } - - $result = file_put_contents($filePath, $json); - return $result !== false; - } - - /** - * Check if cache data is still valid - */ - private function isCacheValid(array $data, string $filePath): bool - { - // Check if file modification time matches - if (isset($data['file_mtime']) && file_exists($data['file_path'])) { - $currentMtime = filemtime($data['file_path']); - if ($currentMtime !== $data['file_mtime']) { - return false; - } - } - - // Check if config hash matches (if present) - if (isset($data['config_hash'])) { - // This would need to be implemented to check current config hash - // For now, we'll assume it's valid - } - - return true; - } - - /** - * Recursively sanitize UTF-8 data to ensure valid encoding - */ - private function sanitizeUtf8(mixed $data): mixed - { - if (is_string($data)) { - // Remove or replace invalid UTF-8 characters - return mb_convert_encoding($data, 'UTF-8', 'UTF-8'); - } - - if (is_array($data)) { - $sanitized = []; - foreach ($data as $key => $value) { - $sanitizedKey = is_string($key) ? mb_convert_encoding($key, 'UTF-8', 'UTF-8') : $key; - $sanitized[$sanitizedKey] = $this->sanitizeUtf8($value); - } - return $sanitized; - } - - if (is_object($data)) { - // Convert objects to arrays for sanitization - $sanitized = []; - foreach ((array) $data as $key => $value) { - $sanitizedKey = is_string($key) ? mb_convert_encoding($key, 'UTF-8', 'UTF-8') : $key; - $sanitized[$sanitizedKey] = $this->sanitizeUtf8($value); - } - return $sanitized; - } - - return $data; - } -} From c74d9ffe508dfc087b148c09a0f3d917b7d072a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Kr=C3=A4mer?= Date: Tue, 7 Oct 2025 01:00:39 +0200 Subject: [PATCH 05/17] Refactoring the Cache --- src/Business/Cache/FileCache.php | 48 ++++++++++--------- .../Cognitive/CognitiveMetricsCollector.php | 40 ++++------------ src/Command/CognitiveMetricsCommand.php | 7 ++- src/Config/CacheConfig.php | 14 ++++++ src/Config/CognitiveConfig.php | 26 ++++++++++ src/Config/MetricsConfig.php | 14 ++++++ 6 files changed, 93 insertions(+), 56 deletions(-) diff --git a/src/Business/Cache/FileCache.php b/src/Business/Cache/FileCache.php index aafa289..c637506 100644 --- a/src/Business/Cache/FileCache.php +++ b/src/Business/Cache/FileCache.php @@ -14,6 +14,7 @@ class FileCache implements CacheItemPoolInterface { private string $cacheDirectory; + /** @var array */ private array $deferred = []; public function __construct(string $cacheDirectory = './.phpcca.cache') @@ -25,19 +26,20 @@ public function __construct(string $cacheDirectory = './.phpcca.cache') public function getItem(string $key): CacheItemInterface { $filePath = $this->getCacheFilePath($key); - + if (!file_exists($filePath)) { return new CacheItem($key, null, false); } - + $data = $this->loadCacheData($filePath); if ($data === null) { return new CacheItem($key, null, false); } - + return new CacheItem($key, $data, true); } + /** @return array */ public function getItems(array $keys = []): iterable { $items = []; @@ -67,11 +69,11 @@ public function clear(): bool public function deleteItem(string $key): bool { $filePath = $this->getCacheFilePath($key); - + if (file_exists($filePath)) { return unlink($filePath); } - + return true; } @@ -91,14 +93,14 @@ public function save(CacheItemInterface $item): bool if (!$item instanceof CacheItem) { return false; } - + $filePath = $this->getCacheFilePath($item->getKey()); $data = $item->get(); - + if ($data === null) { return $this->deleteItem($item->getKey()); } - + return $this->saveCacheData($filePath, $data); } @@ -107,7 +109,7 @@ public function saveDeferred(CacheItemInterface $item): bool if (!$item instanceof CacheItem) { return false; } - + $this->deferred[] = $item; return true; } @@ -139,53 +141,55 @@ private function getCacheFilePath(string $key): string $hash = md5($key); $subDir = substr($hash, 0, 2); $dir = $this->cacheDirectory . '/' . $subDir; - + if (!is_dir($dir)) { if (!mkdir($dir, 0755, true)) { throw new CacheException("Failed to create cache subdirectory: {$dir}"); } } - + return $dir . '/' . $hash . '.cache'; } + /** @return array|null */ private function loadCacheData(string $filePath): ?array { $content = file_get_contents($filePath); if ($content === false) { return null; } - + $data = json_decode($content, true); if ($data === null) { return null; } - + // Data is stored without compression for now - + return $data; } + /** @param array $data */ private function saveCacheData(string $filePath, array $data): bool { // Store data without compression for now (compression can be added later) // This ensures cache works reliably - + // Sanitize data to ensure valid UTF-8 encoding $data = $this->sanitizeUtf8($data); - + $json = json_encode($data, JSON_PRETTY_PRINT); if ($json === false) { return false; } - + $dir = dirname($filePath); if (!is_dir($dir)) { if (!mkdir($dir, 0755, true)) { return false; } } - + $result = file_put_contents($filePath, $json); return $result !== false; } @@ -199,7 +203,7 @@ private function sanitizeUtf8(mixed $data): mixed // Remove or replace invalid UTF-8 characters return mb_convert_encoding($data, 'UTF-8', 'UTF-8'); } - + if (is_array($data)) { $sanitized = []; foreach ($data as $key => $value) { @@ -208,14 +212,14 @@ private function sanitizeUtf8(mixed $data): mixed } return $sanitized; } - + if (is_object($data)) { // Convert objects to arrays for sanitization $array = (array) $data; $sanitized = $this->sanitizeUtf8($array); return (object) $sanitized; } - + return $data; } @@ -224,7 +228,7 @@ private function removeDirectory(string $dir): void if (!is_dir($dir)) { return; } - + $files = array_diff(scandir($dir), ['.', '..']); foreach ($files as $file) { $path = $dir . '/' . $file; diff --git a/src/Business/Cognitive/CognitiveMetricsCollector.php b/src/Business/Cognitive/CognitiveMetricsCollector.php index ab59e77..c76be7d 100644 --- a/src/Business/Cognitive/CognitiveMetricsCollector.php +++ b/src/Business/Cognitive/CognitiveMetricsCollector.php @@ -16,6 +16,7 @@ use Symfony\Component\Messenger\MessageBusInterface; use Throwable; use Psr\Cache\CacheItemPoolInterface; +use Psr\Cache\CacheItemInterface; /** * CognitiveMetricsCollector class that collects cognitive metrics from source files @@ -105,16 +106,16 @@ private function findMetrics(iterable $files): CognitiveMetricsCollection $cacheItem = null; - if ($useCache) { + if ($useCache && $this->cachePool !== null) { $cacheKey = $this->generateCacheKey($file, $configHash); $cacheItem = $this->cachePool->getItem($cacheKey); - + if ($cacheItem->isHit()) { // Use cached result $cachedData = $cacheItem->get(); $metrics = $cachedData['analysis_result']; $this->ignoredItems = $cachedData['ignored_items']; - + $this->messageBus->dispatch(new FileProcessed($file)); } } @@ -148,7 +149,7 @@ private function findMetrics(iterable $files): CognitiveMetricsCollection )); continue; } - + $this->messageBus->dispatch(new FileProcessed($file)); } @@ -292,7 +293,7 @@ private function generateCacheKey(SplFileInfo $file, string $configHash): string { $filePath = $file->getRealPath(); $fileMtime = $file->getMTime(); - + return 'phpcca_' . md5($filePath . '|' . $fileMtime . '|' . $configHash); } @@ -301,13 +302,14 @@ private function generateCacheKey(SplFileInfo $file, string $configHash): string */ private function generateConfigHash(CognitiveConfig $config): string { - return md5(serialize($this->getConfigAsArray($config))); + return md5(serialize($config->toArray())); } /** * Cache the analysis result for a file */ - private function cacheResult($cacheItem, SplFileInfo $file, array $metrics, string $configHash): void + /** @param array $metrics */ + private function cacheResult(CacheItemInterface $cacheItem, SplFileInfo $file, array $metrics, string $configHash): void { if (!$this->cachePool) { return; @@ -322,30 +324,8 @@ private function cacheResult($cacheItem, SplFileInfo $file, array $metrics, stri 'ignored_items' => $this->ignoredItems, 'cached_at' => time() ]; - + $cacheItem->set($data); $this->cachePool->save($cacheItem); } - - /** - * Get configuration as array for serialization - */ - private function getConfigAsArray(CognitiveConfig $config): array - { - return [ - 'excludeFilePatterns' => $config->excludeFilePatterns, - 'excludePatterns' => $config->excludePatterns, - 'scoreThreshold' => $config->scoreThreshold, - 'showOnlyMethodsExceedingThreshold' => $config->showOnlyMethodsExceedingThreshold, - 'showHalsteadComplexity' => $config->showHalsteadComplexity, - 'showCyclomaticComplexity' => $config->showCyclomaticComplexity, - 'groupByClass' => $config->groupByClass, - 'showDetailedCognitiveMetrics' => $config->showDetailedCognitiveMetrics, - 'cache' => $config->cache ? [ - 'enabled' => $config->cache->enabled, - 'directory' => $config->cache->directory, - 'compression' => $config->cache->compression, - ] : null, - ]; - } } diff --git a/src/Command/CognitiveMetricsCommand.php b/src/Command/CognitiveMetricsCommand.php index b0eda26..53a616c 100644 --- a/src/Command/CognitiveMetricsCommand.php +++ b/src/Command/CognitiveMetricsCommand.php @@ -150,13 +150,13 @@ protected function execute(InputInterface $input, OutputInterface $output): int // Handle cache directory override $cacheDir = $input->getOption(self::OPTION_CACHE_DIR); - if ($cacheDir) { - $this->metricsFacade->getConfig()->cache->director = $cacheDir; + if ($cacheDir && $this->metricsFacade->getConfig()->cache !== null) { + $this->metricsFacade->getConfig()->cache->directory = $cacheDir; } // Handle no-cache option $noCache = $input->getOption(self::OPTION_NO_CACHE); - if ($noCache) { + if ($noCache && $this->metricsFacade->getConfig()->cache !== null) { $this->metricsFacade->getConfig()->cache->enabled = false; } @@ -237,5 +237,4 @@ private function loadConfiguration(string $configFile, OutputInterface $output): return false; } } - } diff --git a/src/Config/CacheConfig.php b/src/Config/CacheConfig.php index cce2815..962c465 100644 --- a/src/Config/CacheConfig.php +++ b/src/Config/CacheConfig.php @@ -15,4 +15,18 @@ public function __construct( public bool $compression, ) { } + + /** + * Convert the cache configuration to an array + * + * @return array + */ + public function toArray(): array + { + return [ + 'enabled' => $this->enabled, + 'directory' => $this->directory, + 'compression' => $this->compression, + ]; + } } diff --git a/src/Config/CognitiveConfig.php b/src/Config/CognitiveConfig.php index 2f90d56..7b54856 100644 --- a/src/Config/CognitiveConfig.php +++ b/src/Config/CognitiveConfig.php @@ -29,4 +29,30 @@ public function __construct( public readonly ?CacheConfig $cache = null, ) { } + + /** + * Convert the cognitive configuration to an array + * + * @return array + */ + public function toArray(): array + { + $metricsArray = []; + foreach ($this->metrics as $key => $metric) { + $metricsArray[$key] = $metric->toArray(); + } + + return [ + 'excludeFilePatterns' => $this->excludeFilePatterns, + 'excludePatterns' => $this->excludePatterns, + 'metrics' => $metricsArray, + 'showOnlyMethodsExceedingThreshold' => $this->showOnlyMethodsExceedingThreshold, + 'scoreThreshold' => $this->scoreThreshold, + 'showHalsteadComplexity' => $this->showHalsteadComplexity, + 'showCyclomaticComplexity' => $this->showCyclomaticComplexity, + 'groupByClass' => $this->groupByClass, + 'showDetailedCognitiveMetrics' => $this->showDetailedCognitiveMetrics, + 'cache' => $this->cache?->toArray(), + ]; + } } diff --git a/src/Config/MetricsConfig.php b/src/Config/MetricsConfig.php index 92621d6..863037d 100644 --- a/src/Config/MetricsConfig.php +++ b/src/Config/MetricsConfig.php @@ -15,4 +15,18 @@ public function __construct( public readonly bool $enabled ) { } + + /** + * Convert the metrics configuration to an array + * + * @return array + */ + public function toArray(): array + { + return [ + 'threshold' => $this->threshold, + 'scale' => $this->scale, + 'enabled' => $this->enabled, + ]; + } } From 51fe831539dc1016516e017bad74766d39849873 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Kr=C3=A4mer?= Date: Tue, 7 Oct 2025 01:11:10 +0200 Subject: [PATCH 06/17] Refactoring --- src/Application.php | 4 +-- src/Business/MetricsFacade.php | 28 ++++++------------- src/{Business => }/Cache/CacheItem.php | 2 +- .../Cache/Exception/CacheException.php | 2 +- src/{Business => }/Cache/FileCache.php | 4 +-- 5 files changed, 14 insertions(+), 26 deletions(-) rename src/{Business => }/Cache/CacheItem.php (96%) rename src/{Business => }/Cache/Exception/CacheException.php (75%) rename src/{Business => }/Cache/FileCache.php (97%) diff --git a/src/Application.php b/src/Application.php index e44a561..2e742ce 100644 --- a/src/Application.php +++ b/src/Application.php @@ -4,7 +4,6 @@ namespace Phauthentic\CognitiveCodeAnalysis; -use Phauthentic\CognitiveCodeAnalysis\Business\Cache\FileCache; use Phauthentic\CognitiveCodeAnalysis\Business\Churn\ChangeCounter\ChangeCounterFactory; use Phauthentic\CognitiveCodeAnalysis\Business\Churn\ChurnCalculator; use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Baseline; @@ -17,6 +16,7 @@ use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\ScoreCalculator; use Phauthentic\CognitiveCodeAnalysis\Business\DirectoryScanner; use Phauthentic\CognitiveCodeAnalysis\Business\MetricsFacade; +use Phauthentic\CognitiveCodeAnalysis\Cache\FileCache; use Phauthentic\CognitiveCodeAnalysis\Command\ChurnCommand; use Phauthentic\CognitiveCodeAnalysis\Command\CognitiveMetricsCommand; use Phauthentic\CognitiveCodeAnalysis\Command\EventHandler\ParserErrorHandler; @@ -32,6 +32,7 @@ use PhpParser\NodeTraverser; use PhpParser\NodeTraverserInterface; use PhpParser\ParserFactory; +use Psr\Cache\CacheItemPoolInterface; use Symfony\Component\Config\Definition\Processor; use Symfony\Component\Console\Application as SymfonyApplication; use Symfony\Component\Console\Input\ArgvInput; @@ -45,7 +46,6 @@ use Symfony\Component\Messenger\MessageBus; use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Messenger\Middleware\HandleMessageMiddleware; -use Psr\Cache\CacheItemPoolInterface; /** * diff --git a/src/Business/MetricsFacade.php b/src/Business/MetricsFacade.php index 3a10f23..b0bb2f8 100644 --- a/src/Business/MetricsFacade.php +++ b/src/Business/MetricsFacade.php @@ -12,9 +12,11 @@ use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\CognitiveMetricsCollector; use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Exporter\CognitiveExporterFactory; use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\ScoreCalculator; +use Phauthentic\CognitiveCodeAnalysis\CognitiveAnalysisException; use Phauthentic\CognitiveCodeAnalysis\Config\CognitiveConfig; use Phauthentic\CognitiveCodeAnalysis\Config\ConfigService; use Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\Messenger\Exception\ExceptionInterface; /** * Facade class for collecting and managing code quality metrics. @@ -65,6 +67,8 @@ private function getCognitiveExporterFactory(): CognitiveExporterFactory * * @param string $path The file or directory path to collect metrics from. * @return CognitiveMetricsCollection The collected cognitive metrics. + * @throws CognitiveAnalysisException + * @throws ExceptionInterface */ public function getCognitiveMetrics(string $path): CognitiveMetricsCollection { @@ -82,6 +86,8 @@ public function getCognitiveMetrics(string $path): CognitiveMetricsCollection * * @param array $paths Array of file or directory paths to collect metrics from. * @return CognitiveMetricsCollection The collected cognitive metrics from all paths. + * @throws CognitiveAnalysisException + * @throws ExceptionInterface */ public function getCognitiveMetricsFromPaths(array $paths): CognitiveMetricsCollection { @@ -102,6 +108,8 @@ public function getCognitiveMetricsFromPaths(array $paths): CognitiveMetricsColl * @param string $since * @param CoverageReportReaderInterface|null $coverageReader * @return array> + * @throws CognitiveAnalysisException + * @throws ExceptionInterface */ public function calculateChurn( string $path, @@ -138,26 +146,6 @@ public function getConfig(): CognitiveConfig return $this->configService->getConfig(); } - /** - * Get all ignored classes and methods from the last metrics collection. - * - * @return array> Array with 'classes' and 'methods' keys - */ - public function getIgnored(): array - { - return $this->cognitiveMetricsCollector->getIgnored(); - } - - /** - * Get ignored classes from the last metrics collection. - * - * @return array Array of ignored class FQCNs - */ - public function getIgnoredClasses(): array - { - return $this->cognitiveMetricsCollector->getIgnoredClasses(); - } - /** * @param array> $classes */ diff --git a/src/Business/Cache/CacheItem.php b/src/Cache/CacheItem.php similarity index 96% rename from src/Business/Cache/CacheItem.php rename to src/Cache/CacheItem.php index ce680cf..5dca21f 100644 --- a/src/Business/Cache/CacheItem.php +++ b/src/Cache/CacheItem.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Phauthentic\CognitiveCodeAnalysis\Business\Cache; +namespace Phauthentic\CognitiveCodeAnalysis\Cache; use Psr\Cache\CacheItemInterface; diff --git a/src/Business/Cache/Exception/CacheException.php b/src/Cache/Exception/CacheException.php similarity index 75% rename from src/Business/Cache/Exception/CacheException.php rename to src/Cache/Exception/CacheException.php index 50d44c8..a3e1204 100644 --- a/src/Business/Cache/Exception/CacheException.php +++ b/src/Cache/Exception/CacheException.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Phauthentic\CognitiveCodeAnalysis\Business\Cache\Exception; +namespace Phauthentic\CognitiveCodeAnalysis\Cache\Exception; use Phauthentic\CognitiveCodeAnalysis\CognitiveAnalysisException; diff --git a/src/Business/Cache/FileCache.php b/src/Cache/FileCache.php similarity index 97% rename from src/Business/Cache/FileCache.php rename to src/Cache/FileCache.php index c637506..aed5d46 100644 --- a/src/Business/Cache/FileCache.php +++ b/src/Cache/FileCache.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace Phauthentic\CognitiveCodeAnalysis\Business\Cache; +namespace Phauthentic\CognitiveCodeAnalysis\Cache; -use Phauthentic\CognitiveCodeAnalysis\Business\Cache\Exception\CacheException; +use Phauthentic\CognitiveCodeAnalysis\Cache\Exception\CacheException; use Psr\Cache\CacheItemInterface; use Psr\Cache\CacheItemPoolInterface; From c586a4977fd8e53f49b11a40ed1444de5f81665a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Kr=C3=A4mer?= Date: Tue, 7 Oct 2025 01:34:41 +0200 Subject: [PATCH 07/17] Fixing the seemingly non-deterministic ordering --- .../Cognitive/CognitiveMetricsCollection.php | 2 ++ .../Exporter/MarkdownExporterContent_AllMetrics.md | 14 +++++++------- .../MarkdownExporterContent_CyclomaticOnly.md | 14 +++++++------- .../MarkdownExporterContent_HalsteadOnly.md | 14 +++++++------- .../Exporter/MarkdownExporterContent_Minimal.md | 14 +++++++------- .../MarkdownExporterContent_NoDetailedMetrics.md | 14 +++++++------- .../Exporter/MarkdownExporterContent_Threshold.md | 12 ++++++------ 7 files changed, 43 insertions(+), 41 deletions(-) diff --git a/src/Business/Cognitive/CognitiveMetricsCollection.php b/src/Business/Cognitive/CognitiveMetricsCollection.php index d537c6a..1c24d9a 100644 --- a/src/Business/Cognitive/CognitiveMetricsCollection.php +++ b/src/Business/Cognitive/CognitiveMetricsCollection.php @@ -130,6 +130,8 @@ public function groupBy(string $property): array $grouped[$key]->add($metric); } + ksort($grouped); + return $grouped; } diff --git a/tests/Unit/Business/Cognitive/Exporter/MarkdownExporterContent_AllMetrics.md b/tests/Unit/Business/Cognitive/Exporter/MarkdownExporterContent_AllMetrics.md index 70ef966..8e2d541 100644 --- a/tests/Unit/Business/Cognitive/Exporter/MarkdownExporterContent_AllMetrics.md +++ b/tests/Unit/Business/Cognitive/Exporter/MarkdownExporterContent_AllMetrics.md @@ -6,22 +6,22 @@ --- -* **Class:** TestClass -* **File:** TestClass.php +* **Class:** AnotherClass +* **File:** AnotherClass.php | Method | Lines | Args | Returns | Variables | Property Calls | If | If Nesting | Else | Cognitive Complexity | Halstead Volume | Halstead Difficulty | Halstead Effort | Cyclomatic Complexity | |--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------| -| testMethod | 10 (0.500) | 2 (0.300) | 1 (0.200) | 5 (0.400) | 3 (0.300) | 4 (0.600) | 2 (0.500) | 1 (0.200) | 0.300 | 573.211 | 12.500 | 7,165.138 | 5 (low) | -| anotherMethod | 5 (0.200) | 1 (0.100) | 1 (0.100) | 2 (0.100) | 1 (0.100) | 1 (0.200) | 1 (0.100) | 0 (0.000) | 0.050 | 185.470 | 6.250 | 1,159.188 | 2 (low) | +| complexMethod | 20 (1.000) | 4 (0.600) | 3 (0.500) | 10 (0.800) | 5 (0.500) | 8 (1.200) | 3 (1.000) | 4 (0.600) | 0.800 | 1,357.824 | 25.000 | 33,945.600 | 12 (medium) | --- -* **Class:** AnotherClass -* **File:** AnotherClass.php +* **Class:** TestClass +* **File:** TestClass.php | Method | Lines | Args | Returns | Variables | Property Calls | If | If Nesting | Else | Cognitive Complexity | Halstead Volume | Halstead Difficulty | Halstead Effort | Cyclomatic Complexity | |--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------| -| complexMethod | 20 (1.000) | 4 (0.600) | 3 (0.500) | 10 (0.800) | 5 (0.500) | 8 (1.200) | 3 (1.000) | 4 (0.600) | 0.800 | 1,357.824 | 25.000 | 33,945.600 | 12 (medium) | +| testMethod | 10 (0.500) | 2 (0.300) | 1 (0.200) | 5 (0.400) | 3 (0.300) | 4 (0.600) | 2 (0.500) | 1 (0.200) | 0.300 | 573.211 | 12.500 | 7,165.138 | 5 (low) | +| anotherMethod | 5 (0.200) | 1 (0.100) | 1 (0.100) | 2 (0.100) | 1 (0.100) | 1 (0.200) | 1 (0.100) | 0 (0.000) | 0.050 | 185.470 | 6.250 | 1,159.188 | 2 (low) | --- diff --git a/tests/Unit/Business/Cognitive/Exporter/MarkdownExporterContent_CyclomaticOnly.md b/tests/Unit/Business/Cognitive/Exporter/MarkdownExporterContent_CyclomaticOnly.md index 8670297..1e17cbf 100644 --- a/tests/Unit/Business/Cognitive/Exporter/MarkdownExporterContent_CyclomaticOnly.md +++ b/tests/Unit/Business/Cognitive/Exporter/MarkdownExporterContent_CyclomaticOnly.md @@ -6,22 +6,22 @@ --- -* **Class:** TestClass -* **File:** TestClass.php +* **Class:** AnotherClass +* **File:** AnotherClass.php | Method | Lines | Args | Returns | Variables | Property Calls | If | If Nesting | Else | Cognitive Complexity | Cyclomatic Complexity | |--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------| -| testMethod | 10 (0.500) | 2 (0.300) | 1 (0.200) | 5 (0.400) | 3 (0.300) | 4 (0.600) | 2 (0.500) | 1 (0.200) | 0.300 | 5 (low) | -| anotherMethod | 5 (0.200) | 1 (0.100) | 1 (0.100) | 2 (0.100) | 1 (0.100) | 1 (0.200) | 1 (0.100) | 0 (0.000) | 0.050 | 2 (low) | +| complexMethod | 20 (1.000) | 4 (0.600) | 3 (0.500) | 10 (0.800) | 5 (0.500) | 8 (1.200) | 3 (1.000) | 4 (0.600) | 0.800 | 12 (medium) | --- -* **Class:** AnotherClass -* **File:** AnotherClass.php +* **Class:** TestClass +* **File:** TestClass.php | Method | Lines | Args | Returns | Variables | Property Calls | If | If Nesting | Else | Cognitive Complexity | Cyclomatic Complexity | |--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------| -| complexMethod | 20 (1.000) | 4 (0.600) | 3 (0.500) | 10 (0.800) | 5 (0.500) | 8 (1.200) | 3 (1.000) | 4 (0.600) | 0.800 | 12 (medium) | +| testMethod | 10 (0.500) | 2 (0.300) | 1 (0.200) | 5 (0.400) | 3 (0.300) | 4 (0.600) | 2 (0.500) | 1 (0.200) | 0.300 | 5 (low) | +| anotherMethod | 5 (0.200) | 1 (0.100) | 1 (0.100) | 2 (0.100) | 1 (0.100) | 1 (0.200) | 1 (0.100) | 0 (0.000) | 0.050 | 2 (low) | --- diff --git a/tests/Unit/Business/Cognitive/Exporter/MarkdownExporterContent_HalsteadOnly.md b/tests/Unit/Business/Cognitive/Exporter/MarkdownExporterContent_HalsteadOnly.md index 43b64de..f7eade0 100644 --- a/tests/Unit/Business/Cognitive/Exporter/MarkdownExporterContent_HalsteadOnly.md +++ b/tests/Unit/Business/Cognitive/Exporter/MarkdownExporterContent_HalsteadOnly.md @@ -6,22 +6,22 @@ --- -* **Class:** TestClass -* **File:** TestClass.php +* **Class:** AnotherClass +* **File:** AnotherClass.php | Method | Lines | Args | Returns | Variables | Property Calls | If | If Nesting | Else | Cognitive Complexity | Halstead Volume | Halstead Difficulty | Halstead Effort | |--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------| -| testMethod | 10 (0.500) | 2 (0.300) | 1 (0.200) | 5 (0.400) | 3 (0.300) | 4 (0.600) | 2 (0.500) | 1 (0.200) | 0.300 | 573.211 | 12.500 | 7,165.138 | -| anotherMethod | 5 (0.200) | 1 (0.100) | 1 (0.100) | 2 (0.100) | 1 (0.100) | 1 (0.200) | 1 (0.100) | 0 (0.000) | 0.050 | 185.470 | 6.250 | 1,159.188 | +| complexMethod | 20 (1.000) | 4 (0.600) | 3 (0.500) | 10 (0.800) | 5 (0.500) | 8 (1.200) | 3 (1.000) | 4 (0.600) | 0.800 | 1,357.824 | 25.000 | 33,945.600 | --- -* **Class:** AnotherClass -* **File:** AnotherClass.php +* **Class:** TestClass +* **File:** TestClass.php | Method | Lines | Args | Returns | Variables | Property Calls | If | If Nesting | Else | Cognitive Complexity | Halstead Volume | Halstead Difficulty | Halstead Effort | |--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------| -| complexMethod | 20 (1.000) | 4 (0.600) | 3 (0.500) | 10 (0.800) | 5 (0.500) | 8 (1.200) | 3 (1.000) | 4 (0.600) | 0.800 | 1,357.824 | 25.000 | 33,945.600 | +| testMethod | 10 (0.500) | 2 (0.300) | 1 (0.200) | 5 (0.400) | 3 (0.300) | 4 (0.600) | 2 (0.500) | 1 (0.200) | 0.300 | 573.211 | 12.500 | 7,165.138 | +| anotherMethod | 5 (0.200) | 1 (0.100) | 1 (0.100) | 2 (0.100) | 1 (0.100) | 1 (0.200) | 1 (0.100) | 0 (0.000) | 0.050 | 185.470 | 6.250 | 1,159.188 | --- diff --git a/tests/Unit/Business/Cognitive/Exporter/MarkdownExporterContent_Minimal.md b/tests/Unit/Business/Cognitive/Exporter/MarkdownExporterContent_Minimal.md index 7f6ee37..4d1870e 100644 --- a/tests/Unit/Business/Cognitive/Exporter/MarkdownExporterContent_Minimal.md +++ b/tests/Unit/Business/Cognitive/Exporter/MarkdownExporterContent_Minimal.md @@ -6,22 +6,22 @@ --- -* **Class:** TestClass -* **File:** TestClass.php +* **Class:** AnotherClass +* **File:** AnotherClass.php | Method | Cognitive Complexity | |--------|--------| -| testMethod | 0.300 | -| anotherMethod | 0.050 | +| complexMethod | 0.800 | --- -* **Class:** AnotherClass -* **File:** AnotherClass.php +* **Class:** TestClass +* **File:** TestClass.php | Method | Cognitive Complexity | |--------|--------| -| complexMethod | 0.800 | +| testMethod | 0.300 | +| anotherMethod | 0.050 | --- diff --git a/tests/Unit/Business/Cognitive/Exporter/MarkdownExporterContent_NoDetailedMetrics.md b/tests/Unit/Business/Cognitive/Exporter/MarkdownExporterContent_NoDetailedMetrics.md index 3c21cf5..26dcb92 100644 --- a/tests/Unit/Business/Cognitive/Exporter/MarkdownExporterContent_NoDetailedMetrics.md +++ b/tests/Unit/Business/Cognitive/Exporter/MarkdownExporterContent_NoDetailedMetrics.md @@ -6,22 +6,22 @@ --- -* **Class:** TestClass -* **File:** TestClass.php +* **Class:** AnotherClass +* **File:** AnotherClass.php | Method | Cognitive Complexity | Halstead Volume | Halstead Difficulty | Halstead Effort | Cyclomatic Complexity | |--------|--------|--------|--------|--------|--------| -| testMethod | 0.300 | 573.211 | 12.500 | 7,165.138 | 5 (low) | -| anotherMethod | 0.050 | 185.470 | 6.250 | 1,159.188 | 2 (low) | +| complexMethod | 0.800 | 1,357.824 | 25.000 | 33,945.600 | 12 (medium) | --- -* **Class:** AnotherClass -* **File:** AnotherClass.php +* **Class:** TestClass +* **File:** TestClass.php | Method | Cognitive Complexity | Halstead Volume | Halstead Difficulty | Halstead Effort | Cyclomatic Complexity | |--------|--------|--------|--------|--------|--------| -| complexMethod | 0.800 | 1,357.824 | 25.000 | 33,945.600 | 12 (medium) | +| testMethod | 0.300 | 573.211 | 12.500 | 7,165.138 | 5 (low) | +| anotherMethod | 0.050 | 185.470 | 6.250 | 1,159.188 | 2 (low) | --- diff --git a/tests/Unit/Business/Cognitive/Exporter/MarkdownExporterContent_Threshold.md b/tests/Unit/Business/Cognitive/Exporter/MarkdownExporterContent_Threshold.md index a944ba3..b6f7fd6 100644 --- a/tests/Unit/Business/Cognitive/Exporter/MarkdownExporterContent_Threshold.md +++ b/tests/Unit/Business/Cognitive/Exporter/MarkdownExporterContent_Threshold.md @@ -8,21 +8,21 @@ --- -* **Class:** TestClass -* **File:** TestClass.php +* **Class:** AnotherClass +* **File:** AnotherClass.php | Method | Lines | Args | Returns | Variables | Property Calls | If | If Nesting | Else | Cognitive Complexity | Halstead Volume | Halstead Difficulty | Halstead Effort | Cyclomatic Complexity | |--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------| -| testMethod | 10 (0.500) | 2 (0.300) | 1 (0.200) | 5 (0.400) | 3 (0.300) | 4 (0.600) | 2 (0.500) | 1 (0.200) | 0.300 | 573.211 | 12.500 | 7,165.138 | 5 (low) | +| complexMethod | 20 (1.000) | 4 (0.600) | 3 (0.500) | 10 (0.800) | 5 (0.500) | 8 (1.200) | 3 (1.000) | 4 (0.600) | 0.800 | 1,357.824 | 25.000 | 33,945.600 | 12 (medium) | --- -* **Class:** AnotherClass -* **File:** AnotherClass.php +* **Class:** TestClass +* **File:** TestClass.php | Method | Lines | Args | Returns | Variables | Property Calls | If | If Nesting | Else | Cognitive Complexity | Halstead Volume | Halstead Difficulty | Halstead Effort | Cyclomatic Complexity | |--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------|--------| -| complexMethod | 20 (1.000) | 4 (0.600) | 3 (0.500) | 10 (0.800) | 5 (0.500) | 8 (1.200) | 3 (1.000) | 4 (0.600) | 0.800 | 1,357.824 | 25.000 | 33,945.600 | 12 (medium) | +| testMethod | 10 (0.500) | 2 (0.300) | 1 (0.200) | 5 (0.400) | 3 (0.300) | 4 (0.600) | 2 (0.500) | 1 (0.200) | 0.300 | 573.211 | 12.500 | 7,165.138 | 5 (low) | --- From 3791ff2d9e5eafbcaf894a5c8c98a50def5a3946 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Kr=C3=A4mer?= Date: Tue, 7 Oct 2025 19:50:10 +0200 Subject: [PATCH 08/17] Remove unused methods for retrieving ignored classes and methods --- .../Cognitive/CognitiveMetricsCollector.php | 30 ------------------- 1 file changed, 30 deletions(-) diff --git a/src/Business/Cognitive/CognitiveMetricsCollector.php b/src/Business/Cognitive/CognitiveMetricsCollector.php index 1f772b5..203aa85 100644 --- a/src/Business/Cognitive/CognitiveMetricsCollector.php +++ b/src/Business/Cognitive/CognitiveMetricsCollector.php @@ -191,36 +191,6 @@ private function findSourceFiles(string $path, array $exclude = []): iterable ); } - /** - * Get all ignored classes and methods from the last parsing operation. - * - * @return array> Array with 'classes' and 'methods' keys - */ - public function getIgnored(): array - { - return $this->ignoredItems ?? ['classes' => [], 'methods' => []]; - } - - /** - * Get ignored classes from the last parsing operation. - * - * @return array Array of ignored class FQCNs - */ - public function getIgnoredClasses(): array - { - return $this->ignoredItems['classes'] ?? []; - } - - /** - * Get ignored methods from the last parsing operation. - * - * @return array Array of ignored method keys (ClassName::methodName) - */ - public function getIgnoredMethods(): array - { - return $this->ignoredItems['methods'] ?? []; - } - /** * Get the project root directory path. * From 08c7f1591703a5657991b12343f740b1e56a495f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Kr=C3=A4mer?= Date: Tue, 7 Oct 2025 23:15:19 +0200 Subject: [PATCH 09/17] Refactoring --- .../Cognitive/CognitiveMetricsCollector.php | 34 ++++--- src/Cache/FileCache.php | 90 ++++++++++++------- 2 files changed, 80 insertions(+), 44 deletions(-) diff --git a/src/Business/Cognitive/CognitiveMetricsCollector.php b/src/Business/Cognitive/CognitiveMetricsCollector.php index 203aa85..e86c26e 100644 --- a/src/Business/Cognitive/CognitiveMetricsCollector.php +++ b/src/Business/Cognitive/CognitiveMetricsCollector.php @@ -11,6 +11,7 @@ use Phauthentic\CognitiveCodeAnalysis\CognitiveAnalysisException; use Phauthentic\CognitiveCodeAnalysis\Config\CognitiveConfig; use Phauthentic\CognitiveCodeAnalysis\Config\ConfigService; +use Psr\Cache\InvalidArgumentException; use SplFileInfo; use Symfony\Component\Messenger\Exception\ExceptionInterface; use Symfony\Component\Messenger\MessageBusInterface; @@ -43,7 +44,7 @@ public function __construct( * @param string $path * @param CognitiveConfig $config * @return CognitiveMetricsCollection - * @throws CognitiveAnalysisException + * @throws CognitiveAnalysisException|ExceptionInterface */ public function collect(string $path, CognitiveConfig $config): CognitiveMetricsCollection { @@ -56,7 +57,7 @@ public function collect(string $path, CognitiveConfig $config): CognitiveMetrics * @param array $paths Array of paths to process * @param CognitiveConfig $config * @return CognitiveMetricsCollection Merged collection of metrics from all paths - * @throws CognitiveAnalysisException + * @throws CognitiveAnalysisException|ExceptionInterface */ public function collectFromPaths(array $paths, CognitiveConfig $config): CognitiveMetricsCollection { @@ -91,7 +92,6 @@ private function getCodeFromFile(SplFileInfo $file): string * * @param iterable $files * @return CognitiveMetricsCollection - * @throws ExceptionInterface */ private function findMetrics(iterable $files): CognitiveMetricsCollection { @@ -115,12 +115,10 @@ private function findMetrics(iterable $files): CognitiveMetricsCollection } } - $filename = $this->normalizeFilename($file); - $metricsCollection = $this->processMethodMetrics( $metrics, $metricsCollection, - $filename + $this->normalizeFilename($file) ); } @@ -182,6 +180,7 @@ private function isExcluded(string $classAndMethod): bool * @param string $path Path to the directory or file to scan * @param array $exclude List of regx to exclude * @return iterable An iterable of SplFileInfo objects + * @throws CognitiveAnalysisException */ private function findSourceFiles(string $path, array $exclude = []): iterable { @@ -194,11 +193,12 @@ private function findSourceFiles(string $path, array $exclude = []): iterable /** * Get the project root directory path. * + * Start from the current file's directory and traverse up to find composer.json + * * @return string|null The project root path or null if not found */ private function getProjectRoot(): ?string { - // Start from the current file's directory and traverse up to find composer.json $currentDir = __DIR__; while ($currentDir !== dirname($currentDir)) { @@ -212,7 +212,7 @@ private function getProjectRoot(): ?string } /** - * Generate cache key for a file based on path, modification time, and config hash + * Generate a cache key for a file based on path, modification time, and config hash */ private function generateCacheKey(SplFileInfo $file, string $configHash): string { @@ -262,17 +262,21 @@ public function clearCache(): void } /** - * Normalize filename for test environment + * Normalize filename for the test environment + * + * This is to ensure consistent file paths in test outputs */ private function normalizeFilename(SplFileInfo $file): string { $filename = $file->getRealPath(); - if (getenv('APP_ENV') === 'test') { - $projectRoot = $this->getProjectRoot(); - if ($projectRoot && str_starts_with($filename, $projectRoot)) { - $filename = substr($filename, strlen($projectRoot) + 1); - } + if (getenv('APP_ENV') !== 'test') { + return $filename; + } + + $projectRoot = $this->getProjectRoot(); + if ($projectRoot && str_starts_with($filename, $projectRoot)) { + $filename = substr($filename, strlen($projectRoot) + 1); } return $filename; @@ -282,6 +286,7 @@ private function normalizeFilename(SplFileInfo $file): string * Try to get cached metrics for a file * * @return array{metrics: array|null, cacheItem: CacheItemInterface|null} + * @throws InvalidArgumentException|ExceptionInterface */ private function getCachedMetrics(SplFileInfo $file, string $configHash, bool $useCache): array { @@ -341,6 +346,7 @@ private function processFile( $file, $exception )); + return null; } } diff --git a/src/Cache/FileCache.php b/src/Cache/FileCache.php index 33c571a..db86294 100644 --- a/src/Cache/FileCache.php +++ b/src/Cache/FileCache.php @@ -14,15 +14,24 @@ class FileCache implements CacheItemPoolInterface { private string $cacheDirectory; - /** @var array */ + + /** + * @var array + */ private array $deferred = []; + /** + * @throws CacheException + */ public function __construct(string $cacheDirectory = './.phpcca.cache') { $this->cacheDirectory = rtrim($cacheDirectory, '/'); $this->ensureCacheDirectory(); } + /** + * @throws CacheException + */ public function getItem(string $key): CacheItemInterface { $filePath = $this->getCacheFilePath($key); @@ -39,19 +48,27 @@ public function getItem(string $key): CacheItemInterface return new CacheItem($key, $data, true); } - /** @return array */ + /** + * @return array + * @throws CacheException + */ public function getItems(array $keys = []): iterable { $items = []; foreach ($keys as $key) { $items[$key] = $this->getItem($key); } + return $items; } + /** + * @throws CacheException + */ public function hasItem(string $key): bool { $filePath = $this->getCacheFilePath($key); + return file_exists($filePath) && $this->loadCacheData($filePath) !== null; } @@ -66,6 +83,9 @@ public function clear(): bool } } + /** + * @throws CacheException + */ public function deleteItem(string $key): bool { $filePath = $this->getCacheFilePath($key); @@ -77,6 +97,9 @@ public function deleteItem(string $key): bool return true; } + /** + * @throws CacheException + */ public function deleteItems(array $keys): bool { $success = true; @@ -88,12 +111,11 @@ public function deleteItems(array $keys): bool return $success; } + /** + * @throws CacheException + */ public function save(CacheItemInterface $item): bool { - if (!$item instanceof CacheItem) { - return false; - } - $filePath = $this->getCacheFilePath($item->getKey()); $data = $item->get(); @@ -106,11 +128,8 @@ public function save(CacheItemInterface $item): bool public function saveDeferred(CacheItemInterface $item): bool { - if (!$item instanceof CacheItem) { - return false; - } - $this->deferred[] = $item; + return true; } @@ -126,26 +145,35 @@ public function commit(): bool return $success; } + /** + * @throws CacheException + */ private function ensureCacheDirectory(): void { - if (!is_dir($this->cacheDirectory)) { - if (!mkdir($this->cacheDirectory, 0755, true)) { - throw new CacheException("Failed to create cache directory: {$this->cacheDirectory}"); - } + if ( + !is_dir($this->cacheDirectory) + && !mkdir($this->cacheDirectory, 0755, true) + ) { + throw new CacheException("Failed to create cache directory: {$this->cacheDirectory}"); } } + /** + * Create subdirectories to avoid too many files in one directory + * + * @throws CacheException + */ private function getCacheFilePath(string $key): string { - // Create subdirectories to avoid too many files in one directory $hash = md5($key); $subDir = substr($hash, 0, 2); $dir = $this->cacheDirectory . '/' . $subDir; - if (!is_dir($dir)) { - if (!mkdir($dir, 0755, true)) { - throw new CacheException("Failed to create cache subdirectory: {$dir}"); - } + if ( + !is_dir($dir) + && !mkdir($dir, 0755, true) + ) { + throw new CacheException("Failed to create cache subdirectory: {$dir}"); } return $dir . '/' . $hash . '.cache'; @@ -164,18 +192,18 @@ private function loadCacheData(string $filePath): ?array return null; } - // Data is stored without compression for now - return $data; } - /** @param array $data */ + /** + * Store data + * + * Sanitize data to ensure valid UTF-8 encoding + * + * @param array $data + */ private function saveCacheData(string $filePath, array $data): bool { - // Store data without compression for now (compression can be added later) - // This ensures cache works reliably - - // Sanitize data to ensure valid UTF-8 encoding $data = $this->sanitizeUtf8($data); $json = json_encode($data, JSON_PRETTY_PRINT); @@ -184,13 +212,15 @@ private function saveCacheData(string $filePath, array $data): bool } $dir = dirname($filePath); - if (!is_dir($dir)) { - if (!mkdir($dir, 0755, true)) { - return false; - } + if ( + !is_dir($dir) + && !mkdir($dir, 0755, true) + ) { + return false; } $result = file_put_contents($filePath, $json); + return $result !== false; } From 08997bbcd53762a622f68da5274783191c39b88c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Kr=C3=A4mer?= Date: Tue, 7 Oct 2025 23:43:52 +0200 Subject: [PATCH 10/17] Refactoring --- src/Application.php | 2 +- .../Cognitive/CognitiveMetricsCollector.php | 32 ++++++++----------- src/Business/Cognitive/Parser.php | 8 ++--- src/Business/MetricsFacade.php | 8 +++-- .../{ => Utility}/DirectoryScanner.php | 2 +- src/Command/CognitiveMetricsCommand.php | 7 ++-- tests/Fixtures/Coverage/coverage-clover.xml | 2 +- tests/Fixtures/Coverage/coverage.xml | 4 +-- .../CognitiveMetricsCollectorTest.php | 22 ++++++++++--- tests/Unit/Business/DirectoryScannerTest.php | 3 +- .../Command/CognitiveMetricsCommandTest.php | 4 +-- 11 files changed, 54 insertions(+), 40 deletions(-) rename src/Business/{ => Utility}/DirectoryScanner.php (98%) diff --git a/src/Application.php b/src/Application.php index e6a0f14..cfe66fc 100644 --- a/src/Application.php +++ b/src/Application.php @@ -15,8 +15,8 @@ use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Events\SourceFilesFound; use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Parser; use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\ScoreCalculator; -use Phauthentic\CognitiveCodeAnalysis\Business\DirectoryScanner; use Phauthentic\CognitiveCodeAnalysis\Business\MetricsFacade; +use Phauthentic\CognitiveCodeAnalysis\Business\Utility\DirectoryScanner; use Phauthentic\CognitiveCodeAnalysis\Cache\FileCache; use Phauthentic\CognitiveCodeAnalysis\Command\ChurnCommand; use Phauthentic\CognitiveCodeAnalysis\Command\CognitiveMetricsCommand; diff --git a/src/Business/Cognitive/CognitiveMetricsCollector.php b/src/Business/Cognitive/CognitiveMetricsCollector.php index e86c26e..ea387ef 100644 --- a/src/Business/Cognitive/CognitiveMetricsCollector.php +++ b/src/Business/Cognitive/CognitiveMetricsCollector.php @@ -7,17 +7,17 @@ use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Events\FileProcessed; use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Events\ParserFailed; use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Events\SourceFilesFound; -use Phauthentic\CognitiveCodeAnalysis\Business\DirectoryScanner; +use Phauthentic\CognitiveCodeAnalysis\Business\Utility\DirectoryScanner; use Phauthentic\CognitiveCodeAnalysis\CognitiveAnalysisException; use Phauthentic\CognitiveCodeAnalysis\Config\CognitiveConfig; use Phauthentic\CognitiveCodeAnalysis\Config\ConfigService; +use Psr\Cache\CacheItemInterface; +use Psr\Cache\CacheItemPoolInterface; use Psr\Cache\InvalidArgumentException; use SplFileInfo; use Symfony\Component\Messenger\Exception\ExceptionInterface; use Symfony\Component\Messenger\MessageBusInterface; use Throwable; -use Psr\Cache\CacheItemPoolInterface; -use Psr\Cache\CacheItemInterface; /** * CognitiveMetricsCollector class that collects cognitive metrics from source files @@ -34,7 +34,7 @@ public function __construct( protected readonly DirectoryScanner $directoryScanner, protected readonly ConfigService $configService, protected readonly MessageBusInterface $messageBus, - protected readonly ?CacheItemPoolInterface $cachePool = null, + protected readonly CacheItemPoolInterface $cachePool, ) { } @@ -44,7 +44,7 @@ public function __construct( * @param string $path * @param CognitiveConfig $config * @return CognitiveMetricsCollection - * @throws CognitiveAnalysisException|ExceptionInterface + * @throws CognitiveAnalysisException|ExceptionInterface|InvalidArgumentException */ public function collect(string $path, CognitiveConfig $config): CognitiveMetricsCollection { @@ -57,7 +57,7 @@ public function collect(string $path, CognitiveConfig $config): CognitiveMetrics * @param array $paths Array of paths to process * @param CognitiveConfig $config * @return CognitiveMetricsCollection Merged collection of metrics from all paths - * @throws CognitiveAnalysisException|ExceptionInterface + * @throws CognitiveAnalysisException|ExceptionInterface|InvalidArgumentException */ public function collectFromPaths(array $paths, CognitiveConfig $config): CognitiveMetricsCollection { @@ -92,6 +92,8 @@ private function getCodeFromFile(SplFileInfo $file): string * * @param iterable $files * @return CognitiveMetricsCollection + * @throws ExceptionInterface + * @throws InvalidArgumentException */ private function findMetrics(iterable $files): CognitiveMetricsCollection { @@ -99,7 +101,7 @@ private function findMetrics(iterable $files): CognitiveMetricsCollection $fileCount = 0; $config = $this->configService->getConfig(); $configHash = $this->generateConfigHash($config); - $useCache = $this->cachePool !== null && $config->cache?->enabled === true; + $useCache = $config->cache?->enabled === true; foreach ($files as $file) { // Try to get cached metrics @@ -236,11 +238,7 @@ private function generateConfigHash(CognitiveConfig $config): string /** @param array $metrics */ private function cacheResult(CacheItemInterface $cacheItem, SplFileInfo $file, array $metrics, string $configHash): void { - if (!$this->cachePool) { - return; - } - - $data = [ + $cacheItem->set([ 'version' => '1.0', 'file_path' => $file->getRealPath(), 'file_mtime' => $file->getMTime(), @@ -248,17 +246,14 @@ private function cacheResult(CacheItemInterface $cacheItem, SplFileInfo $file, a 'analysis_result' => $metrics, 'ignored_items' => $this->ignoredItems, 'cached_at' => time() - ]; + ]); - $cacheItem->set($data); $this->cachePool->save($cacheItem); } public function clearCache(): void { - if ($this->cachePool !== null) { - $this->cachePool->clear(); - } + $this->cachePool->clear(); } /** @@ -290,7 +285,7 @@ private function normalizeFilename(SplFileInfo $file): string */ private function getCachedMetrics(SplFileInfo $file, string $configHash, bool $useCache): array { - if (!$useCache || $this->cachePool === null) { + if (!$useCache) { return ['metrics' => null, 'cacheItem' => null]; } @@ -312,6 +307,7 @@ private function getCachedMetrics(SplFileInfo $file, string $configHash, bool $u * Process a single file and parse its metrics * * @return array|null + * @throws ExceptionInterface */ private function processFile( SplFileInfo $file, diff --git a/src/Business/Cognitive/Parser.php b/src/Business/Cognitive/Parser.php index c2b8540..a365adf 100644 --- a/src/Business/Cognitive/Parser.php +++ b/src/Business/Cognitive/Parser.php @@ -7,13 +7,13 @@ use Phauthentic\CognitiveCodeAnalysis\CognitiveAnalysisException; use Phauthentic\CognitiveCodeAnalysis\PhpParser\AnnotationVisitor; use Phauthentic\CognitiveCodeAnalysis\PhpParser\CognitiveMetricsVisitor; +use Phauthentic\CognitiveCodeAnalysis\PhpParser\CombinedMetricsVisitor; use Phauthentic\CognitiveCodeAnalysis\PhpParser\CyclomaticComplexityVisitor; use Phauthentic\CognitiveCodeAnalysis\PhpParser\HalsteadMetricsVisitor; -use Phauthentic\CognitiveCodeAnalysis\PhpParser\CombinedMetricsVisitor; +use PhpParser\Error; +use PhpParser\NodeTraverser; use PhpParser\NodeTraverserInterface; use PhpParser\Parser as PhpParser; -use PhpParser\NodeTraverser; -use PhpParser\Error; use PhpParser\ParserFactory; use ReflectionClass; @@ -187,7 +187,7 @@ public function clearStaticCaches(): void $this->clearStaticProperty('Phauthentic\CognitiveCodeAnalysis\PhpParser\AnnotationVisitor', 'fqcnCache'); // Clear regex pattern caches - $this->clearStaticProperty('Phauthentic\CognitiveCodeAnalysis\Business\DirectoryScanner', 'compiledPatterns'); + $this->clearStaticProperty('Phauthentic\CognitiveCodeAnalysis\Business\Utility\DirectoryScanner', 'compiledPatterns'); $this->clearStaticProperty('Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\CognitiveMetricsCollector', 'compiledPatterns'); // Clear accumulated data in visitors diff --git a/src/Business/MetricsFacade.php b/src/Business/MetricsFacade.php index d8b7b34..d17a9fc 100644 --- a/src/Business/MetricsFacade.php +++ b/src/Business/MetricsFacade.php @@ -4,7 +4,6 @@ namespace Phauthentic\CognitiveCodeAnalysis\Business; -use JsonException; use Phauthentic\CognitiveCodeAnalysis\Business\Churn\ChangeCounter\ChangeCounterFactory; use Phauthentic\CognitiveCodeAnalysis\Business\Churn\ChurnCalculator; use Phauthentic\CognitiveCodeAnalysis\Business\Churn\Exporter\ChurnExporterFactory; @@ -17,6 +16,7 @@ use Phauthentic\CognitiveCodeAnalysis\CognitiveAnalysisException; use Phauthentic\CognitiveCodeAnalysis\Config\CognitiveConfig; use Phauthentic\CognitiveCodeAnalysis\Config\ConfigService; +use Symfony\Component\Messenger\Exception\ExceptionInterface; /** * Facade class for collecting and managing code quality metrics. @@ -67,7 +67,7 @@ private function getCognitiveExporterFactory(): CognitiveExporterFactory * @param string $path The file or directory path to collect metrics from. * @return CognitiveMetricsCollection The collected cognitive metrics. * @throws CognitiveAnalysisException - * @throws \Symfony\Component\Messenger\Exception\ExceptionInterface + * @throws ExceptionInterface */ public function getCognitiveMetrics(string $path): CognitiveMetricsCollection { @@ -87,7 +87,7 @@ public function getCognitiveMetrics(string $path): CognitiveMetricsCollection * @param CoverageReportReaderInterface|null $coverageReader Optional coverage reader for coverage data. * @return CognitiveMetricsCollection The collected cognitive metrics from all paths. * @throws CognitiveAnalysisException - * @throws \Symfony\Component\Messenger\Exception\ExceptionInterface + * @throws ExceptionInterface */ public function getCognitiveMetricsFromPaths(array $paths, ?CoverageReportReaderInterface $coverageReader = null): CognitiveMetricsCollection { @@ -112,6 +112,8 @@ public function getCognitiveMetricsFromPaths(array $paths, ?CoverageReportReader * @param string $since * @param CoverageReportReaderInterface|null $coverageReader * @return array> + * @throws CognitiveAnalysisException + * @throws ExceptionInterface */ public function calculateChurn( string $path, diff --git a/src/Business/DirectoryScanner.php b/src/Business/Utility/DirectoryScanner.php similarity index 98% rename from src/Business/DirectoryScanner.php rename to src/Business/Utility/DirectoryScanner.php index 207d304..9f7f97b 100644 --- a/src/Business/DirectoryScanner.php +++ b/src/Business/Utility/DirectoryScanner.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Phauthentic\CognitiveCodeAnalysis\Business; +namespace Phauthentic\CognitiveCodeAnalysis\Business\Utility; use FilesystemIterator; use Generator; diff --git a/src/Command/CognitiveMetricsCommand.php b/src/Command/CognitiveMetricsCommand.php index e1c87e0..1f31ff9 100644 --- a/src/Command/CognitiveMetricsCommand.php +++ b/src/Command/CognitiveMetricsCommand.php @@ -157,6 +157,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int return Command::FAILURE; } + if ($input->getOption(self::OPTION_CLEAR_CACHE)) { + $this->metricsFacade->clearCache(); + } + // Handle cache directory override $cacheDir = $input->getOption(self::OPTION_CACHE_DIR); if ($cacheDir && $this->metricsFacade->getConfig()->cache !== null) { @@ -169,7 +173,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->metricsFacade->getConfig()->cache->enabled = false; } - $metricsCollection = $this->metricsFacade->getCognitiveMetricsFromPaths($paths); $coverageReader = $this->handleCoverageOptions($input, $output); if ($coverageReader === false) { return Command::FAILURE; @@ -206,7 +209,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int private function parsePaths(string $pathInput): array { $paths = array_map('trim', explode(',', $pathInput)); - return array_filter($paths, function ($path) { + return array_filter($paths, static function ($path) { return !empty($path); }); } diff --git a/tests/Fixtures/Coverage/coverage-clover.xml b/tests/Fixtures/Coverage/coverage-clover.xml index a6cc46f..44c2989 100644 --- a/tests/Fixtures/Coverage/coverage-clover.xml +++ b/tests/Fixtures/Coverage/coverage-clover.xml @@ -1886,7 +1886,7 @@ - + diff --git a/tests/Fixtures/Coverage/coverage.xml b/tests/Fixtures/Coverage/coverage.xml index fda4c47..35fbfa7 100644 --- a/tests/Fixtures/Coverage/coverage.xml +++ b/tests/Fixtures/Coverage/coverage.xml @@ -1755,7 +1755,7 @@ - + @@ -2788,7 +2788,7 @@ - + diff --git a/tests/Unit/Business/Cognitive/CognitiveMetricsCollectorTest.php b/tests/Unit/Business/Cognitive/CognitiveMetricsCollectorTest.php index 2637e8d..c6d2c3c 100644 --- a/tests/Unit/Business/Cognitive/CognitiveMetricsCollectorTest.php +++ b/tests/Unit/Business/Cognitive/CognitiveMetricsCollectorTest.php @@ -7,7 +7,9 @@ use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\CognitiveMetricsCollection; use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\CognitiveMetricsCollector; use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Parser; -use Phauthentic\CognitiveCodeAnalysis\Business\DirectoryScanner; +use Phauthentic\CognitiveCodeAnalysis\Business\Utility\DirectoryScanner; +use Phauthentic\CognitiveCodeAnalysis\Cache\Exception\CacheException; +use Phauthentic\CognitiveCodeAnalysis\Cache\FileCache; use Phauthentic\CognitiveCodeAnalysis\CognitiveAnalysisException; use Phauthentic\CognitiveCodeAnalysis\Config\CognitiveConfig; use Phauthentic\CognitiveCodeAnalysis\Config\ConfigLoader; @@ -18,6 +20,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Config\Definition\Processor; use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Exception\ExceptionInterface; use Symfony\Component\Messenger\MessageBusInterface; /** @@ -29,6 +32,9 @@ class CognitiveMetricsCollectorTest extends TestCase private ConfigService $configService; private MessageBusInterface $messageBus; + /** + * @throws CacheException + */ protected function setUp(): void { parent::setUp(); @@ -53,7 +59,8 @@ protected function setUp(): void new Processor(), new ConfigLoader(), ), - $bus + $bus, + new FileCache(sys_get_temp_dir()) ); $this->configService = new ConfigService( @@ -91,7 +98,8 @@ public function testCollectWithExcludedClasses(): void ), new DirectoryScanner(), $configService, - $this->messageBus + $this->messageBus, + new FileCache(sys_get_temp_dir()) ); $path = './tests/TestCode'; @@ -357,6 +365,11 @@ public function testCollectFromPathsWithMixedTypes(): void $this->assertGreaterThan(0, $metricsCollection->count(), 'Should have metrics from directory and file'); } + /** + * @throws CognitiveAnalysisException + * @throws CacheException + * @throws ExceptionInterface + */ #[Test] public function testFindSourceFilesExcludePatternsNotMergedProperly(): void { @@ -372,7 +385,8 @@ public function testFindSourceFilesExcludePatternsNotMergedProperly(): void ), new DirectoryScanner(), $configService, - $this->messageBus + $this->messageBus, + new FileCache(sys_get_temp_dir()) ); $excludePatterns = ['Paginator\.php$', 'FileWithTwoClasses\.php$']; diff --git a/tests/Unit/Business/DirectoryScannerTest.php b/tests/Unit/Business/DirectoryScannerTest.php index 6c4c350..1202e9b 100644 --- a/tests/Unit/Business/DirectoryScannerTest.php +++ b/tests/Unit/Business/DirectoryScannerTest.php @@ -5,7 +5,6 @@ namespace Phauthentic\CognitiveCodeAnalysis\Tests\Unit\Business; use FilesystemIterator; -use Phauthentic\CognitiveCodeAnalysis\Business\DirectoryScanner; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; use RecursiveDirectoryIterator; @@ -74,7 +73,7 @@ private function deleteDirectory(string $dir): void #[Test] public function testScan(): void { - $scanner = new DirectoryScanner(); + $scanner = new \Phauthentic\CognitiveCodeAnalysis\Business\Utility\DirectoryScanner(); $excludePatterns = ['exclude_me', 'exclude_me_too']; $files = []; diff --git a/tests/Unit/Command/CognitiveMetricsCommandTest.php b/tests/Unit/Command/CognitiveMetricsCommandTest.php index 832cb9a..5df3108 100644 --- a/tests/Unit/Command/CognitiveMetricsCommandTest.php +++ b/tests/Unit/Command/CognitiveMetricsCommandTest.php @@ -267,7 +267,7 @@ public static function multiplePathsDataProvider(): array 'Command should succeed with multiple files' ], 'multiple files with spaces' => [ - __DIR__ . '/../../../src/Command/CognitiveMetricsCommand.php, ' . __DIR__ . '/../../../src/Business/MetricsFacade.php, ' . __DIR__ . '/../../../src/Business/DirectoryScanner.php', + __DIR__ . '/../../../src/Command/CognitiveMetricsCommand.php, ' . __DIR__ . '/../../../src/Business/MetricsFacade.php, ' . __DIR__ . '/../../../src/Business/Utility/DirectoryScanner.php', 'Command should succeed with multiple files and spaces' ], 'multiple directories' => [ @@ -279,7 +279,7 @@ public static function multiplePathsDataProvider(): array 'Command should succeed with mixed directories and files' ], 'mixed paths with spaces' => [ - __DIR__ . '/../../../src/Command, ' . __DIR__ . '/../../../src/Business/MetricsFacade.php, ' . __DIR__ . '/../../../src/Business/DirectoryScanner.php', + __DIR__ . '/../../../src/Command, ' . __DIR__ . '/../../../src/Business/MetricsFacade.php, ' . __DIR__ . '/../../../src/Business/Utility/DirectoryScanner.php', 'Command should succeed with mixed paths and spaces' ], ]; From ca35b72f6c9c5ecf50d6cfaed0a5afd3bb153717 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Kr=C3=A4mer?= Date: Thu, 9 Oct 2025 01:33:37 +0200 Subject: [PATCH 11/17] Testing the Cache Implementation --- src/Cache/FileCache.php | 16 +- src/Config/CacheConfig.php | 2 - src/Config/ConfigFactory.php | 1 - src/Config/ConfigLoader.php | 3 - tests/Unit/Cache/CacheItemTest.php | 188 ++++++++ .../Cache/Exception/CacheExceptionTest.php | 124 +++++ tests/Unit/Cache/FileCacheTest.php | 454 ++++++++++++++++++ 7 files changed, 774 insertions(+), 14 deletions(-) create mode 100644 tests/Unit/Cache/CacheItemTest.php create mode 100644 tests/Unit/Cache/Exception/CacheExceptionTest.php create mode 100644 tests/Unit/Cache/FileCacheTest.php diff --git a/src/Cache/FileCache.php b/src/Cache/FileCache.php index db86294..8cf6fdd 100644 --- a/src/Cache/FileCache.php +++ b/src/Cache/FileCache.php @@ -9,7 +9,7 @@ use Psr\Cache\CacheItemPoolInterface; /** - * PSR-6 File-based Cache implementation with compression support + * PSR-6 File-based Cache implementation */ class FileCache implements CacheItemPoolInterface { @@ -152,7 +152,7 @@ private function ensureCacheDirectory(): void { if ( !is_dir($this->cacheDirectory) - && !mkdir($this->cacheDirectory, 0755, true) + && !@mkdir($this->cacheDirectory, 0755, true) ) { throw new CacheException("Failed to create cache directory: {$this->cacheDirectory}"); } @@ -171,7 +171,7 @@ private function getCacheFilePath(string $key): string if ( !is_dir($dir) - && !mkdir($dir, 0755, true) + && !@mkdir($dir, 0755, true) ) { throw new CacheException("Failed to create cache subdirectory: {$dir}"); } @@ -179,15 +179,15 @@ private function getCacheFilePath(string $key): string return $dir . '/' . $hash . '.cache'; } - /** @return array|null */ - private function loadCacheData(string $filePath): ?array + /** @return mixed|null */ + private function loadCacheData(string $filePath): mixed { $content = file_get_contents($filePath); if ($content === false) { return null; } - $data = json_decode($content, true); + $data = json_decode($content, false); if ($data === null) { return null; } @@ -200,9 +200,9 @@ private function loadCacheData(string $filePath): ?array * * Sanitize data to ensure valid UTF-8 encoding * - * @param array $data + * @param mixed $data */ - private function saveCacheData(string $filePath, array $data): bool + private function saveCacheData(string $filePath, mixed $data): bool { $data = $this->sanitizeUtf8($data); diff --git a/src/Config/CacheConfig.php b/src/Config/CacheConfig.php index 962c465..78296a4 100644 --- a/src/Config/CacheConfig.php +++ b/src/Config/CacheConfig.php @@ -12,7 +12,6 @@ class CacheConfig public function __construct( public bool $enabled, public string $directory, - public bool $compression, ) { } @@ -26,7 +25,6 @@ public function toArray(): array return [ 'enabled' => $this->enabled, 'directory' => $this->directory, - 'compression' => $this->compression, ]; } } diff --git a/src/Config/ConfigFactory.php b/src/Config/ConfigFactory.php index 6ecfead..4379eaf 100644 --- a/src/Config/ConfigFactory.php +++ b/src/Config/ConfigFactory.php @@ -28,7 +28,6 @@ public function fromArray(array $config): CognitiveConfig $cacheConfig = new CacheConfig( enabled: $config['cognitive']['cache']['enabled'] ?? true, directory: $config['cognitive']['cache']['directory'] ?? './.phpcca.cache', - compression: $config['cognitive']['cache']['compression'] ?? true, ); } diff --git a/src/Config/ConfigLoader.php b/src/Config/ConfigLoader.php index da0fd52..6f493ea 100644 --- a/src/Config/ConfigLoader.php +++ b/src/Config/ConfigLoader.php @@ -137,9 +137,6 @@ public function getConfigTreeBuilder(): TreeBuilder ->scalarNode('directory') ->defaultValue('./.phpcca.cache') ->end() - ->booleanNode('compression') - ->defaultValue(true) - ->end() ->end() ->end() ->end() diff --git a/tests/Unit/Cache/CacheItemTest.php b/tests/Unit/Cache/CacheItemTest.php new file mode 100644 index 0000000..ecedbe7 --- /dev/null +++ b/tests/Unit/Cache/CacheItemTest.php @@ -0,0 +1,188 @@ +assertEquals($key, $item->getKey()); + $this->assertEquals($value, $item->get()); + $this->assertTrue($item->isHit()); + } + + public function testConstructorWithMiss(): void + { + $key = 'test-key'; + $value = null; + $isHit = false; + + $item = new CacheItem($key, $value, $isHit); + + $this->assertEquals($key, $item->getKey()); + $this->assertNull($item->get()); + $this->assertFalse($item->isHit()); + } + + public function testSetValue(): void + { + $item = new CacheItem('test-key', 'initial-value', true); + + $newValue = 'new-value'; + $result = $item->set($newValue); + + $this->assertSame($item, $result); + $this->assertEquals($newValue, $item->get()); + } + + public function testSetWithDifferentTypes(): void + { + $item = new CacheItem('test-key', null, false); + + // Test with string + $item->set('string-value'); + $this->assertEquals('string-value', $item->get()); + + // Test with array + $arrayValue = ['key' => 'value', 'number' => 123]; + $item->set($arrayValue); + $this->assertEquals($arrayValue, $item->get()); + + // Test with object + $objectValue = (object) ['property' => 'value']; + $item->set($objectValue); + $this->assertEquals($objectValue, $item->get()); + + // Test with boolean + $item->set(true); + $this->assertTrue($item->get()); + + // Test with integer + $item->set(42); + $this->assertEquals(42, $item->get()); + + // Test with float + $item->set(3.14); + $this->assertEquals(3.14, $item->get()); + } + + public function testSetExpirationReturnsSelf(): void + { + $item = new CacheItem('test-key', 'value', true); + + $result = $item->setExpiration(3600); + + $this->assertSame($item, $result); + } + + public function testGetExpirationReturnsNull(): void + { + $item = new CacheItem('test-key', 'value', true); + + $this->assertNull($item->getExpiration()); + } + + public function testExpiresAtReturnsSelf(): void + { + $item = new CacheItem('test-key', 'value', true); + $expiration = new \DateTime('+1 hour'); + + $result = $item->expiresAt($expiration); + + $this->assertSame($item, $result); + } + + public function testExpiresAtWithNull(): void + { + $item = new CacheItem('test-key', 'value', true); + + $result = $item->expiresAt(null); + + $this->assertSame($item, $result); + } + + public function testExpiresAfterWithInteger(): void + { + $item = new CacheItem('test-key', 'value', true); + + $result = $item->expiresAfter(3600); + + $this->assertSame($item, $result); + } + + public function testExpiresAfterWithDateInterval(): void + { + $item = new CacheItem('test-key', 'value', true); + $interval = new \DateInterval('PT1H'); + + $result = $item->expiresAfter($interval); + + $this->assertSame($item, $result); + } + + public function testExpiresAfterWithNull(): void + { + $item = new CacheItem('test-key', 'value', true); + + $result = $item->expiresAfter(null); + + $this->assertSame($item, $result); + } + + public function testKeyIsImmutable(): void + { + $key = 'original-key'; + $item = new CacheItem($key, 'value', true); + + // The key should remain the same throughout the item's lifecycle + $this->assertEquals($key, $item->getKey()); + + $item->set('new-value'); + $this->assertEquals($key, $item->getKey()); + + $item->setExpiration(3600); + $this->assertEquals($key, $item->getKey()); + } + + public function testIsHitIsImmutable(): void + { + $item = new CacheItem('test-key', 'value', true); + + // isHit should remain true + $this->assertTrue($item->isHit()); + + $item->set('new-value'); + $this->assertTrue($item->isHit()); + + $item->setExpiration(3600); + $this->assertTrue($item->isHit()); + } + + public function testIsHitIsImmutableForMiss(): void + { + $item = new CacheItem('test-key', null, false); + + // isHit should remain false + $this->assertFalse($item->isHit()); + + $item->set('new-value'); + $this->assertFalse($item->isHit()); + + $item->setExpiration(3600); + $this->assertFalse($item->isHit()); + } +} diff --git a/tests/Unit/Cache/Exception/CacheExceptionTest.php b/tests/Unit/Cache/Exception/CacheExceptionTest.php new file mode 100644 index 0000000..4fe8c76 --- /dev/null +++ b/tests/Unit/Cache/Exception/CacheExceptionTest.php @@ -0,0 +1,124 @@ +assertInstanceOf(CognitiveAnalysisException::class, $exception); + $this->assertInstanceOf(\Exception::class, $exception); + } + + public function testDefaultConstructor(): void + { + $exception = new CacheException(); + + $this->assertEquals('', $exception->getMessage()); + $this->assertEquals(0, $exception->getCode()); + $this->assertNull($exception->getPrevious()); + } + + public function testConstructorWithMessage(): void + { + $message = 'Cache operation failed'; + $exception = new CacheException($message); + + $this->assertEquals($message, $exception->getMessage()); + $this->assertEquals(0, $exception->getCode()); + $this->assertNull($exception->getPrevious()); + } + + public function testConstructorWithMessageAndCode(): void + { + $message = 'Cache operation failed'; + $code = 500; + $exception = new CacheException($message, $code); + + $this->assertEquals($message, $exception->getMessage()); + $this->assertEquals($code, $exception->getCode()); + $this->assertNull($exception->getPrevious()); + } + + public function testConstructorWithMessageCodeAndPrevious(): void + { + $message = 'Cache operation failed'; + $code = 500; + $previous = new \RuntimeException('Previous exception'); + $exception = new CacheException($message, $code, $previous); + + $this->assertEquals($message, $exception->getMessage()); + $this->assertEquals($code, $exception->getCode()); + $this->assertSame($previous, $exception->getPrevious()); + } + + public function testCanBeThrownAndCaught(): void + { + $this->expectException(CacheException::class); + $this->expectExceptionMessage('Test exception message'); + + throw new CacheException('Test exception message'); + } + + public function testCanBeCaughtAsParentException(): void + { + $caught = false; + + try { + throw new CacheException('Test message'); + } catch (CognitiveAnalysisException $e) { + $caught = true; + $this->assertEquals('Test message', $e->getMessage()); + } + + $this->assertTrue($caught); + } + + public function testCanBeCaughtAsGenericException(): void + { + $caught = false; + + try { + throw new CacheException('Test message'); + } catch (\Exception $e) { + $caught = true; + $this->assertEquals('Test message', $e->getMessage()); + } + + $this->assertTrue($caught); + } + + public function testExceptionWithSpecialCharacters(): void + { + $message = 'Cache failed: "Invalid UTF-8 sequence \x80"'; + $exception = new CacheException($message); + + $this->assertEquals($message, $exception->getMessage()); + } + + public function testExceptionWithEmptyMessage(): void + { + $exception = new CacheException(''); + + $this->assertEquals('', $exception->getMessage()); + } + + public function testExceptionWithVeryLongMessage(): void + { + $message = str_repeat('A', 1000); + $exception = new CacheException($message); + + $this->assertEquals($message, $exception->getMessage()); + } +} diff --git a/tests/Unit/Cache/FileCacheTest.php b/tests/Unit/Cache/FileCacheTest.php new file mode 100644 index 0000000..73ea1cd --- /dev/null +++ b/tests/Unit/Cache/FileCacheTest.php @@ -0,0 +1,454 @@ +testCacheDir = sys_get_temp_dir() . '/phpcca-cache-test-' . uniqid(); + $this->cache = new FileCache($this->testCacheDir); + } + + protected function tearDown(): void + { + if (is_dir($this->testCacheDir)) { + $this->removeDirectory($this->testCacheDir); + } + } + + public function testConstructorCreatesCacheDirectory(): void + { + $this->assertDirectoryExists($this->testCacheDir); + $this->assertDirectoryIsWritable($this->testCacheDir); + } + + public function testConstructorWithTrailingSlash(): void + { + $cacheDirWithSlash = $this->testCacheDir . '/'; + $cache = new FileCache($cacheDirWithSlash); + + $this->assertDirectoryExists($this->testCacheDir); + } + + public function testGetItemReturnsMissForNonExistentKey(): void + { + $item = $this->cache->getItem('non-existent-key'); + + $this->assertEquals('non-existent-key', $item->getKey()); + $this->assertNull($item->get()); + $this->assertFalse($item->isHit()); + } + + public function testSaveAndGetItem(): void + { + $key = 'test-key'; + $value = 'test-value'; + + $item = new CacheItem($key, $value, true); + $this->assertTrue($this->cache->save($item)); + + $retrievedItem = $this->cache->getItem($key); + $this->assertEquals($key, $retrievedItem->getKey()); + $this->assertEquals($value, $retrievedItem->get()); + $this->assertTrue($retrievedItem->isHit()); + } + + public function testSaveAndGetItemWithArray(): void + { + $key = 'array-key'; + $value = ['key1' => 'value1', 'key2' => 123, 'key3' => ['nested' => true]]; + + $item = new CacheItem($key, $value, true); + $this->assertTrue($this->cache->save($item)); + + $retrievedItem = $this->cache->getItem($key); + $retrievedValue = $retrievedItem->get(); + $this->assertIsObject($retrievedValue); + $this->assertEquals('value1', $retrievedValue->key1); + $this->assertEquals(123, $retrievedValue->key2); + $this->assertIsObject($retrievedValue->key3); + $this->assertTrue($retrievedValue->key3->nested); + $this->assertTrue($retrievedItem->isHit()); + } + + public function testSaveAndGetItemWithObject(): void + { + $key = 'object-key'; + $value = (object) ['property1' => 'value1', 'property2' => 456]; + + $item = new CacheItem($key, $value, true); + $this->assertTrue($this->cache->save($item)); + + $retrievedItem = $this->cache->getItem($key); + $retrievedValue = $retrievedItem->get(); + $this->assertIsObject($retrievedValue); + $this->assertEquals('value1', $retrievedValue->property1); + $this->assertEquals(456, $retrievedValue->property2); + $this->assertTrue($retrievedItem->isHit()); + } + + public function testSaveAndGetItemWithNullValue(): void + { + $key = 'null-key'; + + $item = new CacheItem($key, null, true); + $this->assertTrue($this->cache->save($item)); + + // Saving null should delete the item + $this->assertFalse($this->cache->hasItem($key)); + } + + public function testHasItem(): void + { + $key = 'has-item-test'; + + $this->assertFalse($this->cache->hasItem($key)); + + $item = new CacheItem($key, 'value', true); + $this->cache->save($item); + + $this->assertTrue($this->cache->hasItem($key)); + } + + public function testDeleteItem(): void + { + $key = 'delete-test'; + + // Save an item first + $item = new CacheItem($key, 'value', true); + $this->cache->save($item); + $this->assertTrue($this->cache->hasItem($key)); + + // Delete the item + $this->assertTrue($this->cache->deleteItem($key)); + $this->assertFalse($this->cache->hasItem($key)); + } + + public function testDeleteNonExistentItem(): void + { + $this->assertTrue($this->cache->deleteItem('non-existent-key')); + } + + public function testDeleteItems(): void + { + $keys = ['key1', 'key2', 'key3']; + + // Save items first + foreach ($keys as $key) { + $item = new CacheItem($key, "value-{$key}", true); + $this->cache->save($item); + } + + // Verify all items exist + foreach ($keys as $key) { + $this->assertTrue($this->cache->hasItem($key)); + } + + // Delete all items + $this->assertTrue($this->cache->deleteItems($keys)); + + // Verify all items are deleted + foreach ($keys as $key) { + $this->assertFalse($this->cache->hasItem($key)); + } + } + + public function testGetItems(): void + { + $keys = ['key1', 'key2', 'key3']; + + // Save some items + $item1 = new CacheItem('key1', 'value1', true); + $item2 = new CacheItem('key2', 'value2', true); + $this->cache->save($item1); + $this->cache->save($item2); + + $items = $this->cache->getItems($keys); + + $this->assertCount(3, $items); + $this->assertArrayHasKey('key1', $items); + $this->assertArrayHasKey('key2', $items); + $this->assertArrayHasKey('key3', $items); + + $this->assertTrue($items['key1']->isHit()); + $this->assertEquals('value1', $items['key1']->get()); + + $this->assertTrue($items['key2']->isHit()); + $this->assertEquals('value2', $items['key2']->get()); + + $this->assertFalse($items['key3']->isHit()); + $this->assertNull($items['key3']->get()); + } + + public function testSaveDeferredAndCommit(): void + { + $key1 = 'deferred-key1'; + $key2 = 'deferred-key2'; + + $item1 = new CacheItem($key1, 'value1', true); + $item2 = new CacheItem($key2, 'value2', true); + + $this->assertTrue($this->cache->saveDeferred($item1)); + $this->assertTrue($this->cache->saveDeferred($item2)); + + // Items should not be saved yet + $this->assertFalse($this->cache->hasItem($key1)); + $this->assertFalse($this->cache->hasItem($key2)); + + // Commit the deferred items + $this->assertTrue($this->cache->commit()); + + // Now items should be saved + $this->assertTrue($this->cache->hasItem($key1)); + $this->assertTrue($this->cache->hasItem($key2)); + + $retrievedItem1 = $this->cache->getItem($key1); + $this->assertEquals('value1', $retrievedItem1->get()); + + $retrievedItem2 = $this->cache->getItem($key2); + $this->assertEquals('value2', $retrievedItem2->get()); + } + + public function testClear(): void + { + // Save some items + $item1 = new CacheItem('key1', 'value1', true); + $item2 = new CacheItem('key2', 'value2', true); + $this->cache->save($item1); + $this->cache->save($item2); + + $this->assertTrue($this->cache->hasItem('key1')); + $this->assertTrue($this->cache->hasItem('key2')); + + // Clear the cache + $this->assertTrue($this->cache->clear()); + + // Items should be gone + $this->assertFalse($this->cache->hasItem('key1')); + $this->assertFalse($this->cache->hasItem('key2')); + + // Cache directory should still exist + $this->assertDirectoryExists($this->testCacheDir); + } + + public function testCacheFileStructure(): void + { + $key = 'test-structure'; + $value = 'test-value'; + + $item = new CacheItem($key, $value, true); + $this->cache->save($item); + + // Check that subdirectory was created + $hash = md5($key); + $subDir = substr($hash, 0, 2); + $expectedSubDir = $this->testCacheDir . '/' . $subDir; + + $this->assertDirectoryExists($expectedSubDir); + + // Check that cache file was created + $expectedFile = $expectedSubDir . '/' . $hash . '.cache'; + $this->assertFileExists($expectedFile); + + // Verify file content + $content = file_get_contents($expectedFile); + $this->assertNotFalse($content); + + $data = json_decode($content, false); + $this->assertEquals($value, $data); + } + + public function testUtf8Sanitization(): void + { + $key = 'utf8-test'; + $value = [ + 'valid_utf8' => 'Hello World', + 'invalid_utf8' => "Invalid \x80 sequence", + 'mixed' => "Valid text with \x80 invalid chars", + 'unicode' => 'Unicode: 你好世界 🌍' + ]; + + $item = new CacheItem($key, $value, true); + $this->assertTrue($this->cache->save($item)); + + $retrievedItem = $this->cache->getItem($key); + $retrievedValue = $retrievedItem->get(); + + $this->assertIsObject($retrievedValue); + $this->assertObjectHasProperty('valid_utf8', $retrievedValue); + $this->assertObjectHasProperty('invalid_utf8', $retrievedValue); + $this->assertObjectHasProperty('mixed', $retrievedValue); + $this->assertObjectHasProperty('unicode', $retrievedValue); + + // Verify UTF-8 sanitization worked + $this->assertIsString($retrievedValue->invalid_utf8); + $this->assertIsString($retrievedValue->mixed); + } + + public function testCorruptedCacheFile(): void + { + $key = 'corrupted-test'; + $value = 'test-value'; + + // Save a valid item first + $item = new CacheItem($key, $value, true); + $this->cache->save($item); + + // Corrupt the cache file + $hash = md5($key); + $subDir = substr($hash, 0, 2); + $cacheFile = $this->testCacheDir . '/' . $subDir . '/' . $hash . '.cache'; + + file_put_contents($cacheFile, 'invalid json content'); + + // Should return a miss for corrupted file + $retrievedItem = $this->cache->getItem($key); + $this->assertFalse($retrievedItem->isHit()); + $this->assertNull($retrievedItem->get()); + } + + public function testEmptyCacheFile(): void + { + $key = 'empty-test'; + $value = 'test-value'; + + // Save a valid item first + $item = new CacheItem($key, $value, true); + $this->cache->save($item); + + // Empty the cache file + $hash = md5($key); + $subDir = substr($hash, 0, 2); + $cacheFile = $this->testCacheDir . '/' . $subDir . '/' . $hash . '.cache'; + + file_put_contents($cacheFile, ''); + + // Should return a miss for empty file + $retrievedItem = $this->cache->getItem($key); + $this->assertFalse($retrievedItem->isHit()); + $this->assertNull($retrievedItem->get()); + } + + public function testCacheDirectoryCreationFailure(): void + { + // Create a cache directory that's not writable + $nonWritableDir = '/root/non-writable-cache'; + + $this->expectException(CacheException::class); + $this->expectExceptionMessage('Failed to create cache directory'); + + new FileCache($nonWritableDir); + } + + public function testCacheSubdirectoryCreationFailure(): void + { + // Create a cache with a non-writable parent directory + $parentDir = $this->testCacheDir . '/parent'; + mkdir($parentDir, 0444); // Read-only + + $this->expectException(CacheException::class); + $this->expectExceptionMessage('Failed to create cache directory'); + + $cache = new FileCache($parentDir . '/cache'); + + // Try to save an item which will trigger subdirectory creation + $item = new CacheItem('test-key', 'test-value', true); + $cache->save($item); + } + + public function testJsonEncodeFailure(): void + { + // Create a value that cannot be JSON encoded + $key = 'json-fail-test'; + $value = "\x80"; // Invalid UTF-8 that might cause JSON encoding issues + + $item = new CacheItem($key, $value, true); + + // This should handle the encoding gracefully + $result = $this->cache->save($item); + + // The save might succeed due to UTF-8 sanitization + if ($result) { + $retrievedItem = $this->cache->getItem($key); + $this->assertTrue($retrievedItem->isHit()); + } + } + + public function testLargeDataHandling(): void + { + $key = 'large-data-test'; + $value = str_repeat('A', 10000); // 10KB string + + $item = new CacheItem($key, $value, true); + $this->assertTrue($this->cache->save($item)); + + $retrievedItem = $this->cache->getItem($key); + $this->assertEquals($value, $retrievedItem->get()); + $this->assertTrue($retrievedItem->isHit()); + } + + public function testSpecialCharactersInKey(): void + { + $keys = [ + 'key with spaces', + 'key-with-dashes', + 'key_with_underscores', + 'key.with.dots', + 'key/with/slashes', + 'key\\with\\backslashes', + 'key:with:colons', + 'key;with;semicolons', + 'key"with"quotes', + "key'with'singlequotes" + ]; + + foreach ($keys as $key) { + $value = "value-for-{$key}"; + $item = new CacheItem($key, $value, true); + + $this->assertTrue($this->cache->save($item), "Failed to save key: {$key}"); + $this->assertTrue($this->cache->hasItem($key), "Failed to verify key exists: {$key}"); + + $retrievedItem = $this->cache->getItem($key); + $this->assertEquals($value, $retrievedItem->get(), "Failed to retrieve value for key: {$key}"); + } + } + + private function removeDirectory(string $dir): void + { + if (!is_dir($dir)) { + return; + } + + $scanResult = scandir($dir); + if ($scanResult === false) { + return; + } + + $files = array_diff($scanResult, ['.', '..']); + foreach ($files as $file) { + $path = $dir . '/' . $file; + if (is_dir($path)) { + $this->removeDirectory($path); + continue; + } + unlink($path); + } + + rmdir($dir); + } +} From b4b401439b214bd859ae6260d72e8c337fe666ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Kr=C3=A4mer?= Date: Thu, 9 Oct 2025 01:36:55 +0200 Subject: [PATCH 12/17] Testing the Cache Implementation --- config.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/config.yml b/config.yml index 3c45091..7653736 100644 --- a/config.yml +++ b/config.yml @@ -41,6 +41,5 @@ cognitive: scale: 1.0 enabled: true cache: - enabled: true + enabled: false directory: './.phpcca.cache' - compression: true From 93c713bf02a3a7d80f513bd505203e96bc797c96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Kr=C3=A4mer?= Date: Thu, 9 Oct 2025 01:47:13 +0200 Subject: [PATCH 13/17] Testing the Cache Implementation --- src/Cache/FileCache.php | 8 +- tests/Unit/Cache/CacheItemTest.php | 44 +++--- .../Cache/Exception/CacheExceptionTest.php | 26 ++-- tests/Unit/Cache/FileCacheTest.php | 134 +++++++++--------- 4 files changed, 108 insertions(+), 104 deletions(-) diff --git a/src/Cache/FileCache.php b/src/Cache/FileCache.php index 8cf6fdd..ac72e4d 100644 --- a/src/Cache/FileCache.php +++ b/src/Cache/FileCache.php @@ -146,6 +146,7 @@ public function commit(): bool } /** + * @SuppressWarnings("PHPMD.ErrorControlOperator") * @throws CacheException */ private function ensureCacheDirectory(): void @@ -161,6 +162,7 @@ private function ensureCacheDirectory(): void /** * Create subdirectories to avoid too many files in one directory * + * @SuppressWarnings("PHPMD.ErrorControlOperator") * @throws CacheException */ private function getCacheFilePath(string $key): string @@ -171,7 +173,7 @@ private function getCacheFilePath(string $key): string if ( !is_dir($dir) - && !@mkdir($dir, 0755, true) + && @!mkdir($dir, 0755, true) ) { throw new CacheException("Failed to create cache subdirectory: {$dir}"); } @@ -179,7 +181,9 @@ private function getCacheFilePath(string $key): string return $dir . '/' . $hash . '.cache'; } - /** @return mixed|null */ + /** + * @return mixed|null + */ private function loadCacheData(string $filePath): mixed { $content = file_get_contents($filePath); diff --git a/tests/Unit/Cache/CacheItemTest.php b/tests/Unit/Cache/CacheItemTest.php index ecedbe7..cbcee42 100644 --- a/tests/Unit/Cache/CacheItemTest.php +++ b/tests/Unit/Cache/CacheItemTest.php @@ -41,7 +41,7 @@ public function testConstructorWithMiss(): void public function testSetValue(): void { $item = new CacheItem('test-key', 'initial-value', true); - + $newValue = 'new-value'; $result = $item->set($newValue); @@ -83,16 +83,16 @@ public function testSetWithDifferentTypes(): void public function testSetExpirationReturnsSelf(): void { $item = new CacheItem('test-key', 'value', true); - + $result = $item->setExpiration(3600); - + $this->assertSame($item, $result); } public function testGetExpirationReturnsNull(): void { $item = new CacheItem('test-key', 'value', true); - + $this->assertNull($item->getExpiration()); } @@ -100,27 +100,27 @@ public function testExpiresAtReturnsSelf(): void { $item = new CacheItem('test-key', 'value', true); $expiration = new \DateTime('+1 hour'); - + $result = $item->expiresAt($expiration); - + $this->assertSame($item, $result); } public function testExpiresAtWithNull(): void { $item = new CacheItem('test-key', 'value', true); - + $result = $item->expiresAt(null); - + $this->assertSame($item, $result); } public function testExpiresAfterWithInteger(): void { $item = new CacheItem('test-key', 'value', true); - + $result = $item->expiresAfter(3600); - + $this->assertSame($item, $result); } @@ -128,18 +128,18 @@ public function testExpiresAfterWithDateInterval(): void { $item = new CacheItem('test-key', 'value', true); $interval = new \DateInterval('PT1H'); - + $result = $item->expiresAfter($interval); - + $this->assertSame($item, $result); } public function testExpiresAfterWithNull(): void { $item = new CacheItem('test-key', 'value', true); - + $result = $item->expiresAfter(null); - + $this->assertSame($item, $result); } @@ -150,10 +150,10 @@ public function testKeyIsImmutable(): void // The key should remain the same throughout the item's lifecycle $this->assertEquals($key, $item->getKey()); - + $item->set('new-value'); $this->assertEquals($key, $item->getKey()); - + $item->setExpiration(3600); $this->assertEquals($key, $item->getKey()); } @@ -161,13 +161,13 @@ public function testKeyIsImmutable(): void public function testIsHitIsImmutable(): void { $item = new CacheItem('test-key', 'value', true); - + // isHit should remain true $this->assertTrue($item->isHit()); - + $item->set('new-value'); $this->assertTrue($item->isHit()); - + $item->setExpiration(3600); $this->assertTrue($item->isHit()); } @@ -175,13 +175,13 @@ public function testIsHitIsImmutable(): void public function testIsHitIsImmutableForMiss(): void { $item = new CacheItem('test-key', null, false); - + // isHit should remain false $this->assertFalse($item->isHit()); - + $item->set('new-value'); $this->assertFalse($item->isHit()); - + $item->setExpiration(3600); $this->assertFalse($item->isHit()); } diff --git a/tests/Unit/Cache/Exception/CacheExceptionTest.php b/tests/Unit/Cache/Exception/CacheExceptionTest.php index 4fe8c76..7900d2e 100644 --- a/tests/Unit/Cache/Exception/CacheExceptionTest.php +++ b/tests/Unit/Cache/Exception/CacheExceptionTest.php @@ -16,7 +16,7 @@ class CacheExceptionTest extends TestCase public function testInheritsFromCognitiveAnalysisException(): void { $exception = new CacheException(); - + $this->assertInstanceOf(CognitiveAnalysisException::class, $exception); $this->assertInstanceOf(\Exception::class, $exception); } @@ -24,7 +24,7 @@ public function testInheritsFromCognitiveAnalysisException(): void public function testDefaultConstructor(): void { $exception = new CacheException(); - + $this->assertEquals('', $exception->getMessage()); $this->assertEquals(0, $exception->getCode()); $this->assertNull($exception->getPrevious()); @@ -34,7 +34,7 @@ public function testConstructorWithMessage(): void { $message = 'Cache operation failed'; $exception = new CacheException($message); - + $this->assertEquals($message, $exception->getMessage()); $this->assertEquals(0, $exception->getCode()); $this->assertNull($exception->getPrevious()); @@ -45,7 +45,7 @@ public function testConstructorWithMessageAndCode(): void $message = 'Cache operation failed'; $code = 500; $exception = new CacheException($message, $code); - + $this->assertEquals($message, $exception->getMessage()); $this->assertEquals($code, $exception->getCode()); $this->assertNull($exception->getPrevious()); @@ -57,7 +57,7 @@ public function testConstructorWithMessageCodeAndPrevious(): void $code = 500; $previous = new \RuntimeException('Previous exception'); $exception = new CacheException($message, $code, $previous); - + $this->assertEquals($message, $exception->getMessage()); $this->assertEquals($code, $exception->getCode()); $this->assertSame($previous, $exception->getPrevious()); @@ -67,35 +67,35 @@ public function testCanBeThrownAndCaught(): void { $this->expectException(CacheException::class); $this->expectExceptionMessage('Test exception message'); - + throw new CacheException('Test exception message'); } public function testCanBeCaughtAsParentException(): void { $caught = false; - + try { throw new CacheException('Test message'); } catch (CognitiveAnalysisException $e) { $caught = true; $this->assertEquals('Test message', $e->getMessage()); } - + $this->assertTrue($caught); } public function testCanBeCaughtAsGenericException(): void { $caught = false; - + try { throw new CacheException('Test message'); } catch (\Exception $e) { $caught = true; $this->assertEquals('Test message', $e->getMessage()); } - + $this->assertTrue($caught); } @@ -103,14 +103,14 @@ public function testExceptionWithSpecialCharacters(): void { $message = 'Cache failed: "Invalid UTF-8 sequence \x80"'; $exception = new CacheException($message); - + $this->assertEquals($message, $exception->getMessage()); } public function testExceptionWithEmptyMessage(): void { $exception = new CacheException(''); - + $this->assertEquals('', $exception->getMessage()); } @@ -118,7 +118,7 @@ public function testExceptionWithVeryLongMessage(): void { $message = str_repeat('A', 1000); $exception = new CacheException($message); - + $this->assertEquals($message, $exception->getMessage()); } } diff --git a/tests/Unit/Cache/FileCacheTest.php b/tests/Unit/Cache/FileCacheTest.php index 73ea1cd..90b42ff 100644 --- a/tests/Unit/Cache/FileCacheTest.php +++ b/tests/Unit/Cache/FileCacheTest.php @@ -40,14 +40,14 @@ public function testConstructorWithTrailingSlash(): void { $cacheDirWithSlash = $this->testCacheDir . '/'; $cache = new FileCache($cacheDirWithSlash); - + $this->assertDirectoryExists($this->testCacheDir); } public function testGetItemReturnsMissForNonExistentKey(): void { $item = $this->cache->getItem('non-existent-key'); - + $this->assertEquals('non-existent-key', $item->getKey()); $this->assertNull($item->get()); $this->assertFalse($item->isHit()); @@ -57,10 +57,10 @@ public function testSaveAndGetItem(): void { $key = 'test-key'; $value = 'test-value'; - + $item = new CacheItem($key, $value, true); $this->assertTrue($this->cache->save($item)); - + $retrievedItem = $this->cache->getItem($key); $this->assertEquals($key, $retrievedItem->getKey()); $this->assertEquals($value, $retrievedItem->get()); @@ -71,10 +71,10 @@ public function testSaveAndGetItemWithArray(): void { $key = 'array-key'; $value = ['key1' => 'value1', 'key2' => 123, 'key3' => ['nested' => true]]; - + $item = new CacheItem($key, $value, true); $this->assertTrue($this->cache->save($item)); - + $retrievedItem = $this->cache->getItem($key); $retrievedValue = $retrievedItem->get(); $this->assertIsObject($retrievedValue); @@ -89,10 +89,10 @@ public function testSaveAndGetItemWithObject(): void { $key = 'object-key'; $value = (object) ['property1' => 'value1', 'property2' => 456]; - + $item = new CacheItem($key, $value, true); $this->assertTrue($this->cache->save($item)); - + $retrievedItem = $this->cache->getItem($key); $retrievedValue = $retrievedItem->get(); $this->assertIsObject($retrievedValue); @@ -104,10 +104,10 @@ public function testSaveAndGetItemWithObject(): void public function testSaveAndGetItemWithNullValue(): void { $key = 'null-key'; - + $item = new CacheItem($key, null, true); $this->assertTrue($this->cache->save($item)); - + // Saving null should delete the item $this->assertFalse($this->cache->hasItem($key)); } @@ -115,24 +115,24 @@ public function testSaveAndGetItemWithNullValue(): void public function testHasItem(): void { $key = 'has-item-test'; - + $this->assertFalse($this->cache->hasItem($key)); - + $item = new CacheItem($key, 'value', true); $this->cache->save($item); - + $this->assertTrue($this->cache->hasItem($key)); } public function testDeleteItem(): void { $key = 'delete-test'; - + // Save an item first $item = new CacheItem($key, 'value', true); $this->cache->save($item); $this->assertTrue($this->cache->hasItem($key)); - + // Delete the item $this->assertTrue($this->cache->deleteItem($key)); $this->assertFalse($this->cache->hasItem($key)); @@ -146,21 +146,21 @@ public function testDeleteNonExistentItem(): void public function testDeleteItems(): void { $keys = ['key1', 'key2', 'key3']; - + // Save items first foreach ($keys as $key) { $item = new CacheItem($key, "value-{$key}", true); $this->cache->save($item); } - + // Verify all items exist foreach ($keys as $key) { $this->assertTrue($this->cache->hasItem($key)); } - + // Delete all items $this->assertTrue($this->cache->deleteItems($keys)); - + // Verify all items are deleted foreach ($keys as $key) { $this->assertFalse($this->cache->hasItem($key)); @@ -170,26 +170,26 @@ public function testDeleteItems(): void public function testGetItems(): void { $keys = ['key1', 'key2', 'key3']; - + // Save some items $item1 = new CacheItem('key1', 'value1', true); $item2 = new CacheItem('key2', 'value2', true); $this->cache->save($item1); $this->cache->save($item2); - + $items = $this->cache->getItems($keys); - + $this->assertCount(3, $items); $this->assertArrayHasKey('key1', $items); $this->assertArrayHasKey('key2', $items); $this->assertArrayHasKey('key3', $items); - + $this->assertTrue($items['key1']->isHit()); $this->assertEquals('value1', $items['key1']->get()); - + $this->assertTrue($items['key2']->isHit()); $this->assertEquals('value2', $items['key2']->get()); - + $this->assertFalse($items['key3']->isHit()); $this->assertNull($items['key3']->get()); } @@ -198,27 +198,27 @@ public function testSaveDeferredAndCommit(): void { $key1 = 'deferred-key1'; $key2 = 'deferred-key2'; - + $item1 = new CacheItem($key1, 'value1', true); $item2 = new CacheItem($key2, 'value2', true); - + $this->assertTrue($this->cache->saveDeferred($item1)); $this->assertTrue($this->cache->saveDeferred($item2)); - + // Items should not be saved yet $this->assertFalse($this->cache->hasItem($key1)); $this->assertFalse($this->cache->hasItem($key2)); - + // Commit the deferred items $this->assertTrue($this->cache->commit()); - + // Now items should be saved $this->assertTrue($this->cache->hasItem($key1)); $this->assertTrue($this->cache->hasItem($key2)); - + $retrievedItem1 = $this->cache->getItem($key1); $this->assertEquals('value1', $retrievedItem1->get()); - + $retrievedItem2 = $this->cache->getItem($key2); $this->assertEquals('value2', $retrievedItem2->get()); } @@ -230,17 +230,17 @@ public function testClear(): void $item2 = new CacheItem('key2', 'value2', true); $this->cache->save($item1); $this->cache->save($item2); - + $this->assertTrue($this->cache->hasItem('key1')); $this->assertTrue($this->cache->hasItem('key2')); - + // Clear the cache $this->assertTrue($this->cache->clear()); - + // Items should be gone $this->assertFalse($this->cache->hasItem('key1')); $this->assertFalse($this->cache->hasItem('key2')); - + // Cache directory should still exist $this->assertDirectoryExists($this->testCacheDir); } @@ -249,25 +249,25 @@ public function testCacheFileStructure(): void { $key = 'test-structure'; $value = 'test-value'; - + $item = new CacheItem($key, $value, true); $this->cache->save($item); - + // Check that subdirectory was created $hash = md5($key); $subDir = substr($hash, 0, 2); $expectedSubDir = $this->testCacheDir . '/' . $subDir; - + $this->assertDirectoryExists($expectedSubDir); - + // Check that cache file was created $expectedFile = $expectedSubDir . '/' . $hash . '.cache'; $this->assertFileExists($expectedFile); - + // Verify file content $content = file_get_contents($expectedFile); $this->assertNotFalse($content); - + $data = json_decode($content, false); $this->assertEquals($value, $data); } @@ -281,19 +281,19 @@ public function testUtf8Sanitization(): void 'mixed' => "Valid text with \x80 invalid chars", 'unicode' => 'Unicode: 你好世界 🌍' ]; - + $item = new CacheItem($key, $value, true); $this->assertTrue($this->cache->save($item)); - + $retrievedItem = $this->cache->getItem($key); $retrievedValue = $retrievedItem->get(); - + $this->assertIsObject($retrievedValue); $this->assertObjectHasProperty('valid_utf8', $retrievedValue); $this->assertObjectHasProperty('invalid_utf8', $retrievedValue); $this->assertObjectHasProperty('mixed', $retrievedValue); $this->assertObjectHasProperty('unicode', $retrievedValue); - + // Verify UTF-8 sanitization worked $this->assertIsString($retrievedValue->invalid_utf8); $this->assertIsString($retrievedValue->mixed); @@ -303,18 +303,18 @@ public function testCorruptedCacheFile(): void { $key = 'corrupted-test'; $value = 'test-value'; - + // Save a valid item first $item = new CacheItem($key, $value, true); $this->cache->save($item); - + // Corrupt the cache file $hash = md5($key); $subDir = substr($hash, 0, 2); $cacheFile = $this->testCacheDir . '/' . $subDir . '/' . $hash . '.cache'; - + file_put_contents($cacheFile, 'invalid json content'); - + // Should return a miss for corrupted file $retrievedItem = $this->cache->getItem($key); $this->assertFalse($retrievedItem->isHit()); @@ -325,18 +325,18 @@ public function testEmptyCacheFile(): void { $key = 'empty-test'; $value = 'test-value'; - + // Save a valid item first $item = new CacheItem($key, $value, true); $this->cache->save($item); - + // Empty the cache file $hash = md5($key); $subDir = substr($hash, 0, 2); $cacheFile = $this->testCacheDir . '/' . $subDir . '/' . $hash . '.cache'; - + file_put_contents($cacheFile, ''); - + // Should return a miss for empty file $retrievedItem = $this->cache->getItem($key); $this->assertFalse($retrievedItem->isHit()); @@ -347,10 +347,10 @@ public function testCacheDirectoryCreationFailure(): void { // Create a cache directory that's not writable $nonWritableDir = '/root/non-writable-cache'; - + $this->expectException(CacheException::class); $this->expectExceptionMessage('Failed to create cache directory'); - + new FileCache($nonWritableDir); } @@ -359,12 +359,12 @@ public function testCacheSubdirectoryCreationFailure(): void // Create a cache with a non-writable parent directory $parentDir = $this->testCacheDir . '/parent'; mkdir($parentDir, 0444); // Read-only - + $this->expectException(CacheException::class); $this->expectExceptionMessage('Failed to create cache directory'); - + $cache = new FileCache($parentDir . '/cache'); - + // Try to save an item which will trigger subdirectory creation $item = new CacheItem('test-key', 'test-value', true); $cache->save($item); @@ -375,12 +375,12 @@ public function testJsonEncodeFailure(): void // Create a value that cannot be JSON encoded $key = 'json-fail-test'; $value = "\x80"; // Invalid UTF-8 that might cause JSON encoding issues - + $item = new CacheItem($key, $value, true); - + // This should handle the encoding gracefully $result = $this->cache->save($item); - + // The save might succeed due to UTF-8 sanitization if ($result) { $retrievedItem = $this->cache->getItem($key); @@ -392,10 +392,10 @@ public function testLargeDataHandling(): void { $key = 'large-data-test'; $value = str_repeat('A', 10000); // 10KB string - + $item = new CacheItem($key, $value, true); $this->assertTrue($this->cache->save($item)); - + $retrievedItem = $this->cache->getItem($key); $this->assertEquals($value, $retrievedItem->get()); $this->assertTrue($retrievedItem->isHit()); @@ -415,14 +415,14 @@ public function testSpecialCharactersInKey(): void 'key"with"quotes', "key'with'singlequotes" ]; - + foreach ($keys as $key) { $value = "value-for-{$key}"; $item = new CacheItem($key, $value, true); - + $this->assertTrue($this->cache->save($item), "Failed to save key: {$key}"); $this->assertTrue($this->cache->hasItem($key), "Failed to verify key exists: {$key}"); - + $retrievedItem = $this->cache->getItem($key); $this->assertEquals($value, $retrievedItem->get(), "Failed to retrieve value for key: {$key}"); } From 8a860821a2d62f3726f04a7e77d099e71f8c2fb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Kr=C3=A4mer?= Date: Thu, 9 Oct 2025 01:55:15 +0200 Subject: [PATCH 14/17] Testing the Cache Implementation --- src/Cache/FileCache.php | 2 +- tests/Unit/Cache/FileCacheTest.php | 32 +++++++++++++++--------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/Cache/FileCache.php b/src/Cache/FileCache.php index ac72e4d..92cae15 100644 --- a/src/Cache/FileCache.php +++ b/src/Cache/FileCache.php @@ -191,7 +191,7 @@ private function loadCacheData(string $filePath): mixed return null; } - $data = json_decode($content, false); + $data = json_decode($content, true); if ($data === null) { return null; } diff --git a/tests/Unit/Cache/FileCacheTest.php b/tests/Unit/Cache/FileCacheTest.php index 90b42ff..e7c475c 100644 --- a/tests/Unit/Cache/FileCacheTest.php +++ b/tests/Unit/Cache/FileCacheTest.php @@ -77,11 +77,11 @@ public function testSaveAndGetItemWithArray(): void $retrievedItem = $this->cache->getItem($key); $retrievedValue = $retrievedItem->get(); - $this->assertIsObject($retrievedValue); - $this->assertEquals('value1', $retrievedValue->key1); - $this->assertEquals(123, $retrievedValue->key2); - $this->assertIsObject($retrievedValue->key3); - $this->assertTrue($retrievedValue->key3->nested); + $this->assertIsArray($retrievedValue); + $this->assertEquals('value1', $retrievedValue['key1']); + $this->assertEquals(123, $retrievedValue['key2']); + $this->assertIsArray($retrievedValue['key3']); + $this->assertTrue($retrievedValue['key3']['nested']); $this->assertTrue($retrievedItem->isHit()); } @@ -95,9 +95,9 @@ public function testSaveAndGetItemWithObject(): void $retrievedItem = $this->cache->getItem($key); $retrievedValue = $retrievedItem->get(); - $this->assertIsObject($retrievedValue); - $this->assertEquals('value1', $retrievedValue->property1); - $this->assertEquals(456, $retrievedValue->property2); + $this->assertIsArray($retrievedValue); + $this->assertEquals('value1', $retrievedValue['property1']); + $this->assertEquals(456, $retrievedValue['property2']); $this->assertTrue($retrievedItem->isHit()); } @@ -268,7 +268,7 @@ public function testCacheFileStructure(): void $content = file_get_contents($expectedFile); $this->assertNotFalse($content); - $data = json_decode($content, false); + $data = json_decode($content, true); $this->assertEquals($value, $data); } @@ -288,15 +288,15 @@ public function testUtf8Sanitization(): void $retrievedItem = $this->cache->getItem($key); $retrievedValue = $retrievedItem->get(); - $this->assertIsObject($retrievedValue); - $this->assertObjectHasProperty('valid_utf8', $retrievedValue); - $this->assertObjectHasProperty('invalid_utf8', $retrievedValue); - $this->assertObjectHasProperty('mixed', $retrievedValue); - $this->assertObjectHasProperty('unicode', $retrievedValue); + $this->assertIsArray($retrievedValue); + $this->assertArrayHasKey('valid_utf8', $retrievedValue); + $this->assertArrayHasKey('invalid_utf8', $retrievedValue); + $this->assertArrayHasKey('mixed', $retrievedValue); + $this->assertArrayHasKey('unicode', $retrievedValue); // Verify UTF-8 sanitization worked - $this->assertIsString($retrievedValue->invalid_utf8); - $this->assertIsString($retrievedValue->mixed); + $this->assertIsString($retrievedValue['invalid_utf8']); + $this->assertIsString($retrievedValue['mixed']); } public function testCorruptedCacheFile(): void From 37e6a41f135763d3275041243728f1a8479264a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Kr=C3=A4mer?= Date: Mon, 13 Oct 2025 01:42:13 +0200 Subject: [PATCH 15/17] Cache Strategy --- src/Application.php | 38 +++++- .../Cognitive/Cache/MetricsCacheStrategy.php | 96 ++++++++++++++ .../Cache/MetricsCacheStrategyInterface.php | 46 +++++++ .../Cognitive/Cache/NullCacheStrategy.php | 63 +++++++++ .../Cognitive/CognitiveMetricsCollector.php | 120 +++++------------- 5 files changed, 274 insertions(+), 89 deletions(-) create mode 100644 src/Business/Cognitive/Cache/MetricsCacheStrategy.php create mode 100644 src/Business/Cognitive/Cache/MetricsCacheStrategyInterface.php create mode 100644 src/Business/Cognitive/Cache/NullCacheStrategy.php diff --git a/src/Application.php b/src/Application.php index 37df80c..1950b90 100644 --- a/src/Application.php +++ b/src/Application.php @@ -8,6 +8,9 @@ use Phauthentic\CognitiveCodeAnalysis\Business\Churn\ChurnCalculator; use Phauthentic\CognitiveCodeAnalysis\Business\CodeCoverage\CodeCoverageFactory; use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Baseline; +use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Cache\MetricsCacheStrategy; +use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Cache\MetricsCacheStrategyInterface; +use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Cache\NullCacheStrategy; use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\CognitiveMetricsCollector; use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\CognitiveMetricsSorter; use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Events\FileProcessed; @@ -169,17 +172,50 @@ private function bootstrap(): void private function bootstrapMetricsCollectors(): void { + // Register cache strategies + $this->containerBuilder->register(MetricsCacheStrategy::class, MetricsCacheStrategy::class) + ->setArguments([ + new Reference(CacheItemPoolInterface::class) + ]) + ->setPublic(true); + + $this->containerBuilder->register(NullCacheStrategy::class, NullCacheStrategy::class) + ->setPublic(true); + + // Register cache strategy interface with factory + $this->containerBuilder->register(MetricsCacheStrategyInterface::class) + ->setFactory([$this, 'createCacheStrategy']) + ->setPublic(true); + + // Register the collector with cache strategy $this->containerBuilder->register(CognitiveMetricsCollector::class, CognitiveMetricsCollector::class) ->setArguments([ new Reference(Parser::class), new Reference(DirectoryScanner::class), new Reference(ConfigService::class), new Reference(MessageBusInterface::class), - new Reference(CacheItemPoolInterface::class) + new Reference(MetricsCacheStrategyInterface::class) ]) ->setPublic(true); } + /** + * Factory method to create the appropriate cache strategy based on config + */ + public function createCacheStrategy(): MetricsCacheStrategyInterface + { + $configService = $this->get(ConfigService::class); + $config = $configService->getConfig(); + + // If caching is enabled, use active cache strategy + if ($config->cache?->enabled === true) { + return $this->get(MetricsCacheStrategy::class); + } + + // Otherwise use null cache strategy + return $this->get(NullCacheStrategy::class); + } + private function configureEventBus(): void { $progressbar = new ProgressBarHandler( diff --git a/src/Business/Cognitive/Cache/MetricsCacheStrategy.php b/src/Business/Cognitive/Cache/MetricsCacheStrategy.php new file mode 100644 index 0000000..12b9cfe --- /dev/null +++ b/src/Business/Cognitive/Cache/MetricsCacheStrategy.php @@ -0,0 +1,96 @@ +|null + */ + public function getCachedMetrics(SplFileInfo $file, string $configHash): ?array + { + $cacheKey = $this->generateCacheKey($file, $configHash); + $cacheItem = $this->cachePool->getItem($cacheKey); + + if (!$cacheItem->isHit()) { + return null; + } + + $cachedData = $cacheItem->get(); + return $cachedData['analysis_result'] ?? null; + } + + /** + * Cache metrics for a file + * + * @param SplFileInfo $file + * @param array $metrics + * @param string $configHash + * @param array $ignoredItems + */ + public function cacheMetrics(SplFileInfo $file, array $metrics, string $configHash, array $ignoredItems): void + { + $cacheKey = $this->generateCacheKey($file, $configHash); + $cacheItem = $this->cachePool->getItem($cacheKey); + + $cacheItem->set([ + 'version' => '1.0', + 'file_path' => $file->getRealPath(), + 'file_mtime' => $file->getMTime(), + 'config_hash' => $configHash, + 'analysis_result' => $metrics, + 'ignored_items' => $ignoredItems, + 'cached_at' => time() + ]); + + $this->cachePool->save($cacheItem); + } + + /** + * Generate configuration hash for cache invalidation + * + * @param CognitiveConfig $config + * @return string + */ + public function generateConfigHash(CognitiveConfig $config): string + { + return md5(serialize($config->toArray())); + } + + /** + * Clear all cached data + */ + public function clear(): void + { + $this->cachePool->clear(); + } + + /** + * Generate a cache key for a file based on path, modification time, and config hash + */ + private function generateCacheKey(SplFileInfo $file, string $configHash): string + { + $filePath = $file->getRealPath(); + $fileMtime = $file->getMTime(); + + return 'phpcca_' . md5($filePath . '|' . $fileMtime . '|' . $configHash); + } +} diff --git a/src/Business/Cognitive/Cache/MetricsCacheStrategyInterface.php b/src/Business/Cognitive/Cache/MetricsCacheStrategyInterface.php new file mode 100644 index 0000000..682234a --- /dev/null +++ b/src/Business/Cognitive/Cache/MetricsCacheStrategyInterface.php @@ -0,0 +1,46 @@ +|null + */ + public function getCachedMetrics(SplFileInfo $file, string $configHash): ?array; + + /** + * Cache metrics for a file + * + * @param SplFileInfo $file + * @param array $metrics + * @param string $configHash + * @param array $ignoredItems + */ + public function cacheMetrics(SplFileInfo $file, array $metrics, string $configHash, array $ignoredItems): void; + + /** + * Generate configuration hash for cache invalidation + * + * @param CognitiveConfig $config + * @return string + */ + public function generateConfigHash(CognitiveConfig $config): string; + + /** + * Clear all cached data + */ + public function clear(): void; +} diff --git a/src/Business/Cognitive/Cache/NullCacheStrategy.php b/src/Business/Cognitive/Cache/NullCacheStrategy.php new file mode 100644 index 0000000..526445b --- /dev/null +++ b/src/Business/Cognitive/Cache/NullCacheStrategy.php @@ -0,0 +1,63 @@ +|null + */ + public function getCachedMetrics(SplFileInfo $file, string $configHash): ?array + { + return null; + } + + /** + * Cache metrics for a file + * No-op since no caching is performed + * + * @param SplFileInfo $file + * @param array $metrics + * @param string $configHash + * @param array $ignoredItems + */ + public function cacheMetrics(SplFileInfo $file, array $metrics, string $configHash, array $ignoredItems): void + { + // No-op - no caching performed + } + + /** + * Generate configuration hash for cache invalidation + * Returns empty string since no caching is performed + * + * @param CognitiveConfig $config + * @return string + */ + public function generateConfigHash(CognitiveConfig $config): string + { + return ''; + } + + /** + * Clear all cached data + * No-op since no caching is performed + */ + public function clear(): void + { + // No-op - no caching performed + } +} diff --git a/src/Business/Cognitive/CognitiveMetricsCollector.php b/src/Business/Cognitive/CognitiveMetricsCollector.php index 69c6e5f..5cc5ba1 100644 --- a/src/Business/Cognitive/CognitiveMetricsCollector.php +++ b/src/Business/Cognitive/CognitiveMetricsCollector.php @@ -4,6 +4,7 @@ namespace Phauthentic\CognitiveCodeAnalysis\Business\Cognitive; +use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Cache\MetricsCacheStrategyInterface; use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Events\FileProcessed; use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Events\ParserFailed; use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Events\SourceFilesFound; @@ -11,8 +12,6 @@ use Phauthentic\CognitiveCodeAnalysis\CognitiveAnalysisException; use Phauthentic\CognitiveCodeAnalysis\Config\CognitiveConfig; use Phauthentic\CognitiveCodeAnalysis\Config\ConfigService; -use Psr\Cache\CacheItemInterface; -use Psr\Cache\CacheItemPoolInterface; use SplFileInfo; use Symfony\Component\Messenger\MessageBusInterface; use Throwable; @@ -32,7 +31,7 @@ public function __construct( protected readonly DirectoryScanner $directoryScanner, protected readonly ConfigService $configService, protected readonly MessageBusInterface $messageBus, - protected readonly CacheItemPoolInterface $cachePool, + protected readonly MetricsCacheStrategyInterface $cacheStrategy, ) { } @@ -98,17 +97,13 @@ private function findMetrics(iterable $files): CognitiveMetricsCollection $metricsCollection = new CognitiveMetricsCollection(); $fileCount = 0; $config = $this->configService->getConfig(); - $configHash = $this->generateConfigHash($config); - $useCache = $config->cache?->enabled === true; + $configHash = $this->cacheStrategy->generateConfigHash($config); foreach ($files as $file) { - // Try to get cached metrics - $cached = $this->getCachedMetrics($file, $configHash, $useCache); - $metrics = $cached['metrics']; + $metrics = $this->cacheStrategy->getCachedMetrics($file, $configHash); - // If not cached, process the file if ($metrics === null) { - $metrics = $this->processFile($file, $fileCount, $cached['cacheItem'], $useCache, $configHash); + $metrics = $this->processAndCacheFile($file, $fileCount, $configHash); if ($metrics === null) { continue; @@ -213,51 +208,9 @@ private function getProjectRoot(): ?string return null; } - /** - * Generate a cache key for a file based on path, modification time, and config hash - */ - private function generateCacheKey(SplFileInfo $file, string $configHash): string - { - $filePath = $file->getRealPath(); - $fileMtime = $file->getMTime(); - - return 'phpcca_' . md5($filePath . '|' . $fileMtime . '|' . $configHash); - } - - /** - * Generate configuration hash for cache invalidation - */ - private function generateConfigHash(CognitiveConfig $config): string - { - return md5(serialize($config->toArray())); - } - - /** - * Cache the analysis result for a file - */ - /** @param array $metrics */ - private function cacheResult( - CacheItemInterface $cacheItem, - SplFileInfo $file, - array $metrics, - string $configHash - ): void { - $cacheItem->set([ - 'version' => '1.0', - 'file_path' => $file->getRealPath(), - 'file_mtime' => $file->getMTime(), - 'config_hash' => $configHash, - 'analysis_result' => $metrics, - 'ignored_items' => $this->ignoredItems, - 'cached_at' => time() - ]); - - $this->cachePool->save($cacheItem); - } - public function clearCache(): void { - $this->cachePool->clear(); + $this->cacheStrategy->clear(); } /** @@ -281,32 +234,6 @@ private function normalizeFilename(SplFileInfo $file): string return $filename; } - /** - * Try to get cached metrics for a file - * - * @return array{metrics: array|null, cacheItem: CacheItemInterface|null} - * @throws \InvalidArgumentException - */ - private function getCachedMetrics(SplFileInfo $file, string $configHash, bool $useCache): array - { - if (!$useCache) { - return ['metrics' => null, 'cacheItem' => null]; - } - - $cacheKey = $this->generateCacheKey($file, $configHash); - $cacheItem = $this->cachePool->getItem($cacheKey); - - if (!$cacheItem->isHit()) { - return ['metrics' => null, 'cacheItem' => $cacheItem]; - } - - $cachedData = $cacheItem->get(); - $this->ignoredItems = $cachedData['ignored_items'] ?? []; - $this->messageBus->dispatch(new FileProcessed($file)); - - return ['metrics' => $cachedData['analysis_result'], 'cacheItem' => $cacheItem]; - } - /** * Process a single file and parse its metrics * @@ -315,10 +242,7 @@ private function getCachedMetrics(SplFileInfo $file, string $configHash, bool $u */ private function processFile( SplFileInfo $file, - int &$fileCount, - ?CacheItemInterface $cacheItem, - bool $useCache, - string $configHash + int &$fileCount ): ?array { try { $metrics = $this->parser->parse( @@ -333,11 +257,6 @@ private function processFile( gc_collect_cycles(); } - // Cache the result if caching is enabled - if ($useCache && $cacheItem !== null) { - $this->cacheResult($cacheItem, $file, $metrics, $configHash); - } - $this->messageBus->dispatch(new FileProcessed($file)); return $metrics; @@ -350,4 +269,29 @@ private function processFile( return null; } } + + /** + * Process a file and cache the metrics + * + * @param SplFileInfo $file + * @param int &$fileCount + * @param string $configHash + * @return array|null + */ + private function processAndCacheFile( + SplFileInfo $file, + int &$fileCount, + string $configHash + ): ?array { + $metrics = $this->processFile($file, $fileCount); + + if ($metrics === null) { + return null; + } + + // Cache the metrics + $this->cacheStrategy->cacheMetrics($file, $metrics, $configHash, $this->ignoredItems); + + return $metrics; + } } From d7d6597325b26785e305865a2eca1d819bf8e335 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Kr=C3=A4mer?= Date: Mon, 13 Oct 2025 01:43:10 +0200 Subject: [PATCH 16/17] Revert "Cache Strategy" This reverts commit 37e6a41f135763d3275041243728f1a8479264a2. --- src/Application.php | 38 +----- .../Cognitive/Cache/MetricsCacheStrategy.php | 96 -------------- .../Cache/MetricsCacheStrategyInterface.php | 46 ------- .../Cognitive/Cache/NullCacheStrategy.php | 63 --------- .../Cognitive/CognitiveMetricsCollector.php | 120 +++++++++++++----- 5 files changed, 89 insertions(+), 274 deletions(-) delete mode 100644 src/Business/Cognitive/Cache/MetricsCacheStrategy.php delete mode 100644 src/Business/Cognitive/Cache/MetricsCacheStrategyInterface.php delete mode 100644 src/Business/Cognitive/Cache/NullCacheStrategy.php diff --git a/src/Application.php b/src/Application.php index 1950b90..37df80c 100644 --- a/src/Application.php +++ b/src/Application.php @@ -8,9 +8,6 @@ use Phauthentic\CognitiveCodeAnalysis\Business\Churn\ChurnCalculator; use Phauthentic\CognitiveCodeAnalysis\Business\CodeCoverage\CodeCoverageFactory; use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Baseline; -use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Cache\MetricsCacheStrategy; -use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Cache\MetricsCacheStrategyInterface; -use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Cache\NullCacheStrategy; use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\CognitiveMetricsCollector; use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\CognitiveMetricsSorter; use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Events\FileProcessed; @@ -172,50 +169,17 @@ private function bootstrap(): void private function bootstrapMetricsCollectors(): void { - // Register cache strategies - $this->containerBuilder->register(MetricsCacheStrategy::class, MetricsCacheStrategy::class) - ->setArguments([ - new Reference(CacheItemPoolInterface::class) - ]) - ->setPublic(true); - - $this->containerBuilder->register(NullCacheStrategy::class, NullCacheStrategy::class) - ->setPublic(true); - - // Register cache strategy interface with factory - $this->containerBuilder->register(MetricsCacheStrategyInterface::class) - ->setFactory([$this, 'createCacheStrategy']) - ->setPublic(true); - - // Register the collector with cache strategy $this->containerBuilder->register(CognitiveMetricsCollector::class, CognitiveMetricsCollector::class) ->setArguments([ new Reference(Parser::class), new Reference(DirectoryScanner::class), new Reference(ConfigService::class), new Reference(MessageBusInterface::class), - new Reference(MetricsCacheStrategyInterface::class) + new Reference(CacheItemPoolInterface::class) ]) ->setPublic(true); } - /** - * Factory method to create the appropriate cache strategy based on config - */ - public function createCacheStrategy(): MetricsCacheStrategyInterface - { - $configService = $this->get(ConfigService::class); - $config = $configService->getConfig(); - - // If caching is enabled, use active cache strategy - if ($config->cache?->enabled === true) { - return $this->get(MetricsCacheStrategy::class); - } - - // Otherwise use null cache strategy - return $this->get(NullCacheStrategy::class); - } - private function configureEventBus(): void { $progressbar = new ProgressBarHandler( diff --git a/src/Business/Cognitive/Cache/MetricsCacheStrategy.php b/src/Business/Cognitive/Cache/MetricsCacheStrategy.php deleted file mode 100644 index 12b9cfe..0000000 --- a/src/Business/Cognitive/Cache/MetricsCacheStrategy.php +++ /dev/null @@ -1,96 +0,0 @@ -|null - */ - public function getCachedMetrics(SplFileInfo $file, string $configHash): ?array - { - $cacheKey = $this->generateCacheKey($file, $configHash); - $cacheItem = $this->cachePool->getItem($cacheKey); - - if (!$cacheItem->isHit()) { - return null; - } - - $cachedData = $cacheItem->get(); - return $cachedData['analysis_result'] ?? null; - } - - /** - * Cache metrics for a file - * - * @param SplFileInfo $file - * @param array $metrics - * @param string $configHash - * @param array $ignoredItems - */ - public function cacheMetrics(SplFileInfo $file, array $metrics, string $configHash, array $ignoredItems): void - { - $cacheKey = $this->generateCacheKey($file, $configHash); - $cacheItem = $this->cachePool->getItem($cacheKey); - - $cacheItem->set([ - 'version' => '1.0', - 'file_path' => $file->getRealPath(), - 'file_mtime' => $file->getMTime(), - 'config_hash' => $configHash, - 'analysis_result' => $metrics, - 'ignored_items' => $ignoredItems, - 'cached_at' => time() - ]); - - $this->cachePool->save($cacheItem); - } - - /** - * Generate configuration hash for cache invalidation - * - * @param CognitiveConfig $config - * @return string - */ - public function generateConfigHash(CognitiveConfig $config): string - { - return md5(serialize($config->toArray())); - } - - /** - * Clear all cached data - */ - public function clear(): void - { - $this->cachePool->clear(); - } - - /** - * Generate a cache key for a file based on path, modification time, and config hash - */ - private function generateCacheKey(SplFileInfo $file, string $configHash): string - { - $filePath = $file->getRealPath(); - $fileMtime = $file->getMTime(); - - return 'phpcca_' . md5($filePath . '|' . $fileMtime . '|' . $configHash); - } -} diff --git a/src/Business/Cognitive/Cache/MetricsCacheStrategyInterface.php b/src/Business/Cognitive/Cache/MetricsCacheStrategyInterface.php deleted file mode 100644 index 682234a..0000000 --- a/src/Business/Cognitive/Cache/MetricsCacheStrategyInterface.php +++ /dev/null @@ -1,46 +0,0 @@ -|null - */ - public function getCachedMetrics(SplFileInfo $file, string $configHash): ?array; - - /** - * Cache metrics for a file - * - * @param SplFileInfo $file - * @param array $metrics - * @param string $configHash - * @param array $ignoredItems - */ - public function cacheMetrics(SplFileInfo $file, array $metrics, string $configHash, array $ignoredItems): void; - - /** - * Generate configuration hash for cache invalidation - * - * @param CognitiveConfig $config - * @return string - */ - public function generateConfigHash(CognitiveConfig $config): string; - - /** - * Clear all cached data - */ - public function clear(): void; -} diff --git a/src/Business/Cognitive/Cache/NullCacheStrategy.php b/src/Business/Cognitive/Cache/NullCacheStrategy.php deleted file mode 100644 index 526445b..0000000 --- a/src/Business/Cognitive/Cache/NullCacheStrategy.php +++ /dev/null @@ -1,63 +0,0 @@ -|null - */ - public function getCachedMetrics(SplFileInfo $file, string $configHash): ?array - { - return null; - } - - /** - * Cache metrics for a file - * No-op since no caching is performed - * - * @param SplFileInfo $file - * @param array $metrics - * @param string $configHash - * @param array $ignoredItems - */ - public function cacheMetrics(SplFileInfo $file, array $metrics, string $configHash, array $ignoredItems): void - { - // No-op - no caching performed - } - - /** - * Generate configuration hash for cache invalidation - * Returns empty string since no caching is performed - * - * @param CognitiveConfig $config - * @return string - */ - public function generateConfigHash(CognitiveConfig $config): string - { - return ''; - } - - /** - * Clear all cached data - * No-op since no caching is performed - */ - public function clear(): void - { - // No-op - no caching performed - } -} diff --git a/src/Business/Cognitive/CognitiveMetricsCollector.php b/src/Business/Cognitive/CognitiveMetricsCollector.php index 5cc5ba1..69c6e5f 100644 --- a/src/Business/Cognitive/CognitiveMetricsCollector.php +++ b/src/Business/Cognitive/CognitiveMetricsCollector.php @@ -4,7 +4,6 @@ namespace Phauthentic\CognitiveCodeAnalysis\Business\Cognitive; -use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Cache\MetricsCacheStrategyInterface; use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Events\FileProcessed; use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Events\ParserFailed; use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Events\SourceFilesFound; @@ -12,6 +11,8 @@ use Phauthentic\CognitiveCodeAnalysis\CognitiveAnalysisException; use Phauthentic\CognitiveCodeAnalysis\Config\CognitiveConfig; use Phauthentic\CognitiveCodeAnalysis\Config\ConfigService; +use Psr\Cache\CacheItemInterface; +use Psr\Cache\CacheItemPoolInterface; use SplFileInfo; use Symfony\Component\Messenger\MessageBusInterface; use Throwable; @@ -31,7 +32,7 @@ public function __construct( protected readonly DirectoryScanner $directoryScanner, protected readonly ConfigService $configService, protected readonly MessageBusInterface $messageBus, - protected readonly MetricsCacheStrategyInterface $cacheStrategy, + protected readonly CacheItemPoolInterface $cachePool, ) { } @@ -97,13 +98,17 @@ private function findMetrics(iterable $files): CognitiveMetricsCollection $metricsCollection = new CognitiveMetricsCollection(); $fileCount = 0; $config = $this->configService->getConfig(); - $configHash = $this->cacheStrategy->generateConfigHash($config); + $configHash = $this->generateConfigHash($config); + $useCache = $config->cache?->enabled === true; foreach ($files as $file) { - $metrics = $this->cacheStrategy->getCachedMetrics($file, $configHash); + // Try to get cached metrics + $cached = $this->getCachedMetrics($file, $configHash, $useCache); + $metrics = $cached['metrics']; + // If not cached, process the file if ($metrics === null) { - $metrics = $this->processAndCacheFile($file, $fileCount, $configHash); + $metrics = $this->processFile($file, $fileCount, $cached['cacheItem'], $useCache, $configHash); if ($metrics === null) { continue; @@ -208,9 +213,51 @@ private function getProjectRoot(): ?string return null; } + /** + * Generate a cache key for a file based on path, modification time, and config hash + */ + private function generateCacheKey(SplFileInfo $file, string $configHash): string + { + $filePath = $file->getRealPath(); + $fileMtime = $file->getMTime(); + + return 'phpcca_' . md5($filePath . '|' . $fileMtime . '|' . $configHash); + } + + /** + * Generate configuration hash for cache invalidation + */ + private function generateConfigHash(CognitiveConfig $config): string + { + return md5(serialize($config->toArray())); + } + + /** + * Cache the analysis result for a file + */ + /** @param array $metrics */ + private function cacheResult( + CacheItemInterface $cacheItem, + SplFileInfo $file, + array $metrics, + string $configHash + ): void { + $cacheItem->set([ + 'version' => '1.0', + 'file_path' => $file->getRealPath(), + 'file_mtime' => $file->getMTime(), + 'config_hash' => $configHash, + 'analysis_result' => $metrics, + 'ignored_items' => $this->ignoredItems, + 'cached_at' => time() + ]); + + $this->cachePool->save($cacheItem); + } + public function clearCache(): void { - $this->cacheStrategy->clear(); + $this->cachePool->clear(); } /** @@ -234,6 +281,32 @@ private function normalizeFilename(SplFileInfo $file): string return $filename; } + /** + * Try to get cached metrics for a file + * + * @return array{metrics: array|null, cacheItem: CacheItemInterface|null} + * @throws \InvalidArgumentException + */ + private function getCachedMetrics(SplFileInfo $file, string $configHash, bool $useCache): array + { + if (!$useCache) { + return ['metrics' => null, 'cacheItem' => null]; + } + + $cacheKey = $this->generateCacheKey($file, $configHash); + $cacheItem = $this->cachePool->getItem($cacheKey); + + if (!$cacheItem->isHit()) { + return ['metrics' => null, 'cacheItem' => $cacheItem]; + } + + $cachedData = $cacheItem->get(); + $this->ignoredItems = $cachedData['ignored_items'] ?? []; + $this->messageBus->dispatch(new FileProcessed($file)); + + return ['metrics' => $cachedData['analysis_result'], 'cacheItem' => $cacheItem]; + } + /** * Process a single file and parse its metrics * @@ -242,7 +315,10 @@ private function normalizeFilename(SplFileInfo $file): string */ private function processFile( SplFileInfo $file, - int &$fileCount + int &$fileCount, + ?CacheItemInterface $cacheItem, + bool $useCache, + string $configHash ): ?array { try { $metrics = $this->parser->parse( @@ -257,6 +333,11 @@ private function processFile( gc_collect_cycles(); } + // Cache the result if caching is enabled + if ($useCache && $cacheItem !== null) { + $this->cacheResult($cacheItem, $file, $metrics, $configHash); + } + $this->messageBus->dispatch(new FileProcessed($file)); return $metrics; @@ -269,29 +350,4 @@ private function processFile( return null; } } - - /** - * Process a file and cache the metrics - * - * @param SplFileInfo $file - * @param int &$fileCount - * @param string $configHash - * @return array|null - */ - private function processAndCacheFile( - SplFileInfo $file, - int &$fileCount, - string $configHash - ): ?array { - $metrics = $this->processFile($file, $fileCount); - - if ($metrics === null) { - return null; - } - - // Cache the metrics - $this->cacheStrategy->cacheMetrics($file, $metrics, $configHash, $this->ignoredItems); - - return $metrics; - } } From 14e6024ff514dc07ec690fccc81e41d058b857bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Kr=C3=A4mer?= Date: Tue, 14 Oct 2025 22:02:12 +0200 Subject: [PATCH 17/17] Minor refactor --- .../Cognitive/CognitiveMetricsCollector.php | 46 +-------------- src/Business/Utility/FilenameNormalizer.php | 59 +++++++++++++++++++ 2 files changed, 61 insertions(+), 44 deletions(-) create mode 100644 src/Business/Utility/FilenameNormalizer.php diff --git a/src/Business/Cognitive/CognitiveMetricsCollector.php b/src/Business/Cognitive/CognitiveMetricsCollector.php index 69c6e5f..aed0258 100644 --- a/src/Business/Cognitive/CognitiveMetricsCollector.php +++ b/src/Business/Cognitive/CognitiveMetricsCollector.php @@ -8,6 +8,7 @@ use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Events\ParserFailed; use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Events\SourceFilesFound; use Phauthentic\CognitiveCodeAnalysis\Business\Utility\DirectoryScanner; +use Phauthentic\CognitiveCodeAnalysis\Business\Utility\FilenameNormalizer; use Phauthentic\CognitiveCodeAnalysis\CognitiveAnalysisException; use Phauthentic\CognitiveCodeAnalysis\Config\CognitiveConfig; use Phauthentic\CognitiveCodeAnalysis\Config\ConfigService; @@ -118,7 +119,7 @@ private function findMetrics(iterable $files): CognitiveMetricsCollection $metricsCollection = $this->processMethodMetrics( $metrics, $metricsCollection, - $this->normalizeFilename($file) + FilenameNormalizer::normalize($file) ); } @@ -141,10 +142,8 @@ private function processMethodMetrics( continue; } - [$class, $method] = explode('::', $classAndMethod); - $metricsArray = array_merge($metrics, [ 'class' => $class, 'method' => $method, @@ -192,26 +191,6 @@ private function findSourceFiles(string $path, array $exclude = []): iterable ); } - /** - * Get the project root directory path. - * - * Start from the current file's directory and traverse up to find composer.json - * - * @return string|null The project root path or null if not found - */ - private function getProjectRoot(): ?string - { - $currentDir = __DIR__; - - while ($currentDir !== dirname($currentDir)) { - if (file_exists($currentDir . DIRECTORY_SEPARATOR . 'composer.json')) { - return $currentDir; - } - $currentDir = dirname($currentDir); - } - - return null; - } /** * Generate a cache key for a file based on path, modification time, and config hash @@ -260,27 +239,6 @@ public function clearCache(): void $this->cachePool->clear(); } - /** - * Normalize filename for the test environment - * - * This is to ensure consistent file paths in test outputs - */ - private function normalizeFilename(SplFileInfo $file): string - { - $filename = $file->getRealPath(); - - if (getenv('APP_ENV') !== 'test') { - return $filename; - } - - $projectRoot = $this->getProjectRoot(); - if ($projectRoot && str_starts_with($filename, $projectRoot)) { - $filename = substr($filename, strlen($projectRoot) + 1); - } - - return $filename; - } - /** * Try to get cached metrics for a file * diff --git a/src/Business/Utility/FilenameNormalizer.php b/src/Business/Utility/FilenameNormalizer.php new file mode 100644 index 0000000..14e46f2 --- /dev/null +++ b/src/Business/Utility/FilenameNormalizer.php @@ -0,0 +1,59 @@ +getRealPath(); + + if (getenv('APP_ENV') !== 'test') { + return $filename; + } + + $projectRoot = self::getProjectRoot(); + if ($projectRoot && str_starts_with($filename, $projectRoot)) { + $filename = substr($filename, strlen($projectRoot) + 1); + } + + return $filename; + } + + /** + * Get the project root directory by traversing up from the current directory + * until composer.json is found. + * + * Start from the current file's directory and traverse up to find composer.json + * + * @return string|null The project root path or null if not found + */ + private static function getProjectRoot(): ?string + { + $currentDir = __DIR__; + + while ($currentDir !== dirname($currentDir)) { + if (file_exists($currentDir . DIRECTORY_SEPARATOR . 'composer.json')) { + return $currentDir; + } + $currentDir = dirname($currentDir); + } + + return null; + } +}