From a6aec0b034307b6e2cc5376a82d0c00d8140c390 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sat, 5 Apr 2025 12:20:55 +0400 Subject: [PATCH 01/30] chore: update metafiles --- .editorconfig | 5 +- composer.lock | 959 +++++++++++++++++++++++++------------------------- 2 files changed, 481 insertions(+), 483 deletions(-) diff --git a/.editorconfig b/.editorconfig index 55de6f8..187eb38 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,7 +8,10 @@ indent_style = space indent_size = 4 trim_trailing_whitespace = true -[*.{yml, yaml, sh, conf, neon*}] +[*.yaml] +indent_size = 2 + +[*.yml] indent_size = 2 [*.http] diff --git a/composer.lock b/composer.lock index a726289..55722d7 100644 --- a/composer.lock +++ b/composer.lock @@ -61,16 +61,16 @@ }, { "name": "psr/log", - "version": "3.0.0", + "version": "3.0.2", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001" + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001", - "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", "shasum": "" }, "require": { @@ -105,9 +105,9 @@ "psr-3" ], "support": { - "source": "https://github.com/php-fig/log/tree/3.0.0" + "source": "https://github.com/php-fig/log/tree/3.0.2" }, - "time": "2021-07-14T16:46:02+00:00" + "time": "2024-09-11T13:17:53+00:00" }, { "name": "react/async", @@ -331,16 +331,16 @@ }, { "name": "symfony/console", - "version": "v6.4.10", + "version": "v6.4.20", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "504974cbe43d05f83b201d6498c206f16fc0cdbc" + "reference": "2e4af9c952617cc3f9559ff706aee420a8464c36" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/504974cbe43d05f83b201d6498c206f16fc0cdbc", - "reference": "504974cbe43d05f83b201d6498c206f16fc0cdbc", + "url": "https://api.github.com/repos/symfony/console/zipball/2e4af9c952617cc3f9559ff706aee420a8464c36", + "reference": "2e4af9c952617cc3f9559ff706aee420a8464c36", "shasum": "" }, "require": { @@ -405,7 +405,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.4.10" + "source": "https://github.com/symfony/console/tree/v6.4.20" }, "funding": [ { @@ -421,20 +421,20 @@ "type": "tidelift" } ], - "time": "2024-07-26T12:30:32+00:00" + "time": "2025-03-03T17:16:38+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.5.0", + "version": "v3.5.1", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1" + "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", - "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", "shasum": "" }, "require": { @@ -442,12 +442,12 @@ }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" } }, "autoload": { @@ -472,7 +472,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1" }, "funding": [ { @@ -488,27 +488,27 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:32:20+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "symfony/http-client", - "version": "v6.4.10", + "version": "v6.4.19", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "b5e498f763e0bf5eed8dcd946e50a3b3f71d4ded" + "reference": "3294a433fc9d12ae58128174896b5b1822c28dad" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/b5e498f763e0bf5eed8dcd946e50a3b3f71d4ded", - "reference": "b5e498f763e0bf5eed8dcd946e50a3b3f71d4ded", + "url": "https://api.github.com/repos/symfony/http-client/zipball/3294a433fc9d12ae58128174896b5b1822c28dad", + "reference": "3294a433fc9d12ae58128174896b5b1822c28dad", "shasum": "" }, "require": { "php": ">=8.1", "psr/log": "^1|^2|^3", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/http-client-contracts": "^3.4.1", + "symfony/http-client-contracts": "~3.4.4|^3.5.2", "symfony/service-contracts": "^2.5|^3" }, "conflict": { @@ -565,7 +565,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v6.4.10" + "source": "https://github.com/symfony/http-client/tree/v6.4.19" }, "funding": [ { @@ -581,20 +581,20 @@ "type": "tidelift" } ], - "time": "2024-07-15T09:26:24+00:00" + "time": "2025-02-13T09:55:13+00:00" }, { "name": "symfony/http-client-contracts", - "version": "v3.5.0", + "version": "v3.5.2", "source": { "type": "git", "url": "https://github.com/symfony/http-client-contracts.git", - "reference": "20414d96f391677bf80078aa55baece78b82647d" + "reference": "ee8d807ab20fcb51267fdace50fbe3494c31e645" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/20414d96f391677bf80078aa55baece78b82647d", - "reference": "20414d96f391677bf80078aa55baece78b82647d", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/ee8d807ab20fcb51267fdace50fbe3494c31e645", + "reference": "ee8d807ab20fcb51267fdace50fbe3494c31e645", "shasum": "" }, "require": { @@ -602,12 +602,12 @@ }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" } }, "autoload": { @@ -643,7 +643,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/http-client-contracts/tree/v3.5.0" + "source": "https://github.com/symfony/http-client-contracts/tree/v3.5.2" }, "funding": [ { @@ -659,24 +659,24 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:32:20+00:00" + "time": "2024-12-07T08:49:48+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.30.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "0424dff1c58f028c451efff2045f5d92410bd540" + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/0424dff1c58f028c451efff2045f5d92410bd540", - "reference": "0424dff1c58f028c451efff2045f5d92410bd540", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "provide": { "ext-ctype": "*" @@ -687,8 +687,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -722,7 +722,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" }, "funding": [ { @@ -738,24 +738,24 @@ "type": "tidelift" } ], - "time": "2024-05-31T15:07:36+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.30.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a" + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/64647a7c30b2283f5d49b874d84a18fc22054b7a", - "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "suggest": { "ext-intl": "For best performance" @@ -763,8 +763,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -800,7 +800,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" }, "funding": [ { @@ -816,24 +816,24 @@ "type": "tidelift" } ], - "time": "2024-05-31T15:07:36+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.30.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb" + "reference": "3833d7255cc303546435cb650316bff708a1c75c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/a95281b0be0d9ab48050ebd988b967875cdb9fdb", - "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "suggest": { "ext-intl": "For best performance" @@ -841,8 +841,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -881,7 +881,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" }, "funding": [ { @@ -897,24 +897,24 @@ "type": "tidelift" } ], - "time": "2024-05-31T15:07:36+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.30.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c" + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fd22ab50000ef01661e2a31d850ebaa297f8e03c", - "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "provide": { "ext-mbstring": "*" @@ -925,8 +925,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -961,7 +961,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" }, "funding": [ { @@ -977,20 +977,20 @@ "type": "tidelift" } ], - "time": "2024-06-19T12:30:46+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.5.0", + "version": "v3.5.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f" + "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", - "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0", "shasum": "" }, "require": { @@ -1003,12 +1003,12 @@ }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" } }, "autoload": { @@ -1044,7 +1044,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.5.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.5.1" }, "funding": [ { @@ -1060,20 +1060,20 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:32:20+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "symfony/string", - "version": "v6.4.10", + "version": "v6.4.15", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "ccf9b30251719567bfd46494138327522b9a9446" + "reference": "73a5e66ea2e1677c98d4449177c5a9cf9d8b4c6f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/ccf9b30251719567bfd46494138327522b9a9446", - "reference": "ccf9b30251719567bfd46494138327522b9a9446", + "url": "https://api.github.com/repos/symfony/string/zipball/73a5e66ea2e1677c98d4449177c5a9cf9d8b4c6f", + "reference": "73a5e66ea2e1677c98d4449177c5a9cf9d8b4c6f", "shasum": "" }, "require": { @@ -1130,7 +1130,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v6.4.10" + "source": "https://github.com/symfony/string/tree/v6.4.15" }, "funding": [ { @@ -1146,7 +1146,7 @@ "type": "tidelift" } ], - "time": "2024-07-22T10:21:14+00:00" + "time": "2024-11-13T13:31:12+00:00" }, { "name": "yiisoft/injector", @@ -1477,23 +1477,23 @@ }, { "name": "buggregator/trap", - "version": "1.10.1", + "version": "1.13.12", "source": { "type": "git", "url": "https://github.com/buggregator/trap.git", - "reference": "156ac1e0386a40454e71f1282f3b10bed6e34a12" + "reference": "2a0a6060f7d312e5296a393a303d5867005ff296" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/buggregator/trap/zipball/156ac1e0386a40454e71f1282f3b10bed6e34a12", - "reference": "156ac1e0386a40454e71f1282f3b10bed6e34a12", + "url": "https://api.github.com/repos/buggregator/trap/zipball/2a0a6060f7d312e5296a393a303d5867005ff296", + "reference": "2a0a6060f7d312e5296a393a303d5867005ff296", "shasum": "" }, "require": { "clue/stream-filter": "^1.6", "ext-filter": "*", "ext-sockets": "*", - "nunomaduro/termwind": "^1.15 || ^2", + "nunomaduro/termwind": "^1.15.1 || ^2", "nyholm/psr7": "^1.8", "php": ">=8.1", "php-http/message": "^1.15", @@ -1506,18 +1506,13 @@ "require-dev": { "dereuromark/composer-prefer-lowest": "^0.1.10", "ergebnis/phpunit-slow-test-detector": "^2.14", - "friendsofphp/php-cs-fixer": "^3.54", - "google/protobuf": "^3.23", - "pestphp/pest": "^2.34", - "phpstan/extension-installer": "^1.3", - "phpstan/phpstan": "^1.10", - "phpstan/phpstan-deprecation-rules": "^1.1", - "phpstan/phpstan-phpunit": "^1.3", - "phpstan/phpstan-strict-rules": "^1.5", - "phpunit/phpunit": "^10.5", + "google/protobuf": "^3.25 || ^4.30", + "phpunit/phpunit": "^10.5.10", + "rector/rector": "^1.1", "roxblnfk/unpoly": "^1.8.1", - "vimeo/psalm": "^5.11", - "wayofdev/cs-fixer-config": "^1.4" + "spiral/code-style": "^2.2.2", + "ta-tikoma/phpunit-architecture-test": "^0.8.4", + "vimeo/psalm": "^6.5" }, "suggest": { "ext-simplexml": "To load trap.xml", @@ -1552,6 +1547,7 @@ "description": "A simple and powerful tool for debugging PHP applications.", "homepage": "https://buggregator.dev/", "keywords": [ + "Fibers", "WebSockets", "binary dump", "cli", @@ -1559,6 +1555,7 @@ "debug", "dev", "dump", + "dumper", "helper", "sentry", "server", @@ -1566,23 +1563,19 @@ ], "support": { "issues": "https://github.com/buggregator/trap/issues", - "source": "https://github.com/buggregator/trap/tree/1.10.1" + "source": "https://github.com/buggregator/trap/tree/1.13.12" }, "funding": [ { - "url": "https://github.com/sponsors/buggregator", - "type": "github" - }, - { - "url": "https://patreon.com/butschster", - "type": "patreon" + "url": "https://boosty.to/roxblnfk", + "type": "boosty" }, { "url": "https://patreon.com/roxblnfk", "type": "patreon" } ], - "time": "2024-06-23T14:24:42+00:00" + "time": "2025-04-03T01:29:54+00:00" }, { "name": "clue/ndjson-react", @@ -1716,38 +1709,38 @@ }, { "name": "composer/pcre", - "version": "3.2.0", + "version": "3.3.2", "source": { "type": "git", "url": "https://github.com/composer/pcre.git", - "reference": "ea4ab6f9580a4fd221e0418f2c357cdd39102a90" + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/ea4ab6f9580a4fd221e0418f2c357cdd39102a90", - "reference": "ea4ab6f9580a4fd221e0418f2c357cdd39102a90", + "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", "shasum": "" }, "require": { "php": "^7.4 || ^8.0" }, "conflict": { - "phpstan/phpstan": "<1.11.8" + "phpstan/phpstan": "<1.11.10" }, "require-dev": { - "phpstan/phpstan": "^1.11.8", - "phpstan/phpstan-strict-rules": "^1.1", + "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-strict-rules": "^1 || ^2", "phpunit/phpunit": "^8 || ^9" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "3.x-dev" - }, "phpstan": { "includes": [ "extension.neon" ] + }, + "branch-alias": { + "dev-main": "3.x-dev" } }, "autoload": { @@ -1775,7 +1768,7 @@ ], "support": { "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.2.0" + "source": "https://github.com/composer/pcre/tree/3.3.2" }, "funding": [ { @@ -1791,28 +1784,28 @@ "type": "tidelift" } ], - "time": "2024-07-25T09:36:02+00:00" + "time": "2024-11-12T16:29:46+00:00" }, { "name": "composer/semver", - "version": "3.4.2", + "version": "3.4.3", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "c51258e759afdb17f1fd1fe83bc12baaef6309d6" + "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/c51258e759afdb17f1fd1fe83bc12baaef6309d6", - "reference": "c51258e759afdb17f1fd1fe83bc12baaef6309d6", + "url": "https://api.github.com/repos/composer/semver/zipball/4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", + "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", "shasum": "" }, "require": { "php": "^5.3.2 || ^7.0 || ^8.0" }, "require-dev": { - "phpstan/phpstan": "^1.4", - "symfony/phpunit-bridge": "^4.2 || ^5" + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" }, "type": "library", "extra": { @@ -1856,7 +1849,7 @@ "support": { "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/semver/issues", - "source": "https://github.com/composer/semver/tree/3.4.2" + "source": "https://github.com/composer/semver/tree/3.4.3" }, "funding": [ { @@ -1872,7 +1865,7 @@ "type": "tidelift" } ], - "time": "2024-07-12T11:35:52+00:00" + "time": "2024-09-19T14:15:21+00:00" }, { "name": "composer/xdebug-handler", @@ -2028,29 +2021,27 @@ }, { "name": "doctrine/deprecations", - "version": "1.1.3", + "version": "1.1.4", "source": { "type": "git", "url": "https://github.com/doctrine/deprecations.git", - "reference": "dfbaa3c2d2e9a9df1118213f3b8b0c597bb99fab" + "reference": "31610dbb31faa98e6b5447b62340826f54fbc4e9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/deprecations/zipball/dfbaa3c2d2e9a9df1118213f3b8b0c597bb99fab", - "reference": "dfbaa3c2d2e9a9df1118213f3b8b0c597bb99fab", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/31610dbb31faa98e6b5447b62340826f54fbc4e9", + "reference": "31610dbb31faa98e6b5447b62340826f54fbc4e9", "shasum": "" }, "require": { "php": "^7.1 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^9", - "phpstan/phpstan": "1.4.10 || 1.10.15", - "phpstan/phpstan-phpunit": "^1.0", + "doctrine/coding-standard": "^9 || ^12", + "phpstan/phpstan": "1.4.10 || 2.0.3", + "phpstan/phpstan-phpunit": "^1.0 || ^2", "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "psalm/plugin-phpunit": "0.18.4", - "psr/log": "^1 || ^2 || ^3", - "vimeo/psalm": "4.30.0 || 5.12.0" + "psr/log": "^1 || ^2 || ^3" }, "suggest": { "psr/log": "Allows logging deprecations via PSR-3 logger implementation" @@ -2058,7 +2049,7 @@ "type": "library", "autoload": { "psr-4": { - "Doctrine\\Deprecations\\": "lib/Doctrine/Deprecations" + "Doctrine\\Deprecations\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -2069,40 +2060,46 @@ "homepage": "https://www.doctrine-project.org/", "support": { "issues": "https://github.com/doctrine/deprecations/issues", - "source": "https://github.com/doctrine/deprecations/tree/1.1.3" + "source": "https://github.com/doctrine/deprecations/tree/1.1.4" }, - "time": "2024-01-30T19:34:25+00:00" + "time": "2024-12-07T21:18:45+00:00" }, { "name": "ergebnis/phpunit-slow-test-detector", - "version": "2.15.0", + "version": "2.19.0", "source": { "type": "git", "url": "https://github.com/ergebnis/phpunit-slow-test-detector.git", - "reference": "d9e4ea11d0e7bf1e54df5385bfd94b5156ab0a29" + "reference": "2f66ad7fcc468a5db03592a0c73a930e0c0eccce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ergebnis/phpunit-slow-test-detector/zipball/d9e4ea11d0e7bf1e54df5385bfd94b5156ab0a29", - "reference": "d9e4ea11d0e7bf1e54df5385bfd94b5156ab0a29", + "url": "https://api.github.com/repos/ergebnis/phpunit-slow-test-detector/zipball/2f66ad7fcc468a5db03592a0c73a930e0c0eccce", + "reference": "2f66ad7fcc468a5db03592a0c73a930e0c0eccce", "shasum": "" }, "require": { - "php": "~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0", - "phpunit/phpunit": "^6.5.0 || ^7.5.0 || ^8.5.19 || ^9.0.0 || ^10.0.0 || ^11.0.0" + "php": "~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", + "phpunit/phpunit": "^6.5.0 || ^7.5.0 || ^8.5.19 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0" }, "require-dev": { - "ergebnis/composer-normalize": "^2.43.0", - "ergebnis/license": "^2.4.0", - "ergebnis/php-cs-fixer-config": "^6.31.0", + "ergebnis/composer-normalize": "^2.45.0", + "ergebnis/license": "^2.6.0", + "ergebnis/php-cs-fixer-config": "^6.43.1", "fakerphp/faker": "~1.20.0", - "psalm/plugin-phpunit": "~0.19.0", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.11", + "phpstan/phpstan-deprecation-rules": "^1.2.1", + "phpstan/phpstan-phpunit": "^1.4.1", + "phpstan/phpstan-strict-rules": "^1.6.1", "psr/container": "~1.0.0", - "rector/rector": "^1.1.0", - "vimeo/psalm": "^5.24.0" + "rector/rector": "^1.2.10" }, "type": "library", "extra": { + "branch-alias": { + "dev-main": "2.16-dev" + }, "composer-normalize": { "indent-size": 2, "indent-style": "space" @@ -2138,7 +2135,7 @@ "security": "https://github.com/ergebnis/phpunit-slow-test-detector/blob/main/.github/SECURITY.md", "source": "https://github.com/ergebnis/phpunit-slow-test-detector" }, - "time": "2024-06-16T18:21:35+00:00" + "time": "2025-02-23T15:25:48+00:00" }, { "name": "evenement/evenement", @@ -2234,16 +2231,16 @@ }, { "name": "felixfbecker/language-server-protocol", - "version": "v1.5.2", + "version": "v1.5.3", "source": { "type": "git", "url": "https://github.com/felixfbecker/php-language-server-protocol.git", - "reference": "6e82196ffd7c62f7794d778ca52b69feec9f2842" + "reference": "a9e113dbc7d849e35b8776da39edaf4313b7b6c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/felixfbecker/php-language-server-protocol/zipball/6e82196ffd7c62f7794d778ca52b69feec9f2842", - "reference": "6e82196ffd7c62f7794d778ca52b69feec9f2842", + "url": "https://api.github.com/repos/felixfbecker/php-language-server-protocol/zipball/a9e113dbc7d849e35b8776da39edaf4313b7b6c9", + "reference": "a9e113dbc7d849e35b8776da39edaf4313b7b6c9", "shasum": "" }, "require": { @@ -2284,22 +2281,22 @@ ], "support": { "issues": "https://github.com/felixfbecker/php-language-server-protocol/issues", - "source": "https://github.com/felixfbecker/php-language-server-protocol/tree/v1.5.2" + "source": "https://github.com/felixfbecker/php-language-server-protocol/tree/v1.5.3" }, - "time": "2022-03-02T22:36:06+00:00" + "time": "2024-04-30T00:40:11+00:00" }, { "name": "fidry/cpu-core-counter", - "version": "1.1.0", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/theofidry/cpu-core-counter.git", - "reference": "f92996c4d5c1a696a6a970e20f7c4216200fcc42" + "reference": "8520451a140d3f46ac33042715115e290cf5785f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/f92996c4d5c1a696a6a970e20f7c4216200fcc42", - "reference": "f92996c4d5c1a696a6a970e20f7c4216200fcc42", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/8520451a140d3f46ac33042715115e290cf5785f", + "reference": "8520451a140d3f46ac33042715115e290cf5785f", "shasum": "" }, "require": { @@ -2339,7 +2336,7 @@ ], "support": { "issues": "https://github.com/theofidry/cpu-core-counter/issues", - "source": "https://github.com/theofidry/cpu-core-counter/tree/1.1.0" + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.2.0" }, "funding": [ { @@ -2347,30 +2344,30 @@ "type": "github" } ], - "time": "2024-02-07T09:43:46+00:00" + "time": "2024-08-06T10:04:20+00:00" }, { "name": "filp/whoops", - "version": "2.15.4", + "version": "2.18.0", "source": { "type": "git", "url": "https://github.com/filp/whoops.git", - "reference": "a139776fa3f5985a50b509f2a02ff0f709d2a546" + "reference": "a7de6c3c6c3c022f5cfc337f8ede6a14460cf77e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filp/whoops/zipball/a139776fa3f5985a50b509f2a02ff0f709d2a546", - "reference": "a139776fa3f5985a50b509f2a02ff0f709d2a546", + "url": "https://api.github.com/repos/filp/whoops/zipball/a7de6c3c6c3c022f5cfc337f8ede6a14460cf77e", + "reference": "a7de6c3c6c3c022f5cfc337f8ede6a14460cf77e", "shasum": "" }, "require": { - "php": "^5.5.9 || ^7.0 || ^8.0", + "php": "^7.1 || ^8.0", "psr/log": "^1.0.1 || ^2.0 || ^3.0" }, "require-dev": { - "mockery/mockery": "^0.9 || ^1.0", - "phpunit/phpunit": "^4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.3", - "symfony/var-dumper": "^2.6 || ^3.0 || ^4.0 || ^5.0" + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^7.5.20 || ^8.5.8 || ^9.3.3", + "symfony/var-dumper": "^4.0 || ^5.0" }, "suggest": { "symfony/var-dumper": "Pretty print complex values better with var-dumper available", @@ -2410,7 +2407,7 @@ ], "support": { "issues": "https://github.com/filp/whoops/issues", - "source": "https://github.com/filp/whoops/tree/2.15.4" + "source": "https://github.com/filp/whoops/tree/2.18.0" }, "funding": [ { @@ -2418,20 +2415,20 @@ "type": "github" } ], - "time": "2023-11-03T12:00:00+00:00" + "time": "2025-03-15T12:00:00+00:00" }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.61.1", + "version": "v3.75.0", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "94a87189f55814e6cabca2d9a33b06de384a2ab8" + "reference": "399a128ff2fdaf4281e4e79b755693286cdf325c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/94a87189f55814e6cabca2d9a33b06de384a2ab8", - "reference": "94a87189f55814e6cabca2d9a33b06de384a2ab8", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/399a128ff2fdaf4281e4e79b755693286cdf325c", + "reference": "399a128ff2fdaf4281e4e79b755693286cdf325c", "shasum": "" }, "require": { @@ -2439,40 +2436,41 @@ "composer/semver": "^3.4", "composer/xdebug-handler": "^3.0.3", "ext-filter": "*", + "ext-hash": "*", "ext-json": "*", "ext-tokenizer": "*", - "fidry/cpu-core-counter": "^1.0", + "fidry/cpu-core-counter": "^1.2", "php": "^7.4 || ^8.0", "react/child-process": "^0.6.5", "react/event-loop": "^1.0", "react/promise": "^2.0 || ^3.0", "react/socket": "^1.0", "react/stream": "^1.0", - "sebastian/diff": "^4.0 || ^5.0 || ^6.0", - "symfony/console": "^5.4 || ^6.0 || ^7.0", - "symfony/event-dispatcher": "^5.4 || ^6.0 || ^7.0", - "symfony/filesystem": "^5.4 || ^6.0 || ^7.0", - "symfony/finder": "^5.4 || ^6.0 || ^7.0", - "symfony/options-resolver": "^5.4 || ^6.0 || ^7.0", - "symfony/polyfill-mbstring": "^1.28", - "symfony/polyfill-php80": "^1.28", - "symfony/polyfill-php81": "^1.28", - "symfony/process": "^5.4 || ^6.0 || ^7.0", - "symfony/stopwatch": "^5.4 || ^6.0 || ^7.0" + "sebastian/diff": "^4.0 || ^5.1 || ^6.0 || ^7.0", + "symfony/console": "^5.4 || ^6.4 || ^7.0", + "symfony/event-dispatcher": "^5.4 || ^6.4 || ^7.0", + "symfony/filesystem": "^5.4 || ^6.4 || ^7.0", + "symfony/finder": "^5.4 || ^6.4 || ^7.0", + "symfony/options-resolver": "^5.4 || ^6.4 || ^7.0", + "symfony/polyfill-mbstring": "^1.31", + "symfony/polyfill-php80": "^1.31", + "symfony/polyfill-php81": "^1.31", + "symfony/process": "^5.4 || ^6.4 || ^7.2", + "symfony/stopwatch": "^5.4 || ^6.4 || ^7.0" }, "require-dev": { - "facile-it/paraunit": "^1.3 || ^2.3", - "infection/infection": "^0.29.5", - "justinrainbow/json-schema": "^5.2", + "facile-it/paraunit": "^1.3.1 || ^2.6", + "infection/infection": "^0.29.14", + "justinrainbow/json-schema": "^5.3 || ^6.2", "keradus/cli-executor": "^2.1", - "mikey179/vfsstream": "^1.6.11", + "mikey179/vfsstream": "^1.6.12", "php-coveralls/php-coveralls": "^2.7", "php-cs-fixer/accessible-object": "^1.1", - "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.5", - "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.5", - "phpunit/phpunit": "^9.6.19 || ^10.5.21 || ^11.2", - "symfony/var-dumper": "^5.4 || ^6.0 || ^7.0", - "symfony/yaml": "^5.4 || ^6.0 || ^7.0" + "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.6", + "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.6", + "phpunit/phpunit": "^9.6.22 || ^10.5.45 || ^11.5.12", + "symfony/var-dumper": "^5.4.48 || ^6.4.18 || ^7.2.3", + "symfony/yaml": "^5.4.45 || ^6.4.18 || ^7.2.3" }, "suggest": { "ext-dom": "For handling output formats in XML", @@ -2513,7 +2511,7 @@ ], "support": { "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.61.1" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.75.0" }, "funding": [ { @@ -2521,32 +2519,33 @@ "type": "github" } ], - "time": "2024-07-31T14:33:15+00:00" + "time": "2025-03-31T18:40:42+00:00" }, { "name": "jean85/pretty-package-versions", - "version": "2.0.6", + "version": "2.1.1", "source": { "type": "git", "url": "https://github.com/Jean85/pretty-package-versions.git", - "reference": "f9fdd29ad8e6d024f52678b570e5593759b550b4" + "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/f9fdd29ad8e6d024f52678b570e5593759b550b4", - "reference": "f9fdd29ad8e6d024f52678b570e5593759b550b4", + "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/4d7aa5dab42e2a76d99559706022885de0e18e1a", + "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a", "shasum": "" }, "require": { - "composer-runtime-api": "^2.0.0", - "php": "^7.1|^8.0" + "composer-runtime-api": "^2.1.0", + "php": "^7.4|^8.0" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.2", "jean85/composer-provided-replaced-stub-package": "^1.0", - "phpstan/phpstan": "^1.4", - "phpunit/phpunit": "^7.5|^8.5|^9.4", - "vimeo/psalm": "^4.3" + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^7.5|^8.5|^9.6", + "rector/rector": "^2.0", + "vimeo/psalm": "^4.3 || ^5.0" }, "type": "library", "extra": { @@ -2578,22 +2577,22 @@ ], "support": { "issues": "https://github.com/Jean85/pretty-package-versions/issues", - "source": "https://github.com/Jean85/pretty-package-versions/tree/2.0.6" + "source": "https://github.com/Jean85/pretty-package-versions/tree/2.1.1" }, - "time": "2024-03-08T09:58:59+00:00" + "time": "2025-03-19T14:43:43+00:00" }, { "name": "myclabs/deep-copy", - "version": "1.12.0", + "version": "1.13.0", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c" + "reference": "024473a478be9df5fdaca2c793f2232fe788e414" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", - "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/024473a478be9df5fdaca2c793f2232fe788e414", + "reference": "024473a478be9df5fdaca2c793f2232fe788e414", "shasum": "" }, "require": { @@ -2632,7 +2631,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.0" }, "funding": [ { @@ -2640,20 +2639,20 @@ "type": "tidelift" } ], - "time": "2024-06-12T14:39:25+00:00" + "time": "2025-02-12T12:17:51+00:00" }, { "name": "netresearch/jsonmapper", - "version": "v4.4.1", + "version": "v4.5.0", "source": { "type": "git", "url": "https://github.com/cweiske/jsonmapper.git", - "reference": "132c75c7dd83e45353ebb9c6c9f591952995bbf0" + "reference": "8e76efb98ee8b6afc54687045e1b8dba55ac76e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/132c75c7dd83e45353ebb9c6c9f591952995bbf0", - "reference": "132c75c7dd83e45353ebb9c6c9f591952995bbf0", + "url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/8e76efb98ee8b6afc54687045e1b8dba55ac76e5", + "reference": "8e76efb98ee8b6afc54687045e1b8dba55ac76e5", "shasum": "" }, "require": { @@ -2689,22 +2688,22 @@ "support": { "email": "cweiske@cweiske.de", "issues": "https://github.com/cweiske/jsonmapper/issues", - "source": "https://github.com/cweiske/jsonmapper/tree/v4.4.1" + "source": "https://github.com/cweiske/jsonmapper/tree/v4.5.0" }, - "time": "2024-01-31T06:18:54+00:00" + "time": "2024-09-08T10:13:13+00:00" }, { "name": "nikic/php-parser", - "version": "v4.19.1", + "version": "v4.19.4", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "4e1b88d21c69391150ace211e9eaf05810858d0b" + "reference": "715f4d25e225bc47b293a8b997fe6ce99bf987d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/4e1b88d21c69391150ace211e9eaf05810858d0b", - "reference": "4e1b88d21c69391150ace211e9eaf05810858d0b", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/715f4d25e225bc47b293a8b997fe6ce99bf987d2", + "reference": "715f4d25e225bc47b293a8b997fe6ce99bf987d2", "shasum": "" }, "require": { @@ -2713,7 +2712,7 @@ }, "require-dev": { "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" }, "bin": [ "bin/php-parse" @@ -2745,46 +2744,46 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.19.1" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.19.4" }, - "time": "2024-03-17T08:10:35+00:00" + "time": "2024-09-29T15:01:53+00:00" }, { "name": "nunomaduro/collision", - "version": "v7.10.0", + "version": "v7.12.0", "source": { "type": "git", "url": "https://github.com/nunomaduro/collision.git", - "reference": "49ec67fa7b002712da8526678abd651c09f375b2" + "reference": "995245421d3d7593a6960822063bdba4f5d7cf1a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/collision/zipball/49ec67fa7b002712da8526678abd651c09f375b2", - "reference": "49ec67fa7b002712da8526678abd651c09f375b2", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/995245421d3d7593a6960822063bdba4f5d7cf1a", + "reference": "995245421d3d7593a6960822063bdba4f5d7cf1a", "shasum": "" }, "require": { - "filp/whoops": "^2.15.3", - "nunomaduro/termwind": "^1.15.1", + "filp/whoops": "^2.17.0", + "nunomaduro/termwind": "^1.17.0", "php": "^8.1.0", - "symfony/console": "^6.3.4" + "symfony/console": "^6.4.17" }, "conflict": { "laravel/framework": ">=11.0.0" }, "require-dev": { - "brianium/paratest": "^7.3.0", - "laravel/framework": "^10.28.0", - "laravel/pint": "^1.13.3", - "laravel/sail": "^1.25.0", - "laravel/sanctum": "^3.3.1", - "laravel/tinker": "^2.8.2", - "nunomaduro/larastan": "^2.6.4", - "orchestra/testbench-core": "^8.13.0", - "pestphp/pest": "^2.23.2", - "phpunit/phpunit": "^10.4.1", - "sebastian/environment": "^6.0.1", - "spatie/laravel-ignition": "^2.3.1" + "brianium/paratest": "^7.4.8", + "laravel/framework": "^10.48.29", + "laravel/pint": "^1.21.2", + "laravel/sail": "^1.41.0", + "laravel/sanctum": "^3.3.3", + "laravel/tinker": "^2.10.1", + "nunomaduro/larastan": "^2.10.0", + "orchestra/testbench-core": "^8.35.0", + "pestphp/pest": "^2.36.0", + "phpunit/phpunit": "^10.5.36", + "sebastian/environment": "^6.1.0", + "spatie/laravel-ignition": "^2.9.1" }, "type": "library", "extra": { @@ -2843,37 +2842,36 @@ "type": "patreon" } ], - "time": "2023-10-11T15:45:01+00:00" + "time": "2025-03-14T22:35:49+00:00" }, { "name": "nunomaduro/termwind", - "version": "v1.15.1", + "version": "v1.17.0", "source": { "type": "git", "url": "https://github.com/nunomaduro/termwind.git", - "reference": "8ab0b32c8caa4a2e09700ea32925441385e4a5dc" + "reference": "5369ef84d8142c1d87e4ec278711d4ece3cbf301" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/8ab0b32c8caa4a2e09700ea32925441385e4a5dc", - "reference": "8ab0b32c8caa4a2e09700ea32925441385e4a5dc", + "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/5369ef84d8142c1d87e4ec278711d4ece3cbf301", + "reference": "5369ef84d8142c1d87e4ec278711d4ece3cbf301", "shasum": "" }, "require": { "ext-mbstring": "*", - "php": "^8.0", - "symfony/console": "^5.3.0|^6.0.0" + "php": "^8.1", + "symfony/console": "^6.4.15" }, "require-dev": { - "ergebnis/phpstan-rules": "^1.0.", - "illuminate/console": "^8.0|^9.0", - "illuminate/support": "^8.0|^9.0", - "laravel/pint": "^1.0.0", - "pestphp/pest": "^1.21.0", - "pestphp/pest-plugin-mock": "^1.0", - "phpstan/phpstan": "^1.4.6", - "phpstan/phpstan-strict-rules": "^1.1.0", - "symfony/var-dumper": "^5.2.7|^6.0.0", + "illuminate/console": "^10.48.24", + "illuminate/support": "^10.48.24", + "laravel/pint": "^1.18.2", + "pestphp/pest": "^2.36.0", + "pestphp/pest-plugin-mock": "2.0.0", + "phpstan/phpstan": "^1.12.11", + "phpstan/phpstan-strict-rules": "^1.6.1", + "symfony/var-dumper": "^6.4.15", "thecodingmachine/phpstan-strict-rules": "^1.0.0" }, "type": "library", @@ -2913,7 +2911,7 @@ ], "support": { "issues": "https://github.com/nunomaduro/termwind/issues", - "source": "https://github.com/nunomaduro/termwind/tree/v1.15.1" + "source": "https://github.com/nunomaduro/termwind/tree/v1.17.0" }, "funding": [ { @@ -2929,20 +2927,20 @@ "type": "github" } ], - "time": "2023-02-08T01:06:31+00:00" + "time": "2024-11-21T10:36:35+00:00" }, { "name": "nyholm/psr7", - "version": "1.8.1", + "version": "1.8.2", "source": { "type": "git", "url": "https://github.com/Nyholm/psr7.git", - "reference": "aa5fc277a4f5508013d571341ade0c3886d4d00e" + "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Nyholm/psr7/zipball/aa5fc277a4f5508013d571341ade0c3886d4d00e", - "reference": "aa5fc277a4f5508013d571341ade0c3886d4d00e", + "url": "https://api.github.com/repos/Nyholm/psr7/zipball/a71f2b11690f4b24d099d6b16690a90ae14fc6f3", + "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3", "shasum": "" }, "require": { @@ -2995,7 +2993,7 @@ ], "support": { "issues": "https://github.com/Nyholm/psr7/issues", - "source": "https://github.com/Nyholm/psr7/tree/1.8.1" + "source": "https://github.com/Nyholm/psr7/tree/1.8.2" }, "funding": [ { @@ -3007,40 +3005,41 @@ "type": "github" } ], - "time": "2023-11-13T09:31:12+00:00" + "time": "2024-09-09T07:06:30+00:00" }, { "name": "pestphp/pest", - "version": "v2.35.0", + "version": "v2.36.0", "source": { "type": "git", "url": "https://github.com/pestphp/pest.git", - "reference": "d0ff2c8ec294b7aa7fcb0f3ddc4fdec864234646" + "reference": "f8c88bd14dc1772bfaf02169afb601ecdf2724cd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest/zipball/d0ff2c8ec294b7aa7fcb0f3ddc4fdec864234646", - "reference": "d0ff2c8ec294b7aa7fcb0f3ddc4fdec864234646", + "url": "https://api.github.com/repos/pestphp/pest/zipball/f8c88bd14dc1772bfaf02169afb601ecdf2724cd", + "reference": "f8c88bd14dc1772bfaf02169afb601ecdf2724cd", "shasum": "" }, "require": { "brianium/paratest": "^7.3.1", - "nunomaduro/collision": "^7.10.0|^8.3.0", - "nunomaduro/termwind": "^1.15.1|^2.0.1", + "nunomaduro/collision": "^7.11.0|^8.4.0", + "nunomaduro/termwind": "^1.16.0|^2.1.0", "pestphp/pest-plugin": "^2.1.1", "pestphp/pest-plugin-arch": "^2.7.0", "php": "^8.1.0", - "phpunit/phpunit": "^10.5.17" + "phpunit/phpunit": "^10.5.36" }, "conflict": { - "phpunit/phpunit": ">10.5.17", + "filp/whoops": "<2.16.0", + "phpunit/phpunit": ">10.5.36", "sebastian/exporter": "<5.1.0", "webmozart/assert": "<1.11.0" }, "require-dev": { - "pestphp/pest-dev-tools": "^2.16.0", - "pestphp/pest-plugin-type-coverage": "^2.8.5", - "symfony/process": "^6.4.0|^7.1.3" + "pestphp/pest-dev-tools": "^2.17.0", + "pestphp/pest-plugin-type-coverage": "^2.8.7", + "symfony/process": "^6.4.0|^7.1.5" }, "bin": [ "bin/pest" @@ -3103,7 +3102,7 @@ ], "support": { "issues": "https://github.com/pestphp/pest/issues", - "source": "https://github.com/pestphp/pest/tree/v2.35.0" + "source": "https://github.com/pestphp/pest/tree/v2.36.0" }, "funding": [ { @@ -3115,7 +3114,7 @@ "type": "github" } ], - "time": "2024-08-02T10:57:29+00:00" + "time": "2024-10-15T15:30:56+00:00" }, { "name": "pestphp/pest-plugin", @@ -3378,16 +3377,16 @@ }, { "name": "php-http/message", - "version": "1.16.1", + "version": "1.16.2", "source": { "type": "git", "url": "https://github.com/php-http/message.git", - "reference": "5997f3289332c699fa2545c427826272498a2088" + "reference": "06dd5e8562f84e641bf929bfe699ee0f5ce8080a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/message/zipball/5997f3289332c699fa2545c427826272498a2088", - "reference": "5997f3289332c699fa2545c427826272498a2088", + "url": "https://api.github.com/repos/php-http/message/zipball/06dd5e8562f84e641bf929bfe699ee0f5ce8080a", + "reference": "06dd5e8562f84e641bf929bfe699ee0f5ce8080a", "shasum": "" }, "require": { @@ -3441,9 +3440,9 @@ ], "support": { "issues": "https://github.com/php-http/message/issues", - "source": "https://github.com/php-http/message/tree/1.16.1" + "source": "https://github.com/php-http/message/tree/1.16.2" }, - "time": "2024-03-07T13:22:09+00:00" + "time": "2024-10-02T11:34:13+00:00" }, { "name": "phpdocumentor/reflection-common", @@ -3500,16 +3499,16 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "5.4.1", + "version": "5.6.1", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "9d07b3f7fdcf5efec5d1609cba3c19c5ea2bdc9c" + "reference": "e5e784149a09bd69d9a5e3b01c5cbd2e2bd653d8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/9d07b3f7fdcf5efec5d1609cba3c19c5ea2bdc9c", - "reference": "9d07b3f7fdcf5efec5d1609cba3c19c5ea2bdc9c", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/e5e784149a09bd69d9a5e3b01c5cbd2e2bd653d8", + "reference": "e5e784149a09bd69d9a5e3b01c5cbd2e2bd653d8", "shasum": "" }, "require": { @@ -3518,17 +3517,17 @@ "php": "^7.4 || ^8.0", "phpdocumentor/reflection-common": "^2.2", "phpdocumentor/type-resolver": "^1.7", - "phpstan/phpdoc-parser": "^1.7", + "phpstan/phpdoc-parser": "^1.7|^2.0", "webmozart/assert": "^1.9.1" }, "require-dev": { - "mockery/mockery": "~1.3.5", + "mockery/mockery": "~1.3.5 || ~1.6.0", "phpstan/extension-installer": "^1.1", "phpstan/phpstan": "^1.8", "phpstan/phpstan-mockery": "^1.1", "phpstan/phpstan-webmozart-assert": "^1.2", "phpunit/phpunit": "^9.5", - "vimeo/psalm": "^5.13" + "psalm/phar": "^5.26" }, "type": "library", "extra": { @@ -3558,29 +3557,29 @@ "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", "support": { "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.4.1" + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.1" }, - "time": "2024-05-21T05:55:05+00:00" + "time": "2024-12-07T09:39:29+00:00" }, { "name": "phpdocumentor/type-resolver", - "version": "1.8.2", + "version": "1.10.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "153ae662783729388a584b4361f2545e4d841e3c" + "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/153ae662783729388a584b4361f2545e4d841e3c", - "reference": "153ae662783729388a584b4361f2545e4d841e3c", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/679e3ce485b99e84c775d28e2e96fade9a7fb50a", + "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a", "shasum": "" }, "require": { "doctrine/deprecations": "^1.0", "php": "^7.3 || ^8.0", "phpdocumentor/reflection-common": "^2.0", - "phpstan/phpdoc-parser": "^1.13" + "phpstan/phpdoc-parser": "^1.18|^2.0" }, "require-dev": { "ext-tokenizer": "*", @@ -3616,36 +3615,36 @@ "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", "support": { "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.8.2" + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.10.0" }, - "time": "2024-02-23T11:10:43+00:00" + "time": "2024-11-09T15:12:26+00:00" }, { "name": "phpstan/phpdoc-parser", - "version": "1.29.1", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "fcaefacf2d5c417e928405b71b400d4ce10daaf4" + "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/fcaefacf2d5c417e928405b71b400d4ce10daaf4", - "reference": "fcaefacf2d5c417e928405b71b400d4ce10daaf4", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", + "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0" + "php": "^7.4 || ^8.0" }, "require-dev": { "doctrine/annotations": "^2.0", - "nikic/php-parser": "^4.15", + "nikic/php-parser": "^5.3.0", "php-parallel-lint/php-parallel-lint": "^1.2", "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^1.5", - "phpstan/phpstan-phpunit": "^1.1", - "phpstan/phpstan-strict-rules": "^1.0", - "phpunit/phpunit": "^9.5", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", "symfony/process": "^5.2" }, "type": "library", @@ -3663,38 +3662,38 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.29.1" + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.1.0" }, - "time": "2024-05-31T08:52:43+00:00" + "time": "2025-02-19T13:28:12+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "10.1.15", + "version": "10.1.16", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "5da8b1728acd1e6ffdf2ff32ffbdfd04307f26ae" + "reference": "7e308268858ed6baedc8704a304727d20bc07c77" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/5da8b1728acd1e6ffdf2ff32ffbdfd04307f26ae", - "reference": "5da8b1728acd1e6ffdf2ff32ffbdfd04307f26ae", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7e308268858ed6baedc8704a304727d20bc07c77", + "reference": "7e308268858ed6baedc8704a304727d20bc07c77", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.18 || ^5.0", + "nikic/php-parser": "^4.19.1 || ^5.1.0", "php": ">=8.1", - "phpunit/php-file-iterator": "^4.0", - "phpunit/php-text-template": "^3.0", - "sebastian/code-unit-reverse-lookup": "^3.0", - "sebastian/complexity": "^3.0", - "sebastian/environment": "^6.0", - "sebastian/lines-of-code": "^2.0", - "sebastian/version": "^4.0", - "theseer/tokenizer": "^1.2.0" + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-text-template": "^3.0.1", + "sebastian/code-unit-reverse-lookup": "^3.0.0", + "sebastian/complexity": "^3.2.0", + "sebastian/environment": "^6.1.0", + "sebastian/lines-of-code": "^2.0.2", + "sebastian/version": "^4.0.1", + "theseer/tokenizer": "^1.2.3" }, "require-dev": { "phpunit/phpunit": "^10.1" @@ -3706,7 +3705,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "10.1-dev" + "dev-main": "10.1.x-dev" } }, "autoload": { @@ -3735,7 +3734,7 @@ "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/10.1.15" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.16" }, "funding": [ { @@ -3743,7 +3742,7 @@ "type": "github" } ], - "time": "2024-06-29T08:25:15+00:00" + "time": "2024-08-22T04:31:57+00:00" }, { "name": "phpunit/php-file-iterator", @@ -3990,16 +3989,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.5.17", + "version": "10.5.36", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "c1f736a473d21957ead7e94fcc029f571895abf5" + "reference": "aa0a8ce701ea7ee314b0dfaa8970dc94f3f8c870" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c1f736a473d21957ead7e94fcc029f571895abf5", - "reference": "c1f736a473d21957ead7e94fcc029f571895abf5", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/aa0a8ce701ea7ee314b0dfaa8970dc94f3f8c870", + "reference": "aa0a8ce701ea7ee314b0dfaa8970dc94f3f8c870", "shasum": "" }, "require": { @@ -4009,26 +4008,26 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.10.1", - "phar-io/manifest": "^2.0.3", - "phar-io/version": "^3.0.2", + "myclabs/deep-copy": "^1.12.0", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", "php": ">=8.1", - "phpunit/php-code-coverage": "^10.1.5", - "phpunit/php-file-iterator": "^4.0", - "phpunit/php-invoker": "^4.0", - "phpunit/php-text-template": "^3.0", - "phpunit/php-timer": "^6.0", - "sebastian/cli-parser": "^2.0", - "sebastian/code-unit": "^2.0", - "sebastian/comparator": "^5.0", - "sebastian/diff": "^5.0", - "sebastian/environment": "^6.0", - "sebastian/exporter": "^5.1", - "sebastian/global-state": "^6.0.1", - "sebastian/object-enumerator": "^5.0", - "sebastian/recursion-context": "^5.0", - "sebastian/type": "^4.0", - "sebastian/version": "^4.0" + "phpunit/php-code-coverage": "^10.1.16", + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-invoker": "^4.0.0", + "phpunit/php-text-template": "^3.0.1", + "phpunit/php-timer": "^6.0.0", + "sebastian/cli-parser": "^2.0.1", + "sebastian/code-unit": "^2.0.0", + "sebastian/comparator": "^5.0.2", + "sebastian/diff": "^5.1.1", + "sebastian/environment": "^6.1.0", + "sebastian/exporter": "^5.1.2", + "sebastian/global-state": "^6.0.2", + "sebastian/object-enumerator": "^5.0.0", + "sebastian/recursion-context": "^5.0.0", + "sebastian/type": "^4.0.0", + "sebastian/version": "^4.0.1" }, "suggest": { "ext-soap": "To be able to generate mocks based on WSDL files" @@ -4071,7 +4070,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.17" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.36" }, "funding": [ { @@ -4087,7 +4086,7 @@ "type": "tidelift" } ], - "time": "2024-04-05T04:39:01+00:00" + "time": "2024-10-08T15:36:51+00:00" }, { "name": "psr/event-dispatcher", @@ -4321,33 +4320,33 @@ }, { "name": "react/child-process", - "version": "v0.6.5", + "version": "v0.6.6", "source": { "type": "git", "url": "https://github.com/reactphp/child-process.git", - "reference": "e71eb1aa55f057c7a4a0d08d06b0b0a484bead43" + "reference": "1721e2b93d89b745664353b9cfc8f155ba8a6159" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/child-process/zipball/e71eb1aa55f057c7a4a0d08d06b0b0a484bead43", - "reference": "e71eb1aa55f057c7a4a0d08d06b0b0a484bead43", + "url": "https://api.github.com/repos/reactphp/child-process/zipball/1721e2b93d89b745664353b9cfc8f155ba8a6159", + "reference": "1721e2b93d89b745664353b9cfc8f155ba8a6159", "shasum": "" }, "require": { "evenement/evenement": "^3.0 || ^2.0 || ^1.0", "php": ">=5.3.0", "react/event-loop": "^1.2", - "react/stream": "^1.2" + "react/stream": "^1.4" }, "require-dev": { - "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35", - "react/socket": "^1.8", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/socket": "^1.16", "sebastian/environment": "^5.0 || ^3.0 || ^2.0 || ^1.0" }, "type": "library", "autoload": { "psr-4": { - "React\\ChildProcess\\": "src" + "React\\ChildProcess\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -4384,19 +4383,15 @@ ], "support": { "issues": "https://github.com/reactphp/child-process/issues", - "source": "https://github.com/reactphp/child-process/tree/v0.6.5" + "source": "https://github.com/reactphp/child-process/tree/v0.6.6" }, "funding": [ { - "url": "https://github.com/WyriHaximus", - "type": "github" - }, - { - "url": "https://github.com/clue", - "type": "github" + "url": "https://opencollective.com/reactphp", + "type": "open_collective" } ], - "time": "2022-09-16T13:41:56+00:00" + "time": "2025-01-01T16:37:48+00:00" }, { "name": "react/dns", @@ -4802,16 +4797,16 @@ }, { "name": "sebastian/comparator", - "version": "5.0.1", + "version": "5.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "2db5010a484d53ebf536087a70b4a5423c102372" + "reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2db5010a484d53ebf536087a70b4a5423c102372", - "reference": "2db5010a484d53ebf536087a70b4a5423c102372", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e", + "reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e", "shasum": "" }, "require": { @@ -4822,7 +4817,7 @@ "sebastian/exporter": "^5.0" }, "require-dev": { - "phpunit/phpunit": "^10.3" + "phpunit/phpunit": "^10.5" }, "type": "library", "extra": { @@ -4867,7 +4862,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.1" + "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.3" }, "funding": [ { @@ -4875,7 +4870,7 @@ "type": "github" } ], - "time": "2023-08-14T13:18:12+00:00" + "time": "2024-10-18T14:56:07+00:00" }, { "name": "sebastian/complexity", @@ -5550,16 +5545,16 @@ }, { "name": "spatie/array-to-xml", - "version": "3.3.0", + "version": "3.4.0", "source": { "type": "git", "url": "https://github.com/spatie/array-to-xml.git", - "reference": "f56b220fe2db1ade4c88098d83413ebdfc3bf876" + "reference": "7dcfc67d60b0272926dabad1ec01f6b8a5fb5e67" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/array-to-xml/zipball/f56b220fe2db1ade4c88098d83413ebdfc3bf876", - "reference": "f56b220fe2db1ade4c88098d83413ebdfc3bf876", + "url": "https://api.github.com/repos/spatie/array-to-xml/zipball/7dcfc67d60b0272926dabad1ec01f6b8a5fb5e67", + "reference": "7dcfc67d60b0272926dabad1ec01f6b8a5fb5e67", "shasum": "" }, "require": { @@ -5602,7 +5597,7 @@ "xml" ], "support": { - "source": "https://github.com/spatie/array-to-xml/tree/3.3.0" + "source": "https://github.com/spatie/array-to-xml/tree/3.4.0" }, "funding": [ { @@ -5614,20 +5609,20 @@ "type": "github" } ], - "time": "2024-05-01T10:20:27+00:00" + "time": "2024-12-16T12:45:15+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v6.4.8", + "version": "v6.4.13", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "8d7507f02b06e06815e56bb39aa0128e3806208b" + "reference": "0ffc48080ab3e9132ea74ef4e09d8dcf26bf897e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/8d7507f02b06e06815e56bb39aa0128e3806208b", - "reference": "8d7507f02b06e06815e56bb39aa0128e3806208b", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/0ffc48080ab3e9132ea74ef4e09d8dcf26bf897e", + "reference": "0ffc48080ab3e9132ea74ef4e09d8dcf26bf897e", "shasum": "" }, "require": { @@ -5678,7 +5673,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v6.4.8" + "source": "https://github.com/symfony/event-dispatcher/tree/v6.4.13" }, "funding": [ { @@ -5694,20 +5689,20 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:49:08+00:00" + "time": "2024-09-25T14:18:03+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v3.5.0", + "version": "v3.5.1", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50" + "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/8f93aec25d41b72493c6ddff14e916177c9efc50", - "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/7642f5e970b672283b7823222ae8ef8bbc160b9f", + "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f", "shasum": "" }, "require": { @@ -5716,12 +5711,12 @@ }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" } }, "autoload": { @@ -5754,7 +5749,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.0" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.1" }, "funding": [ { @@ -5770,20 +5765,20 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:32:20+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "symfony/filesystem", - "version": "v6.4.9", + "version": "v6.4.13", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "b51ef8059159330b74a4d52f68e671033c0fe463" + "reference": "4856c9cf585d5a0313d8d35afd681a526f038dd3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/b51ef8059159330b74a4d52f68e671033c0fe463", - "reference": "b51ef8059159330b74a4d52f68e671033c0fe463", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/4856c9cf585d5a0313d8d35afd681a526f038dd3", + "reference": "4856c9cf585d5a0313d8d35afd681a526f038dd3", "shasum": "" }, "require": { @@ -5820,7 +5815,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v6.4.9" + "source": "https://github.com/symfony/filesystem/tree/v6.4.13" }, "funding": [ { @@ -5836,20 +5831,20 @@ "type": "tidelift" } ], - "time": "2024-06-28T09:49:33+00:00" + "time": "2024-10-25T15:07:50+00:00" }, { "name": "symfony/finder", - "version": "v6.4.10", + "version": "v6.4.17", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "af29198d87112bebdd397bd7735fbd115997824c" + "reference": "1d0e8266248c5d9ab6a87e3789e6dc482af3c9c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/af29198d87112bebdd397bd7735fbd115997824c", - "reference": "af29198d87112bebdd397bd7735fbd115997824c", + "url": "https://api.github.com/repos/symfony/finder/zipball/1d0e8266248c5d9ab6a87e3789e6dc482af3c9c7", + "reference": "1d0e8266248c5d9ab6a87e3789e6dc482af3c9c7", "shasum": "" }, "require": { @@ -5884,7 +5879,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v6.4.10" + "source": "https://github.com/symfony/finder/tree/v6.4.17" }, "funding": [ { @@ -5900,20 +5895,20 @@ "type": "tidelift" } ], - "time": "2024-07-24T07:06:38+00:00" + "time": "2024-12-29T13:51:37+00:00" }, { "name": "symfony/options-resolver", - "version": "v6.4.8", + "version": "v6.4.16", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "22ab9e9101ab18de37839074f8a1197f55590c1b" + "reference": "368128ad168f20e22c32159b9f761e456cec0c78" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/22ab9e9101ab18de37839074f8a1197f55590c1b", - "reference": "22ab9e9101ab18de37839074f8a1197f55590c1b", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/368128ad168f20e22c32159b9f761e456cec0c78", + "reference": "368128ad168f20e22c32159b9f761e456cec0c78", "shasum": "" }, "require": { @@ -5951,7 +5946,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v6.4.8" + "source": "https://github.com/symfony/options-resolver/tree/v6.4.16" }, "funding": [ { @@ -5967,30 +5962,30 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:49:08+00:00" + "time": "2024-11-20T10:57:02+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.30.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "77fa7995ac1b21ab60769b7323d600a991a90433" + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/77fa7995ac1b21ab60769b7323d600a991a90433", - "reference": "77fa7995ac1b21ab60769b7323d600a991a90433", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -6031,7 +6026,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" }, "funding": [ { @@ -6047,30 +6042,30 @@ "type": "tidelift" } ], - "time": "2024-05-31T15:07:36+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-php81", - "version": "v1.30.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php81.git", - "reference": "3fb075789fb91f9ad9af537c4012d523085bd5af" + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/3fb075789fb91f9ad9af537c4012d523085bd5af", - "reference": "3fb075789fb91f9ad9af537c4012d523085bd5af", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -6107,7 +6102,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-php81/tree/v1.31.0" }, "funding": [ { @@ -6123,20 +6118,20 @@ "type": "tidelift" } ], - "time": "2024-06-19T12:30:46+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/process", - "version": "v6.4.8", + "version": "v6.4.20", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "8d92dd79149f29e89ee0f480254db595f6a6a2c5" + "reference": "e2a61c16af36c9a07e5c9906498b73e091949a20" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/8d92dd79149f29e89ee0f480254db595f6a6a2c5", - "reference": "8d92dd79149f29e89ee0f480254db595f6a6a2c5", + "url": "https://api.github.com/repos/symfony/process/zipball/e2a61c16af36c9a07e5c9906498b73e091949a20", + "reference": "e2a61c16af36c9a07e5c9906498b73e091949a20", "shasum": "" }, "require": { @@ -6168,7 +6163,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v6.4.8" + "source": "https://github.com/symfony/process/tree/v6.4.20" }, "funding": [ { @@ -6184,20 +6179,20 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:49:08+00:00" + "time": "2025-03-10T17:11:00+00:00" }, { "name": "symfony/stopwatch", - "version": "v6.4.8", + "version": "v6.4.19", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "63e069eb616049632cde9674c46957819454b8aa" + "reference": "dfe1481c12c06266d0c3d58c0cb4b09bd497ab9c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/63e069eb616049632cde9674c46957819454b8aa", - "reference": "63e069eb616049632cde9674c46957819454b8aa", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/dfe1481c12c06266d0c3d58c0cb4b09bd497ab9c", + "reference": "dfe1481c12c06266d0c3d58c0cb4b09bd497ab9c", "shasum": "" }, "require": { @@ -6230,7 +6225,7 @@ "description": "Provides a way to profile code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/stopwatch/tree/v6.4.8" + "source": "https://github.com/symfony/stopwatch/tree/v6.4.19" }, "funding": [ { @@ -6246,20 +6241,20 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:49:08+00:00" + "time": "2025-02-21T10:06:30+00:00" }, { "name": "symfony/var-dumper", - "version": "v6.4.10", + "version": "v6.4.18", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "a71cc3374f5fb9759da1961d28c452373b343dd4" + "reference": "4ad10cf8b020e77ba665305bb7804389884b4837" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/a71cc3374f5fb9759da1961d28c452373b343dd4", - "reference": "a71cc3374f5fb9759da1961d28c452373b343dd4", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/4ad10cf8b020e77ba665305bb7804389884b4837", + "reference": "4ad10cf8b020e77ba665305bb7804389884b4837", "shasum": "" }, "require": { @@ -6315,7 +6310,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v6.4.10" + "source": "https://github.com/symfony/var-dumper/tree/v6.4.18" }, "funding": [ { @@ -6331,7 +6326,7 @@ "type": "tidelift" } ], - "time": "2024-07-26T12:30:32+00:00" + "time": "2025-01-17T11:26:11+00:00" }, { "name": "ta-tikoma/phpunit-architecture-test", @@ -6444,16 +6439,16 @@ }, { "name": "vimeo/psalm", - "version": "5.25.0", + "version": "5.26.1", "source": { "type": "git", "url": "https://github.com/vimeo/psalm.git", - "reference": "01a8eb06b9e9cc6cfb6a320bf9fb14331919d505" + "reference": "d747f6500b38ac4f7dfc5edbcae6e4b637d7add0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vimeo/psalm/zipball/01a8eb06b9e9cc6cfb6a320bf9fb14331919d505", - "reference": "01a8eb06b9e9cc6cfb6a320bf9fb14331919d505", + "url": "https://api.github.com/repos/vimeo/psalm/zipball/d747f6500b38ac4f7dfc5edbcae6e4b637d7add0", + "reference": "d747f6500b38ac4f7dfc5edbcae6e4b637d7add0", "shasum": "" }, "require": { @@ -6474,7 +6469,7 @@ "felixfbecker/language-server-protocol": "^1.5.2", "fidry/cpu-core-counter": "^0.4.1 || ^0.5.1 || ^1.0.0", "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0", - "nikic/php-parser": "^4.16", + "nikic/php-parser": "^4.17", "php": "^7.4 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0", "sebastian/diff": "^4.0 || ^5.0 || ^6.0", "spatie/array-to-xml": "^2.17.0 || ^3.0", @@ -6517,11 +6512,11 @@ "type": "project", "extra": { "branch-alias": { - "dev-master": "5.x-dev", - "dev-4.x": "4.x-dev", - "dev-3.x": "3.x-dev", + "dev-1.x": "1.x-dev", "dev-2.x": "2.x-dev", - "dev-1.x": "1.x-dev" + "dev-3.x": "3.x-dev", + "dev-4.x": "4.x-dev", + "dev-master": "5.x-dev" } }, "autoload": { @@ -6550,7 +6545,7 @@ "issues": "https://github.com/vimeo/psalm/issues", "source": "https://github.com/vimeo/psalm" }, - "time": "2024-06-16T15:08:35+00:00" + "time": "2024-09-08T18:53:08+00:00" }, { "name": "wayofdev/cs-fixer-config", @@ -6693,13 +6688,13 @@ ], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { "php": ">=8.1" }, - "platform-dev": [], + "platform-dev": {}, "platform-overrides": { "php": "8.1.27" }, From 29afe3165d5a6a57fad35a21b7cac0edce369648 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sat, 5 Apr 2025 12:25:06 +0400 Subject: [PATCH 02/30] chore: init context.yaml and prompts.yaml --- context.yaml | 4 ++++ resources/prompts.yaml | 1 + 2 files changed, 5 insertions(+) create mode 100644 context.yaml create mode 100644 resources/prompts.yaml diff --git a/context.yaml b/context.yaml new file mode 100644 index 0000000..05bfc1a --- /dev/null +++ b/context.yaml @@ -0,0 +1,4 @@ +$schema: 'https://raw.githubusercontent.com/context-hub/generator/refs/heads/main/json-schema.json' + +import: + - path: resources/prompts.yaml diff --git a/resources/prompts.yaml b/resources/prompts.yaml new file mode 100644 index 0000000..a527384 --- /dev/null +++ b/resources/prompts.yaml @@ -0,0 +1 @@ +$schema: 'https://raw.githubusercontent.com/context-hub/generator/refs/heads/main/json-schema.json' From fd3d0f18081d7b22efb813d3951ccd574a78b951 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sat, 5 Apr 2025 14:16:24 +0400 Subject: [PATCH 03/30] docs: add prompt for generating class docs Cover classes: SoftwareCollection, Software, TaskManager --- context.yaml | 16 +++++++ resources/prompts.yaml | 3 ++ resources/prompts/cover-class-by-docs.yaml | 44 ++++++++++++++++++ src/Module/Common/Config/Embed/Software.php | 38 +++++++++++---- src/Module/Downloader/SoftwareCollection.php | 26 +++++++++-- src/Module/Downloader/TaskManager.php | 49 +++++++++++++++++++- 6 files changed, 162 insertions(+), 14 deletions(-) create mode 100644 resources/prompts/cover-class-by-docs.yaml diff --git a/context.yaml b/context.yaml index 05bfc1a..1e843e1 100644 --- a/context.yaml +++ b/context.yaml @@ -2,3 +2,19 @@ $schema: 'https://raw.githubusercontent.com/context-hub/generator/refs/heads/mai import: - path: resources/prompts.yaml + +documents: + # Project structure overview + - description: 'Project structure overview' + outputPath: project-structure.md + overwrite: true + sources: + - type: text + content: | + The PSR-4 is used in the project. + - type: tree + sourcePaths: ['src'] + showCharCount: true + showSize: true + - type: file + sourcePaths: ['README.md'] diff --git a/resources/prompts.yaml b/resources/prompts.yaml index a527384..f5beecd 100644 --- a/resources/prompts.yaml +++ b/resources/prompts.yaml @@ -1 +1,4 @@ $schema: 'https://raw.githubusercontent.com/context-hub/generator/refs/heads/main/json-schema.json' + +import: + - path: ./prompts/cover-class-by-docs.yaml diff --git a/resources/prompts/cover-class-by-docs.yaml b/resources/prompts/cover-class-by-docs.yaml new file mode 100644 index 0000000..a7bfe87 --- /dev/null +++ b/resources/prompts/cover-class-by-docs.yaml @@ -0,0 +1,44 @@ +$schema: 'https://raw.githubusercontent.com/context-hub/generator/refs/heads/main/json-schema.json' + +prompts: + - id: cover-class-by-docs + description: Generate documentation for a specific class in the project + schema: + properties: + class-name: + description: Class name to cover with docs + required: + - class-name + + messages: + - role: user + content: | + Open {{class-name}} class. + + Cover it with comments or improve existing ones using following instructions: + - Use passive forms instead of we or you. + - Separate title and content with empty line. + - Use @link annotation if it's necessary to write some URL. + - Use @see annotation if it's necessary to reference to a class or method. + - Keep only essential and non-obvious documentation. + - Don't use @inheritDoc annotations. + - Keep docs clean + - if the method has the same signature in the parent interface, don't comment the implementation if there is no additional things. + - remove trailing spaces + - Use generic annotations if possible. + - Use inline annotations like {@see ClassName} if it needed to mention in a text block. + - Add class or method usage example using markdown like. + ```php + // Comment for the following code + some code + ``` + - Inner comments order: title, description, example, template annotations, params and return annotations, references and internal/deprecated annotations. + - Property comments rules: If it possible, write inline comment without title like `/** @var non-empty-string $name User name */`. + - Multiline comments rules: if a comment starts with an annotation, the second line should start with whitespaces aligned with the annotation length: + ```php + /** + * @var non-empty-string $name Comment on the first line + * comment on the second line... + * comment on the third line + */ + ``` diff --git a/src/Module/Common/Config/Embed/Software.php b/src/Module/Common/Config/Embed/Software.php index 42f336c..e2fe547 100644 --- a/src/Module/Common/Config/Embed/Software.php +++ b/src/Module/Common/Config/Embed/Software.php @@ -8,6 +8,11 @@ use Internal\DLoad\Module\Common\Internal\Attribute\XPathEmbedList; /** + * Software configuration entity. + * + * Represents a software package that can be downloaded through the system. + * Contains all necessary metadata for proper identification and retrieval. + * * @psalm-import-type RepositoryArray from Repository * @psalm-import-type FileArray from File * @psalm-type SoftwareArray = array{ @@ -21,35 +26,48 @@ */ final class Software { - /** - * @var non-empty-string - */ + /** @var non-empty-string $name Software package name */ #[XPath('@name')] public string $name; /** - * If {@see null}, the name in lower case will be used. - * @var non-empty-string|null + * @var non-empty-string|null $alias CLI command alias + * If null, the name in lowercase will be used as the identifier. */ #[XPath('@alias')] public ?string $alias = null; + /** @var string|null $homepage Official software homepage URL */ #[XPath('@homepage')] public ?string $homepage = null; + /** @var string $description Short description of the software */ #[XPath('@description')] public string $description = ''; - /** @var File */ + /** @var Repository[] $repositories List of repositories where the software can be found */ #[XPathEmbedList('repository', Repository::class)] public array $repositories = []; - /** @var File */ + /** @var File[] $files List of files to be extracted after download */ #[XPathEmbedList('file', File::class)] public array $files = []; /** - * @param SoftwareArray $softwareArray + * Creates a Software instance from array configuration. + * + * ```php + * $software = Software::fromArray([ + * 'name' => 'RoadRunner', + * 'alias' => 'rr', + * 'description' => 'High performance PHP application server', + * 'repositories' => [ + * ['type' => 'github', 'uri' => 'roadrunner-server/roadrunner'] + * ] + * ]); + * ``` + * + * @param SoftwareArray $softwareArray Configuration array */ public static function fromArray(mixed $softwareArray): self { @@ -71,7 +89,9 @@ public static function fromArray(mixed $softwareArray): self } /** - * @return non-empty-string + * Returns the software identifier. + * + * @return non-empty-string Software identifier (alias or lowercase name) */ public function getId(): string { diff --git a/src/Module/Downloader/SoftwareCollection.php b/src/Module/Downloader/SoftwareCollection.php index bce7f0f..f27c50c 100644 --- a/src/Module/Downloader/SoftwareCollection.php +++ b/src/Module/Downloader/SoftwareCollection.php @@ -10,6 +10,8 @@ use IteratorAggregate; /** + * Collection of software package configurations. + * * @implements IteratorAggregate */ final class SoftwareCollection implements \IteratorAggregate, \Countable @@ -17,6 +19,9 @@ final class SoftwareCollection implements \IteratorAggregate, \Countable /** @var array */ private array $registry = []; + /** + * Creates a collection from registry and optionally loads default entries. + */ public function __construct(CustomSoftwareRegistry $softwareRegistry) { foreach ($softwareRegistry->software as $software) { @@ -26,6 +31,19 @@ public function __construct(CustomSoftwareRegistry $softwareRegistry) $softwareRegistry->overwrite or $this->loadDefaultRegistry(); } + /** + * Finds software configuration by name. + * + * ```php + * $software = $collection->findSoftware('rr'); + * if ($software !== null) { + * // Process software configuration + * } + * ``` + * + * @param non-empty-string $name Software name or alias + * @return Software|null Found software configuration or null + */ public function findSoftware(string $name): ?Software { return $this->registry[$name] ?? null; @@ -39,14 +57,16 @@ public function getIterator(): \Traversable yield from $this->registry; } - /** - * @return int<0, max> - */ public function count(): int { return \count($this->registry); } + /** + * Loads default software registry from embedded JSON file. + * + * @see Software::fromArray() For parsing logic + */ private function loadDefaultRegistry(): void { $json = \json_decode( diff --git a/src/Module/Downloader/TaskManager.php b/src/Module/Downloader/TaskManager.php index 687e7ec..1fc8bc7 100644 --- a/src/Module/Downloader/TaskManager.php +++ b/src/Module/Downloader/TaskManager.php @@ -6,20 +6,59 @@ use Internal\DLoad\Service\Logger; +/** + * Task Manager Service + * + * Manages async tasks with Fiber-based coroutines. Tasks are executed concurrently + * and can be suspended/resumed. + * + * ```php + * // Create a task that can be suspended and resumed + * $task = function () { + * // First part of execution + * \Fiber::suspend(); + * // Execution continues after resume + * }; + * + * $taskManager = new TaskManager($logger); + * $taskManager->addTask($task); + * + * // Await for all tasks to finish + * $taskManager->await(); + * ``` + */ final class TaskManager { - /** @var array<\Fiber> */ + /** @var array<\Fiber> Active fiber tasks */ private array $tasks = []; + /** + * Constructor + * + * @param Logger $logger Error logging service + */ public function __construct( - private Logger $logger, + private readonly Logger $logger, ) {} + /** + * Adds a new task to the execution queue + * + * @param \Closure $callback Task implementation + */ public function addTask(\Closure $callback): void { $this->tasks[] = new \Fiber($callback); } + /** + * Creates a task processor generator + * + * Returns a generator that manages the execution cycle of all registered tasks. + * Each yield represents a step in task execution. + * + * @return \Generator Task processing generator + */ public function getProcessor(): \Generator { start: @@ -51,6 +90,12 @@ public function getProcessor(): \Generator goto start; } + /** + * Executes all tasks until completion + * + * Runs the processor generator until all tasks are complete. + * Blocks execution until all tasks are finished. + */ public function await(): void { $processor = $this->getProcessor(); From 63b724b1ee2045dbc36e337507c1c052ec60cfb5 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sat, 5 Apr 2025 15:06:09 +0400 Subject: [PATCH 04/30] docs: cover Services with comments --- resources/prompts/cover-class-by-docs.yaml | 6 +- src/Service/Container.php | 49 +++++++++---- src/Service/Destroyable.php | 26 ++++++- src/Service/Factoriable.php | 20 ++++- src/Service/Logger.php | 85 ++++++++++++++++++++-- 5 files changed, 159 insertions(+), 27 deletions(-) diff --git a/resources/prompts/cover-class-by-docs.yaml b/resources/prompts/cover-class-by-docs.yaml index a7bfe87..37b20e7 100644 --- a/resources/prompts/cover-class-by-docs.yaml +++ b/resources/prompts/cover-class-by-docs.yaml @@ -13,9 +13,11 @@ prompts: messages: - role: user content: | - Open {{class-name}} class. + Read {{class-name}} class(es) or interface(s). - Cover it with comments or improve existing ones using following instructions: + Cover it with comments using following instructions: + - Don't break signatures. You can only add or improve comments. + - Don't remove existing @internal, @link, @see, @api, or @deprecated annotations. - Use passive forms instead of we or you. - Separate title and content with empty line. - Use @link annotation if it's necessary to write some URL. diff --git a/src/Service/Container.php b/src/Service/Container.php index 7a27efb..03e01a5 100644 --- a/src/Service/Container.php +++ b/src/Service/Container.php @@ -5,53 +5,70 @@ namespace Internal\DLoad\Service; /** - * Application container. + * Application dependency injection container. + * + * Manages service instances throughout the application lifecycle. + * + * ```php + * // Retrieving a service instance + * $downloader = $container->get(Downloader::class); + * ``` * * @internal */ -interface Container +interface Container extends Destroyable { /** + * Retrieves a service from the container. + * + * If the service is requested for the first time, it will be instantiated and persisted for future requests. + * * @template T - * @param class-string $id - * @param array $arguments Will be used if the object is created for the first time. - * @return T + * @param class-string $id Service identifier + * @param array $arguments Constructor arguments used only on first instantiation + * @return T The requested service instance * * @psalm-suppress MoreSpecificImplementedParamType, InvalidReturnType */ public function get(string $id, array $arguments = []): object; /** - * @param class-string $id + * Checks if the service is registered in the container. + * + * It means that the container has a cached service instance or a binding. + * + * @param class-string $id Service identifier + * @return bool Whether the service is available * * @psalm-suppress MoreSpecificImplementedParamType */ public function has(string $id): bool; /** + * Registers an existing service instance in the container. + * * @template T of object - * @param T $service - * @param class-string|null $id + * @param T $service Service instance to register + * @param class-string|null $id Optional service identifier (defaults to object's class) */ public function set(object $service, ?string $id = null): void; /** - * Create an object of the specified class without caching. + * Creates a new instance without storing it in the container. * * @template T - * @param class-string $class - * @return T + * @param class-string $class Class to instantiate + * @param array $arguments Constructor arguments + * @return T Newly created instance */ public function make(string $class, array $arguments = []): object; /** - * Declare a factory or predefined arguments for the specified class. + * Configures how a service should be instantiated. * * @template T of object - * @param class-string $id - * @param array|\Closure(Container): T $binding + * @param class-string $id Service identifier + * @param array|\Closure(Container): T $binding Factory function or constructor arguments */ public function bind(string $id, \Closure|array|null $binding = null): void; - - public function destroy(): void; } diff --git a/src/Service/Destroyable.php b/src/Service/Destroyable.php index 4e70e34..691b71d 100644 --- a/src/Service/Destroyable.php +++ b/src/Service/Destroyable.php @@ -5,11 +5,33 @@ namespace Internal\DLoad\Service; /** - * Should be used to destroy objects and free resources. + * Interface for services that need cleanup on destruction. + * + * Implementing classes should release resources properly when no longer needed. + * + * ```php + * class FileHandler implements Destroyable + * { + * private $handle; + * + * public function destroy(): void + * { + * if ($this->handle) { + * fclose($this->handle); + * $this->handle = null; + * } + * } + * } + * ``` * * @internal */ interface Destroyable { + /** + * Performs cleanup before object destruction. + * + * Called by the Container when shutting down or releasing services. + */ public function destroy(): void; -} +} \ No newline at end of file diff --git a/src/Service/Factoriable.php b/src/Service/Factoriable.php index 2b6075b..2e4d4b0 100644 --- a/src/Service/Factoriable.php +++ b/src/Service/Factoriable.php @@ -5,11 +5,27 @@ namespace Internal\DLoad\Service; /** - * Class creates new instances of itself. + * Interface for classes that can create instances of themselves. + * + * Implementing classes should provide a static factory method for instantiation. + * + * ```php + * class Config implements Factoriable + * { + * private function __construct( + * private string $path + * ) {} + * + * public static function create(string $path): self + * { + * return new self($path); + * } + * } + * ``` * * @method static create * Method creates new instance of the class. May contain any injectable parameters. * * @internal */ -interface Factoriable {} +interface Factoriable {} \ No newline at end of file diff --git a/src/Service/Logger.php b/src/Service/Logger.php index 6dd0a79..9c0add7 100644 --- a/src/Service/Logger.php +++ b/src/Service/Logger.php @@ -7,16 +7,29 @@ use Symfony\Component\Console\Output\OutputInterface; /** - * Console color logger + * Console color logger for terminal output. + * + * Provides formatted console output with color support and verbosity control. + * + * ```php + * $logger = new Logger($output); + * $logger->info("Processing %s files", $count); + * $logger->error("Failed to download %s", $url); + * ``` * * @internal */ final class Logger { + /** @var bool Whether debug messages should be displayed */ private readonly bool $debug; + /** @var bool Whether verbose messages should be displayed */ private readonly bool $verbose; + /** + * @param OutputInterface|null $output Console output interface + */ public function __construct( private readonly ?OutputInterface $output = null, ) { @@ -24,31 +37,68 @@ public function __construct( $this->verbose = $output?->isVerbose() ?? false; } + /** + * Outputs a message with a newline. + * + * @param string $message Text to output + */ public function print(string $message): void { $this->echo($message . "\n", false); } + /** + * Displays a highlighted status message with sender identification. + * + * @param non-empty-string $sender Source component name + * @param non-empty-string $message Message format string + * @param string|int|float|bool ...$values Values to format the message + */ public function status(string $sender, string $message, string|int|float|bool ...$values): void { - $this->echo("\033[47;1;30m " . $sender . " \033[0m " . \sprintf($message, ...$values) . "\n\n", false); + $this->echo("\033[47;1;30m " . $sender . " \033[0m " . \sprintf($message, ...self::values($values)) . "\n\n", false); } + /** + * Outputs a success/confirmation message in green. + * + * @param string $message Message format string + * @param string|int|float|bool ...$values Format values + */ public function info(string $message, string|int|float|bool ...$values): void { - $this->echo("\033[32m" . \sprintf($message, ...$values) . "\033[0m\n", false); + $this->echo("\033[32m" . \sprintf($message, ...self::values($values)) . "\033[0m\n", false); } + /** + * Outputs a debug message in blue (only when debug mode is enabled). + * + * @param string $message Message format string + * @param string|int|float|bool ...$values Format values + */ public function debug(string $message, string|int|float|bool ...$values): void { - $this->echo("\033[34m" . \sprintf($message, ...$values) . "\033[0m\n"); + $this->echo("\033[34m" . \sprintf($message, ...self::values($values)) . "\033[0m\n"); } + /** + * Outputs an error message in red. + * + * @param string $message Message format string + * @param string|int|float|bool ...$values Format values + */ public function error(string $message, string|int|float|bool ...$values): void { - $this->echo("\033[31m" . \sprintf($message, ...$values) . "\033[0m\n"); + $this->echo("\033[31m" . \sprintf($message, ...self::values($values)) . "\033[0m\n"); } + /** + * Formats and outputs exception details. + * + * @param \Throwable $e Exception to display + * @param string|null $header Optional header text + * @param bool $important Whether to show regardless of debug setting + */ public function exception(\Throwable $e, ?string $header = null, bool $important = false): void { $r = "----------------------\n"; @@ -69,6 +119,31 @@ public function exception(\Throwable $e, ?string $header = null, bool $important $this->echo($r, !$important); } + /** + * Converts values to string representations. + * + * @param array<\Stringable|string|int|float|bool> $values Values to convert + * @return array Converted values + */ + private static function values(array $values): array + { + $result = []; + foreach ($values as $k => $value) { + $result[$k] = match (true) { + \is_bool($value) => $value ? 'TRUE' : 'FALSE', + default => (string) $value, + }; + } + + return $result; + } + + /** + * Internal method to output text based on verbosity settings. + * + * @param string $message Text to output + * @param bool $debug Whether message should be shown only in debug mode + */ private function echo(string $message, bool $debug = true): void { if ($debug && !$this->debug) { From c262d1e02f333e958f090b0bd4741ffe83e66e95 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sat, 5 Apr 2025 15:24:28 +0400 Subject: [PATCH 05/30] docs: cover Console commands with comments --- resources/prompts/cover-class-by-docs.yaml | 11 ++-- src/Command/Base.php | 41 ++++++++++++++- src/Command/Get.php | 59 +++++++++++++++++++++- src/Command/ListSoftware.php | 30 +++++++++++ 4 files changed, 135 insertions(+), 6 deletions(-) diff --git a/resources/prompts/cover-class-by-docs.yaml b/resources/prompts/cover-class-by-docs.yaml index 37b20e7..3e18868 100644 --- a/resources/prompts/cover-class-by-docs.yaml +++ b/resources/prompts/cover-class-by-docs.yaml @@ -25,15 +25,20 @@ prompts: - Keep only essential and non-obvious documentation. - Don't use @inheritDoc annotations. - Keep docs clean - - if the method has the same signature in the parent interface, don't comment the implementation if there is no additional things. - - remove trailing spaces + - if the method has the same signature in the parent, don't comment the implementation if there is no additional things. + - remove trailing spaces even in empty comment lines - Use generic annotations if possible. - Use inline annotations like {@see ClassName} if it needed to mention in a text block. - - Add class or method usage example using markdown like. + - Add class or method usage example using markdown like: ```php // Comment for the following code some code ``` + If commenting a console command, feel fre to provide a usage example in the form of a command line: + ```bash + # Comment for the following command + some command + ``` - Inner comments order: title, description, example, template annotations, params and return annotations, references and internal/deprecated annotations. - Property comments rules: If it possible, write inline comment without title like `/** @var non-empty-string $name User name */`. - Multiline comments rules: if a comment starts with an annotation, the second line should start with whitespaces aligned with the annotation length: diff --git a/src/Command/Base.php b/src/Command/Base.php index 57fd5ee..b506acc 100644 --- a/src/Command/Base.php +++ b/src/Command/Base.php @@ -15,20 +15,55 @@ use Symfony\Component\Console\Style\SymfonyStyle; /** + * Base abstract class for all DLoad commands. + * + * Provides common functionality for command initialization, container setup, + * and configuration handling. + * + * ```php + * // Extend to create a custom command + * final class CustomCommand extends Base + * { + * protected function execute(InputInterface $input, OutputInterface $output): int + * { + * parent::execute($input, $output); + * // Command implementation + * return Command::SUCCESS; + * } + * } + * ``` + * * @internal */ abstract class Base extends Command { + /** @var Logger Service for logging command execution */ protected Logger $logger; + /** @var Container IoC container with services */ protected Container $container; + /** + * Configures command options. + * + * Adds option for specifying configuration file location. + */ public function configure(): void { parent::configure(); $this->addOption('config', null, InputOption::VALUE_OPTIONAL, 'Path to the configuration file'); } + /** + * Initializes the command execution environment. + * + * Sets up logger, container, and registers input/output in the container. + * + * @param InputInterface $input Command input + * @param OutputInterface $output Command output + * + * @return int Command success code + */ protected function execute( InputInterface $input, OutputInterface $output, @@ -50,6 +85,10 @@ protected function execute( } /** + * Resolves configuration file path from input or default location. + * + * @param InputInterface $input Command input + * * @return non-empty-string|null Path to the configuration file */ private function getConfigFile(InputInterface $input): ?string @@ -69,4 +108,4 @@ private function getConfigFile(InputInterface $input): ?string return null; } -} +} \ No newline at end of file diff --git a/src/Command/Get.php b/src/Command/Get.php index 5f522db..3f436fa 100644 --- a/src/Command/Get.php +++ b/src/Command/Get.php @@ -18,6 +18,32 @@ use Symfony\Component\Console\Output\OutputInterface; /** + * Fetches software packages based on command arguments or configuration. + * + * Downloads software binaries from configured repositories with proper + * system/architecture detection. Can work with both CLI arguments and + * configuration file definitions. + * + * ```php + * // Download software programmatically + * $command = new Get(); + * $command->run(new ArrayInput(['software' => 'rr']), new ConsoleOutput()); + * ``` + * + * ```bash + * # Download single software + * ./vendor/bin/dload get rr + * + * # Download specific version of software + * ./vendor/bin/dload get rr --stability=beta + * + * # Download multiple software packages + * ./vendor/bin/dload get rr dolt temporal + * + * # Download software defined in config file + * ./vendor/bin/dload get --config=./dload.xml + * ``` + * * @internal */ #[AsCommand( @@ -26,18 +52,39 @@ )] final class Get extends Base { + /** @var string Argument name for software identifiers */ private const ARG_SOFTWARE = 'software'; + /** + * Configures command arguments and options. + */ public function configure(): void { parent::configure(); - $this->addArgument(self::ARG_SOFTWARE, InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'Software name, e.g. "rr", "dolt", "temporal" etc.'); + $this->addArgument( + self::ARG_SOFTWARE, + InputArgument::OPTIONAL | InputArgument::IS_ARRAY, + 'Software name, e.g. "rr", "dolt", "temporal" etc.', + ); $this->addOption('path', null, InputOption::VALUE_OPTIONAL, 'Path to store the binary, e.g. "./bin"', "."); $this->addOption('arch', null, InputOption::VALUE_OPTIONAL, 'Architecture, e.g. "amd64", "arm64" etc.'); $this->addOption('os', null, InputOption::VALUE_OPTIONAL, 'Operating system, e.g. "linux", "darwin" etc.'); $this->addOption('stability', null, InputOption::VALUE_OPTIONAL, 'Stability, e.g. "stable", "beta" etc.'); } + /** + * Executes the command to download specified software. + * + * Determines system parameters, resolves download actions based on input, + * and runs the download tasks. + * + * @param InputInterface $input Command input + * @param OutputInterface $output Command output + * + * @return int Command result code + * + * @throws \RuntimeException When no software is specified to download + */ protected function execute(InputInterface $input, OutputInterface $output): int { parent::execute($input, $output); @@ -65,7 +112,15 @@ protected function execute(InputInterface $input, OutputInterface $output): int } /** - * @return list + * Resolves download actions from input arguments or config. + * + * Uses explicitly specified software names from CLI arguments if present, + * otherwise falls back to configuration file definitions. + * + * @param InputInterface $input Command input + * @param Actions $actionsConfig Available actions from config + * + * @return list List of download configurations to process */ private function getDownloadActions(InputInterface $input, Actions $actionsConfig): array { diff --git a/src/Command/ListSoftware.php b/src/Command/ListSoftware.php index f2a211f..27aa665 100644 --- a/src/Command/ListSoftware.php +++ b/src/Command/ListSoftware.php @@ -12,6 +12,25 @@ use Symfony\Component\Console\Output\OutputInterface; /** + * Displays a list of available software packages. + * + * Shows all registered software packages with their IDs, names, + * repository information, and descriptions. + * + * ```php + * // List software programmatically + * $command = new ListSoftware(); + * $command->run(new ArrayInput([]), new ConsoleOutput()); + * ``` + * + * ```bash + * # List all available software packages + * ./vendor/bin/dload software + * + * # List software with custom configuration + * ./vendor/bin/dload software --config=./custom-dload.xml + * ``` + * * @internal */ #[AsCommand( @@ -20,6 +39,17 @@ )] final class ListSoftware extends Base { + /** + * Lists all available software packages in a formatted output. + * + * Displays the software ID, name, homepage (if available), + * repository information, and description for each registered software. + * + * @param InputInterface $input Command input + * @param OutputInterface $output Command output + * + * @return int Command result code + */ protected function execute( InputInterface $input, OutputInterface $output, From 31c452fb43490cab7bb4e61e8cd3ef5bbd965ad4 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sat, 5 Apr 2025 15:54:22 +0400 Subject: [PATCH 06/30] docs: cover Archive module with comments --- resources/prompts/cover-class-by-docs.yaml | 1 + src/Module/Archive/Archive.php | 24 ++++++++- src/Module/Archive/ArchiveFactory.php | 51 ++++++++++++++++--- .../Archive/Exception/ArchiveException.php | 7 +++ src/Module/Archive/Internal/Archive.php | 32 +++++++++++- src/Module/Archive/Internal/PharArchive.php | 21 ++++++++ .../Archive/Internal/PharAwareArchive.php | 43 +++++++++++++++- .../Archive/Internal/TarPharArchive.php | 20 ++++++++ .../Archive/Internal/ZipPharArchive.php | 26 ++++++++++ 9 files changed, 214 insertions(+), 11 deletions(-) create mode 100644 src/Module/Archive/Exception/ArchiveException.php diff --git a/resources/prompts/cover-class-by-docs.yaml b/resources/prompts/cover-class-by-docs.yaml index 3e18868..44fb782 100644 --- a/resources/prompts/cover-class-by-docs.yaml +++ b/resources/prompts/cover-class-by-docs.yaml @@ -39,6 +39,7 @@ prompts: # Comment for the following command some command ``` + Don't skip internal classes. - Inner comments order: title, description, example, template annotations, params and return annotations, references and internal/deprecated annotations. - Property comments rules: If it possible, write inline comment without title like `/** @var non-empty-string $name User name */`. - Multiline comments rules: if a comment starts with an annotation, the second line should start with whitespaces aligned with the annotation length: diff --git a/src/Module/Archive/Archive.php b/src/Module/Archive/Archive.php index 0965fb8..6d0a078 100644 --- a/src/Module/Archive/Archive.php +++ b/src/Module/Archive/Archive.php @@ -4,13 +4,35 @@ namespace Internal\DLoad\Module\Archive; +use Internal\DLoad\Module\Archive\Exception\ArchiveException; + +/** + * Archive extraction interface + * + * Provides methods to extract contents from archive files. + */ interface Archive { /** - * Iterate archive files. If a {@see \SplFileInfo} is backed into the generator, the file will be + * Iterate through archive files and extract them + * + * Iterates through all files in the archive and yields {@see \SplFileInfo} objects. + * If a {@see \SplFileInfo} is yielded back into the generator, the file will be * extracted to the given location. * + * ```php + * // Extract only specific files + * $archive = $factory->create(new \SplFileInfo('archive.zip')); + * foreach ($archive->extract() as $path => $fileInfo) { + * if (str_ends_with($path, '.php')) { + * // Extract PHP files to a specific directory + * yield new \SplFileInfo('/path/to/extract/' . basename($path)); + * } + * } + * ``` + * * @return \Generator + * @throws ArchiveException */ public function extract(): \Generator; } diff --git a/src/Module/Archive/ArchiveFactory.php b/src/Module/Archive/ArchiveFactory.php index d645b2c..c02bb61 100644 --- a/src/Module/Archive/ArchiveFactory.php +++ b/src/Module/Archive/ArchiveFactory.php @@ -10,20 +10,30 @@ use Internal\DLoad\Module\Archive\Internal\ZipPharArchive; /** + * Factory for creating archive handlers + * + * Creates appropriate archive handlers based on file extension or custom matchers. + * + * ```php + * $factory = new ArchiveFactory(); + * $archive = $factory->create(new \SplFileInfo('archive.zip')); + * foreach ($archive->extract() as $path => $fileInfo) { + * // Process extracted files + * } + * ``` + * * @psalm-type ArchiveMatcher = \Closure(\SplFileInfo): ?Archive */ final class ArchiveFactory { - /** @var list */ + /** @var list List of supported file extensions */ private array $extensions = []; - /** - * @var array - */ + /** @var array List of archive type matchers */ private array $matchers = []; /** - * FactoryTrait constructor. + * Creates factory with default archive type handlers */ public function __construct() { @@ -31,6 +41,20 @@ public function __construct() } /** + * Extends factory with custom archive matcher + * + * Adds a custom matcher to the beginning of the matchers list. + * + * ```php + * $factory->extend( + * fn(\SplFileInfo $file) => str_ends_with($file->getFilename(), '.rar') + * ? new RarArchive($file) + * : null, + * ['rar'] + * ); + * ``` + * + * @param \Closure $matcher Function that creates archive handler or returns null * @param list $extensions List of supported extensions */ public function extend(\Closure $matcher, array $extensions = []): void @@ -39,6 +63,13 @@ public function extend(\Closure $matcher, array $extensions = []): void $this->extensions = \array_unique(\array_merge($this->extensions, $extensions)); } + /** + * Creates archive handler for the given file + * + * @param \SplFileInfo $file Archive file + * @return Archive Archive handler + * @throws \InvalidArgumentException When no suitable archive handler found + */ public function create(\SplFileInfo $file): Archive { $errors = []; @@ -60,6 +91,8 @@ public function create(\SplFileInfo $file): Archive } /** + * Returns list of supported archive extensions + * * @return list */ public function getSupportedExtensions(): array @@ -67,6 +100,9 @@ public function getSupportedExtensions(): array return $this->extensions; } + /** + * Registers default archive type handlers + */ private function bootDefaultMatchers(): void { $this->extend($this->matcher( @@ -86,9 +122,10 @@ private function bootDefaultMatchers(): void } /** - * @param string $extension - * @param ArchiveMatcher $then + * Creates a matcher function for files with specific extension * + * @param string $extension File extension to match + * @param ArchiveMatcher $then Function to create archive handler * @return ArchiveMatcher */ private function matcher(string $extension, \Closure $then): \Closure diff --git a/src/Module/Archive/Exception/ArchiveException.php b/src/Module/Archive/Exception/ArchiveException.php new file mode 100644 index 0000000..9669c76 --- /dev/null +++ b/src/Module/Archive/Exception/ArchiveException.php @@ -0,0 +1,7 @@ +getPathname() => $file; + * // Extract file if requested + * } + * } + * } + * ``` + * + * @internal + */ abstract class Archive implements ArchiveInterface { /** - * @param \SplFileInfo $archive + * Creates archive handler and validates the archive file + * + * @param \SplFileInfo $archive Archive file + * @throws \InvalidArgumentException When archive is invalid */ public function __construct(\SplFileInfo $archive) { @@ -17,7 +42,10 @@ public function __construct(\SplFileInfo $archive) } /** - * @param \SplFileInfo $archive + * Validates that archive file exists and is readable + * + * @param \SplFileInfo $archive Archive file to validate + * @throws \InvalidArgumentException When archive is invalid */ private function assertArchiveValid(\SplFileInfo $archive): void { diff --git a/src/Module/Archive/Internal/PharArchive.php b/src/Module/Archive/Internal/PharArchive.php index b02d287..b386ca3 100644 --- a/src/Module/Archive/Internal/PharArchive.php +++ b/src/Module/Archive/Internal/PharArchive.php @@ -4,8 +4,29 @@ namespace Internal\DLoad\Module\Archive\Internal; +/** + * Archive handler for standard PHAR archives + * + * ```php + * // Creating and using a PHAR archive handler + * $archive = new PharArchive(new \SplFileInfo('package.phar')); + * foreach ($archive->extract() as $path => $fileInfo) { + * if (str_ends_with($path, '.php')) { + * yield new \SplFileInfo('/tmp/extract/' . basename($path)); + * } + * } + * ``` + * + * @internal + */ final class PharArchive extends PharAwareArchive { + /** + * Opens PHAR archive for reading + * + * @param \SplFileInfo $file Archive file + * @return \PharData + */ protected function open(\SplFileInfo $file): \PharData { return new \PharData($file->getPathname()); diff --git a/src/Module/Archive/Internal/PharAwareArchive.php b/src/Module/Archive/Internal/PharAwareArchive.php index 9ccfd66..04e9c25 100644 --- a/src/Module/Archive/Internal/PharAwareArchive.php +++ b/src/Module/Archive/Internal/PharAwareArchive.php @@ -4,10 +4,45 @@ namespace Internal\DLoad\Module\Archive\Internal; +use Internal\DLoad\Module\Archive\Exception\ArchiveException; + +/** + * Base class for PharData-based archive handlers + * + * Provides common functionality for archives handled by PHP's PharData. + * + * ```php + * // Example implementation of a custom PharData-based archive + * class CustomPharArchive extends PharAwareArchive + * { + * protected function open(\SplFileInfo $file): \PharData + * { + * // Custom opening logic + * return new \PharData($file->getPathname(), $customFlags); + * } + * } + * + * // Usage + * $archive = new CustomPharArchive(new \SplFileInfo('archive.custom')); + * foreach ($archive->extract() as $path => $fileInfo) { + * // Extract to destination + * yield new \SplFileInfo('/path/to/extract/' . basename($path)); + * } + * ``` + * + * @internal + */ abstract class PharAwareArchive extends Archive { + /** @var \PharData Opened archive instance */ protected \PharData $archive; + /** + * Creates and opens archive + * + * @param \SplFileInfo $archive Archive file + * @throws \LogicException When archive cannot be opened + */ public function __construct(\SplFileInfo $archive) { parent::__construct($archive); @@ -17,7 +52,7 @@ public function __construct(\SplFileInfo $archive) public function extract(): \Generator { $phar = $this->archive; - $phar->isReadable() or throw new \LogicException( + $phar->isReadable() or throw new ArchiveException( \sprintf('Could not open "%s" for reading.', $this->archive->getPathname()), ); @@ -32,5 +67,11 @@ public function extract(): \Generator } } + /** + * Opens archive with specific format + * + * @param \SplFileInfo $file Archive file + * @return \PharData + */ abstract protected function open(\SplFileInfo $file): \PharData; } diff --git a/src/Module/Archive/Internal/TarPharArchive.php b/src/Module/Archive/Internal/TarPharArchive.php index 98b9e7a..d30c917 100644 --- a/src/Module/Archive/Internal/TarPharArchive.php +++ b/src/Module/Archive/Internal/TarPharArchive.php @@ -4,8 +4,28 @@ namespace Internal\DLoad\Module\Archive\Internal; +/** + * Archive handler for TAR.GZ archives + * + * ```php + * // Extracting from TAR.GZ archive + * $archive = new TarPharArchive(new \SplFileInfo('package.tar.gz')); + * foreach ($archive->extract() as $path => $fileInfo) { + * // Extract all files to a directory + * yield new \SplFileInfo('/tmp/extract/' . basename($path)); + * } + * ``` + * + * @internal + */ final class TarPharArchive extends PharAwareArchive { + /** + * Opens TAR.GZ archive for reading + * + * @param \SplFileInfo $file Archive file + * @return \PharData + */ protected function open(\SplFileInfo $file): \PharData { return new \PharData($file->getPathname()); diff --git a/src/Module/Archive/Internal/ZipPharArchive.php b/src/Module/Archive/Internal/ZipPharArchive.php index 436d2d3..ec96d12 100644 --- a/src/Module/Archive/Internal/ZipPharArchive.php +++ b/src/Module/Archive/Internal/ZipPharArchive.php @@ -4,8 +4,34 @@ namespace Internal\DLoad\Module\Archive\Internal; +/** + * Archive handler for ZIP archives + * + * ```php + * // Extracting specific file types from ZIP archive + * $archive = new ZipPharArchive(new \SplFileInfo('package.zip')); + * $extractDir = '/tmp/extract'; + * + * foreach ($archive->extract() as $path => $fileInfo) { + * // Only extract binary files + * if (pathinfo($path, PATHINFO_EXTENSION) === 'bin') { + * yield new \SplFileInfo($extractDir . '/' . basename($path)); + * } + * } + * ``` + * + * @internal + */ final class ZipPharArchive extends PharAwareArchive { + /** + * Opens ZIP archive for reading + * + * Uses ZIP and GZ formats to properly handle ZIP archives. + * + * @param \SplFileInfo $file Archive file + * @return \PharData + */ protected function open(\SplFileInfo $file): \PharData { $format = \Phar::ZIP | \Phar::GZ; From 19e43211ed27d250b6dbfa1f01328b9c4326ba75 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sat, 5 Apr 2025 16:51:22 +0400 Subject: [PATCH 07/30] docs: cover Container implementation and common enums with docs --- resources/prompts/cover-class-by-docs.yaml | 3 +- src/Bootstrap.php | 3 +- src/Command/Base.php | 20 ++++---- src/DLoad.php | 2 +- .../Archive/Exception/ArchiveException.php | 2 +- src/Module/Common/Architecture.php | 10 ++++ .../Internal/Attribute/ConfigAttribute.php | 2 + src/Module/Common/Internal/Attribute/Env.php | 7 +++ .../Internal/Attribute/InputArgument.php | 7 +++ .../Common/Internal/Attribute/InputOption.php | 7 +++ .../Common/Internal/Attribute/PhpIni.php | 9 +++- .../Common/Internal/Attribute/XPath.php | 8 ++++ .../Internal/Attribute/XPathEmbedList.php | 9 +++- .../Internal/Injection/ConfigLoader.php | 21 +++++++++ .../{Container.php => ObjectContainer.php} | 47 +++++-------------- src/Module/Common/OperatingSystem.php | 10 ++++ src/Module/Common/Stability.php | 11 +++++ src/Service/Container.php | 6 ++- src/Service/Destroyable.php | 2 +- src/Service/Factoriable.php | 2 +- 20 files changed, 132 insertions(+), 56 deletions(-) rename src/Module/Common/Internal/{Container.php => ObjectContainer.php} (74%) diff --git a/resources/prompts/cover-class-by-docs.yaml b/resources/prompts/cover-class-by-docs.yaml index 44fb782..f3946f3 100644 --- a/resources/prompts/cover-class-by-docs.yaml +++ b/resources/prompts/cover-class-by-docs.yaml @@ -22,12 +22,13 @@ prompts: - Separate title and content with empty line. - Use @link annotation if it's necessary to write some URL. - Use @see annotation if it's necessary to reference to a class or method. + - Use extended types like class-string, non-empty-string, non-empty-array, non-empty-list, etc, if it's logically correct. - Keep only essential and non-obvious documentation. - Don't use @inheritDoc annotations. - Keep docs clean - if the method has the same signature in the parent, don't comment the implementation if there is no additional things. - remove trailing spaces even in empty comment lines - - Use generic annotations if possible. + - Use generic annotations if possible. Iven f.e. for int based on the logic. - Use inline annotations like {@see ClassName} if it needed to mention in a text block. - Add class or method usage example using markdown like: ```php diff --git a/src/Bootstrap.php b/src/Bootstrap.php index 9745f6b..511e84d 100644 --- a/src/Bootstrap.php +++ b/src/Bootstrap.php @@ -6,6 +6,7 @@ use Internal\DLoad\Module\Common\Architecture; use Internal\DLoad\Module\Common\Internal\Injection\ConfigLoader; +use Internal\DLoad\Module\Common\Internal\ObjectContainer; use Internal\DLoad\Module\Common\OperatingSystem; use Internal\DLoad\Module\Common\Stability; use Internal\DLoad\Service\Container; @@ -21,7 +22,7 @@ private function __construct( private Container $container, ) {} - public static function init(Container $container = new Module\Common\Internal\Container()): self + public static function init(Container $container = new ObjectContainer()): self { return new self($container); } diff --git a/src/Command/Base.php b/src/Command/Base.php index b506acc..c6c7a54 100644 --- a/src/Command/Base.php +++ b/src/Command/Base.php @@ -16,10 +16,10 @@ /** * Base abstract class for all DLoad commands. - * + * * Provides common functionality for command initialization, container setup, * and configuration handling. - * + * * ```php * // Extend to create a custom command * final class CustomCommand extends Base @@ -32,7 +32,7 @@ * } * } * ``` - * + * * @internal */ abstract class Base extends Command @@ -45,7 +45,7 @@ abstract class Base extends Command /** * Configures command options. - * + * * Adds option for specifying configuration file location. */ public function configure(): void @@ -56,12 +56,12 @@ public function configure(): void /** * Initializes the command execution environment. - * + * * Sets up logger, container, and registers input/output in the container. - * + * * @param InputInterface $input Command input * @param OutputInterface $output Command output - * + * * @return int Command success code */ protected function execute( @@ -86,9 +86,9 @@ protected function execute( /** * Resolves configuration file path from input or default location. - * + * * @param InputInterface $input Command input - * + * * @return non-empty-string|null Path to the configuration file */ private function getConfigFile(InputInterface $input): ?string @@ -108,4 +108,4 @@ private function getConfigFile(InputInterface $input): ?string return null; } -} \ No newline at end of file +} diff --git a/src/DLoad.php b/src/DLoad.php index cabd90a..06c611b 100644 --- a/src/DLoad.php +++ b/src/DLoad.php @@ -141,7 +141,7 @@ private function shouldBeExtracted(\SplFileInfo $source, array $mapping): ?\SplF foreach ($mapping as $conf) { if (\preg_match($conf->pattern, $source->getFilename())) { - $newName = match(true) { + $newName = match (true) { $conf->rename === null => $source->getFilename(), $source->getExtension() === '' => $conf->rename, default => $conf->rename . '.' . $source->getExtension(), diff --git a/src/Module/Archive/Exception/ArchiveException.php b/src/Module/Archive/Exception/ArchiveException.php index 9669c76..a6c1d26 100644 --- a/src/Module/Archive/Exception/ArchiveException.php +++ b/src/Module/Archive/Exception/ArchiveException.php @@ -4,4 +4,4 @@ namespace Internal\DLoad\Module\Archive\Exception; -class ArchiveException extends \RuntimeException{} +class ArchiveException extends \RuntimeException {} diff --git a/src/Module/Common/Architecture.php b/src/Module/Common/Architecture.php index 9fb0d6a..907dcf0 100644 --- a/src/Module/Common/Architecture.php +++ b/src/Module/Common/Architecture.php @@ -10,6 +10,16 @@ /** * Architecture enumeration. * + * Represents CPU architectures supported by the downloader system. + * + * ```php + * // To get the current system architecture, use DI Container: + * $arch = $container->get(Architecture::class); + * + * // To create an architecture instance from a build configuration name: + * $arch = Architecture::tryFromBuildName('my-software_amd64'); + * ``` + * * @internal */ enum Architecture: string implements Factoriable diff --git a/src/Module/Common/Internal/Attribute/ConfigAttribute.php b/src/Module/Common/Internal/Attribute/ConfigAttribute.php index da16ae8..4307de0 100644 --- a/src/Module/Common/Internal/Attribute/ConfigAttribute.php +++ b/src/Module/Common/Internal/Attribute/ConfigAttribute.php @@ -5,6 +5,8 @@ namespace Internal\DLoad\Module\Common\Internal\Attribute; /** + * Base interface for all configuration attributes. + * * @internal */ interface ConfigAttribute {} diff --git a/src/Module/Common/Internal/Attribute/Env.php b/src/Module/Common/Internal/Attribute/Env.php index 781f928..dbab5b1 100644 --- a/src/Module/Common/Internal/Attribute/Env.php +++ b/src/Module/Common/Internal/Attribute/Env.php @@ -5,11 +5,18 @@ namespace Internal\DLoad\Module\Common\Internal\Attribute; /** + * Environment variable configuration attribute. + * + * Maps a property to an environment variable. + * * @internal */ #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::IS_REPEATABLE)] final class Env implements ConfigAttribute { + /** + * @param non-empty-string $name Environment variable name + */ public function __construct( public string $name, ) {} diff --git a/src/Module/Common/Internal/Attribute/InputArgument.php b/src/Module/Common/Internal/Attribute/InputArgument.php index e141eea..19de71f 100644 --- a/src/Module/Common/Internal/Attribute/InputArgument.php +++ b/src/Module/Common/Internal/Attribute/InputArgument.php @@ -5,11 +5,18 @@ namespace Internal\DLoad\Module\Common\Internal\Attribute; /** + * Command line argument configuration attribute. + * + * Maps a property to a command line argument. + * * @internal */ #[\Attribute(\Attribute::TARGET_PROPERTY)] final class InputArgument implements ConfigAttribute { + /** + * @param non-empty-string $name Argument name + */ public function __construct( public string $name, ) {} diff --git a/src/Module/Common/Internal/Attribute/InputOption.php b/src/Module/Common/Internal/Attribute/InputOption.php index dd85785..6f2209e 100644 --- a/src/Module/Common/Internal/Attribute/InputOption.php +++ b/src/Module/Common/Internal/Attribute/InputOption.php @@ -5,11 +5,18 @@ namespace Internal\DLoad\Module\Common\Internal\Attribute; /** + * Command line option configuration attribute. + * + * Maps a property to a command line option. + * * @internal */ #[\Attribute(\Attribute::TARGET_PROPERTY)] final class InputOption implements ConfigAttribute { + /** + * @param non-empty-string $name Option name + */ public function __construct( public string $name, ) {} diff --git a/src/Module/Common/Internal/Attribute/PhpIni.php b/src/Module/Common/Internal/Attribute/PhpIni.php index 6b1d4bd..9b981d9 100644 --- a/src/Module/Common/Internal/Attribute/PhpIni.php +++ b/src/Module/Common/Internal/Attribute/PhpIni.php @@ -5,11 +5,18 @@ namespace Internal\DLoad\Module\Common\Internal\Attribute; /** + * PHP INI configuration attribute. + * + * Maps a property to a PHP INI setting. + * * @internal */ -#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::IS_REPEATABLE)] +#[\Attribute(\Attribute::TARGET_PROPERTY)] final class PhpIni implements ConfigAttribute { + /** + * @param non-empty-string $option PHP INI option name + */ public function __construct( public string $option, ) {} diff --git a/src/Module/Common/Internal/Attribute/XPath.php b/src/Module/Common/Internal/Attribute/XPath.php index aa7b98c..0293ab9 100644 --- a/src/Module/Common/Internal/Attribute/XPath.php +++ b/src/Module/Common/Internal/Attribute/XPath.php @@ -5,11 +5,19 @@ namespace Internal\DLoad\Module\Common\Internal\Attribute; /** + * XPath configuration attribute. + * + * Maps a property to an XML element or attribute using XPath expression. + * * @internal */ #[\Attribute(\Attribute::TARGET_PROPERTY)] final class XPath implements ConfigAttribute { + /** + * @param non-empty-string $path XPath expression + * @param int<0, max> $key Index in the result array (if XPath returns multiple items) + */ public function __construct( public string $path, public int $key = 0, diff --git a/src/Module/Common/Internal/Attribute/XPathEmbedList.php b/src/Module/Common/Internal/Attribute/XPathEmbedList.php index 0a0c6d6..d2dcd81 100644 --- a/src/Module/Common/Internal/Attribute/XPathEmbedList.php +++ b/src/Module/Common/Internal/Attribute/XPathEmbedList.php @@ -5,14 +5,19 @@ namespace Internal\DLoad\Module\Common\Internal\Attribute; /** + * XPath embedding list configuration attribute. + * + * Maps a property to a list of objects by loading each from an XML element + * that matches the XPath expression. + * * @internal */ #[\Attribute(\Attribute::TARGET_PROPERTY)] final class XPathEmbedList implements ConfigAttribute { /** - * @param non-empty-string $path - * @param class-string $class + * @param non-empty-string $path XPath expression to locate elements + * @param class-string $class Class to instantiate for each matched element */ public function __construct( public string $path, diff --git a/src/Module/Common/Internal/Injection/ConfigLoader.php b/src/Module/Common/Internal/Injection/ConfigLoader.php index 8aaacc0..64c1763 100644 --- a/src/Module/Common/Internal/Injection/ConfigLoader.php +++ b/src/Module/Common/Internal/Injection/ConfigLoader.php @@ -14,6 +14,11 @@ use Internal\DLoad\Service\Logger; /** + * Configuration loader service. + * + * Hydrates configuration objects with values from different sources + * based on their property attributes. + * * @internal */ final class ConfigLoader @@ -21,6 +26,8 @@ final class ConfigLoader private \SimpleXMLElement|null $xml = null; /** + * Creates a new configuration loader. + * * @psalm-suppress RiskyTruthyFalsyComparison */ public function __construct( @@ -40,6 +47,9 @@ public function __construct( } } + /** + * Hydrates a configuration object with values from the configured sources. + */ public function hydrate(object $config): void { // Read class properties @@ -55,6 +65,8 @@ public function hydrate(object $config): void } /** + * Injects values into a property based on its configuration attributes. + * * @param \ReflectionProperty $property * @param list<\ReflectionAttribute> $attributes */ @@ -115,6 +127,9 @@ private function injectValue(object $config, \ReflectionProperty $property, arra } } + /** + * Gets a value from XML using an XPath expression. + */ private function getXPath(XPath $attribute): mixed { $value = $this->xml?->xpath($attribute->path); @@ -124,6 +139,9 @@ private function getXPath(XPath $attribute): mixed : null; } + /** + * Gets a list of objects from XML using an XPath expression. + */ private function getXPathEmbeddedList(XPathEmbedList $attribute): array { if ($this->xml === null) { @@ -147,6 +165,9 @@ private function getXPathEmbeddedList(XPathEmbedList $attribute): array return $result; } + /** + * Creates a new loader instance with the specified XML element. + */ private function withXml(\SimpleXMLElement $xml): self { $self = clone $this; diff --git a/src/Module/Common/Internal/Container.php b/src/Module/Common/Internal/ObjectContainer.php similarity index 74% rename from src/Module/Common/Internal/Container.php rename to src/Module/Common/Internal/ObjectContainer.php index 3997eb6..23b849c 100644 --- a/src/Module/Common/Internal/Container.php +++ b/src/Module/Common/Internal/ObjectContainer.php @@ -5,19 +5,21 @@ namespace Internal\DLoad\Module\Common\Internal; use Internal\DLoad\Module\Common\Internal\Injection\ConfigLoader; -use Internal\DLoad\Service\Container as AppContainerInterface; -use Internal\DLoad\Service\Destroyable; +use Internal\DLoad\Service\Container; use Internal\DLoad\Service\Factoriable; use Psr\Container\ContainerInterface; use Psr\Container\NotFoundExceptionInterface; use Yiisoft\Injector\Injector; /** - * Simple container. + * Simple dependency injection container. + * + * Provides service creation and caching with autowiring capabilities. + * Automatically loads configuration for config classes. * * @internal */ -final class Container implements AppContainerInterface, ContainerInterface, Destroyable +final class ObjectContainer implements Container, ContainerInterface { /** @var array */ private array $cache = []; @@ -38,48 +40,23 @@ public function __construct() $this->cache[ContainerInterface::class] = $this; } - /** - * @template T of object - * @param class-string $id - * @param array $arguments Will be used if the object is created for the first time. - * @return T - * - * @psalm-suppress MoreSpecificImplementedParamType, InvalidReturnType - */ public function get(string $id, array $arguments = []): object { /** @psalm-suppress InvalidReturnStatement */ return $this->cache[$id] ??= $this->make($id, $arguments); } - /** - * @param class-string $id - * - * @psalm-suppress MoreSpecificImplementedParamType - */ public function has(string $id): bool { return \array_key_exists($id, $this->cache) || \array_key_exists($id, $this->factory); } - /** - * @template T of object - * @param T $service - * @param class-string|null $id - */ public function set(object $service, ?string $id = null): void { \assert($id === null || $service instanceof $id, "Service must be instance of {$id}."); $this->cache[$id ?? \get_class($service)] = $service; } - /** - * Create an object of the specified class without caching. - * - * @template T - * @param class-string $class - * @return T - */ public function make(string $class, array $arguments = []): object { $binding = $this->factory[$class] ?? null; @@ -109,11 +86,9 @@ public function make(string $class, array $arguments = []): object } /** - * Declare a factory or predefined arguments for the specified class. - * - * @template T of object - * @param class-string $id - * @param array|\Closure(Container): T $binding + * @template T + * @param class-string $id Service identifier + * @param null|array|\Closure(Container): T $binding Factory function or constructor arguments */ public function bind(string $id, \Closure|array|null $binding = null): void { @@ -126,7 +101,9 @@ public function bind(string $id, \Closure|array|null $binding = null): void "Class `$id` must have a factory or be a factory itself and implement `Factoriable`.", ); - $this->factory[$id] = $id::create(...); + /** @var T $object */ + $object = $id::create(...); + $this->factory[$id] = $object; } public function destroy(): void diff --git a/src/Module/Common/OperatingSystem.php b/src/Module/Common/OperatingSystem.php index c67c9d3..7fead08 100644 --- a/src/Module/Common/OperatingSystem.php +++ b/src/Module/Common/OperatingSystem.php @@ -10,6 +10,16 @@ /** * Operating system enumeration. * + * Represents the different operating systems supported by the downloader. + * + * ```php + * // Recommended: Get from container (autowired with build config) + * $os = $container->get(OperatingSystem::class); + * + * // Or create from build config name + * $os = OperatingSystem::tryFromBuildName('my-software_darwin'); + * ``` + * * @internal */ enum OperatingSystem: string implements Factoriable diff --git a/src/Module/Common/Stability.php b/src/Module/Common/Stability.php index d211558..383eac5 100644 --- a/src/Module/Common/Stability.php +++ b/src/Module/Common/Stability.php @@ -10,6 +10,17 @@ /** * Software stability level. * + * Defines the stability level of software releases from most stable to least. + * Used to filter releases based on desired stability. + * + * ```php + * // Getting stability from build config + * $stability = Stability::create($buildConfig); + * + * // Getting the stability level weight (higher number means more stable) + * $weight = $stability->getWeight(); + * ``` + * * @internal */ enum Stability: string implements Factoriable diff --git a/src/Service/Container.php b/src/Service/Container.php index 03e01a5..08cc503 100644 --- a/src/Service/Container.php +++ b/src/Service/Container.php @@ -47,7 +47,7 @@ public function has(string $id): bool; /** * Registers an existing service instance in the container. * - * @template T of object + * @template T * @param T $service Service instance to register * @param class-string|null $id Optional service identifier (defaults to object's class) */ @@ -64,11 +64,13 @@ public function set(object $service, ?string $id = null): void; public function make(string $class, array $arguments = []): object; /** + * Declares a factory or predefined arguments for the specified class. + * * Configures how a service should be instantiated. * * @template T of object * @param class-string $id Service identifier - * @param array|\Closure(Container): T $binding Factory function or constructor arguments + * @param null|array|\Closure(Container): T $binding Factory function or constructor arguments */ public function bind(string $id, \Closure|array|null $binding = null): void; } diff --git a/src/Service/Destroyable.php b/src/Service/Destroyable.php index 691b71d..7c1f715 100644 --- a/src/Service/Destroyable.php +++ b/src/Service/Destroyable.php @@ -34,4 +34,4 @@ interface Destroyable * Called by the Container when shutting down or releasing services. */ public function destroy(): void; -} \ No newline at end of file +} diff --git a/src/Service/Factoriable.php b/src/Service/Factoriable.php index 2e4d4b0..bd07024 100644 --- a/src/Service/Factoriable.php +++ b/src/Service/Factoriable.php @@ -28,4 +28,4 @@ * * @internal */ -interface Factoriable {} \ No newline at end of file +interface Factoriable {} From a6650b6c26078f111c6ba48add02121eaa7de191 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sat, 5 Apr 2025 17:03:31 +0400 Subject: [PATCH 08/30] docs: cover configs and inputs in Common module --- resources/prompts/cover-class-by-docs.yaml | 1 + src/Module/Common/Config/Action/Download.php | 17 +++++++++++--- src/Module/Common/Config/Actions.php | 6 ++++- .../Common/Config/CustomSoftwareRegistry.php | 8 ++++++- src/Module/Common/Config/Downloader.php | 6 +++++ src/Module/Common/Config/Embed/File.php | 19 ++++++++++++++-- src/Module/Common/Config/Embed/Repository.php | 19 +++++++++++++++- src/Module/Common/Config/Embed/Software.php | 22 +++++++++---------- src/Module/Common/Config/GitHub.php | 5 +++++ src/Module/Common/Input/Build.php | 5 +++++ src/Module/Common/Input/Destination.php | 5 +++++ 11 files changed, 94 insertions(+), 19 deletions(-) diff --git a/resources/prompts/cover-class-by-docs.yaml b/resources/prompts/cover-class-by-docs.yaml index f3946f3..e11b2be 100644 --- a/resources/prompts/cover-class-by-docs.yaml +++ b/resources/prompts/cover-class-by-docs.yaml @@ -28,6 +28,7 @@ prompts: - Keep docs clean - if the method has the same signature in the parent, don't comment the implementation if there is no additional things. - remove trailing spaces even in empty comment lines + - keep 120 symbols limit - Use generic annotations if possible. Iven f.e. for int based on the logic. - Use inline annotations like {@see ClassName} if it needed to mention in a text block. - Add class or method usage example using markdown like: diff --git a/src/Module/Common/Config/Action/Download.php b/src/Module/Common/Config/Action/Download.php index 7c616d7..6618d87 100644 --- a/src/Module/Common/Config/Action/Download.php +++ b/src/Module/Common/Config/Action/Download.php @@ -7,20 +7,31 @@ use Internal\DLoad\Module\Common\Internal\Attribute\XPath; /** + * Download action configuration. + * + * Represents a single download action from the configuration file. + * Defines what software to download considering its version constraint. + * + * ```php + * $action = Download::fromSoftwareId('roadrunner'); + * ``` + * * @internal */ final class Download { - /** @var non-empty-string */ + /** @var non-empty-string $software Software identifier to download */ #[XPath('@software')] public string $software; - /** @var non-empty-string|null */ + /** @var non-empty-string|null $version Version constraint (composer-style) */ #[XPath('@version')] public ?string $version = null; /** - * @param non-empty-string $software + * Creates a download action from a software identifier. + * + * @param non-empty-string $software Software identifier */ public static function fromSoftwareId(string $software): self { diff --git a/src/Module/Common/Config/Actions.php b/src/Module/Common/Config/Actions.php index ec6daad..0dd0c69 100644 --- a/src/Module/Common/Config/Actions.php +++ b/src/Module/Common/Config/Actions.php @@ -8,11 +8,15 @@ use Internal\DLoad\Module\Common\Internal\Attribute\XPathEmbedList; /** + * Configuration actions container. + * + * Contains the list of download actions defined in the configuration file. + * * @internal */ final class Actions { - /** @var list */ + /** @var list $downloads Collection of download actions */ #[XPathEmbedList('/dload/actions/download', Download::class)] public array $downloads = []; } diff --git a/src/Module/Common/Config/CustomSoftwareRegistry.php b/src/Module/Common/Config/CustomSoftwareRegistry.php index e18b264..7eff982 100644 --- a/src/Module/Common/Config/CustomSoftwareRegistry.php +++ b/src/Module/Common/Config/CustomSoftwareRegistry.php @@ -7,13 +7,19 @@ use Internal\DLoad\Module\Common\Internal\Attribute\XPath; use Internal\DLoad\Module\Common\Internal\Attribute\XPathEmbedList; +/** + * Custom software registry configuration. + * + * Holds settings for custom software definitions provided in the configuration. + */ final class CustomSoftwareRegistry { + /** @var bool $overwrite Whether to overwrite built-in software definitions */ #[XPath('/dload/registry/@overwrite')] public bool $overwrite = false; /** - * @var \Internal\DLoad\Module\Common\Config\Embed\Software[] + * @var Embed\Software[] $software Custom software definitions from the configuration */ #[XPathEmbedList('/dload/registry/software', Embed\Software::class)] public array $software = []; diff --git a/src/Module/Common/Config/Downloader.php b/src/Module/Common/Config/Downloader.php index 855ca8b..6a24d4d 100644 --- a/src/Module/Common/Config/Downloader.php +++ b/src/Module/Common/Config/Downloader.php @@ -6,8 +6,14 @@ use Internal\DLoad\Module\Common\Internal\Attribute\XPath; +/** + * Downloader configuration. + * + * Contains global settings for the download functionality. + */ final class Downloader { + /** @var string|null $tmpDir Temporary directory for downloads */ #[XPath('/dload/@temp-dir')] public ?string $tmpDir = null; } diff --git a/src/Module/Common/Config/Embed/File.php b/src/Module/Common/Config/Embed/File.php index 3f4f60d..4de3032 100644 --- a/src/Module/Common/Config/Embed/File.php +++ b/src/Module/Common/Config/Embed/File.php @@ -7,6 +7,17 @@ use Internal\DLoad\Module\Common\Internal\Attribute\XPath; /** + * File configuration. + * + * Defines how a file should be extracted and optionally renamed after download. + * + * ```php + * $file = File::fromArray([ + * 'pattern' => '/^roadrunner(?:\.exe)?$/', + * 'rename' => 'rr' + * ]); + * ``` + * * @psalm-type FileArray = array{ * rename?: non-empty-string, * pattern?: non-empty-string @@ -15,16 +26,20 @@ final class File { /** - * @var non-empty-string|null In case of not null, found file will be renamed to this value with the same extension. + * @var non-empty-string|null $rename In case of not null, found file will be renamed to this value + * with the same extension. */ #[XPath('@rename')] public ?string $rename = null; + /** @var non-empty-string $pattern Regular expression pattern to match files */ #[XPath('@pattern')] public string $pattern = '/^.*$/'; /** - * @param FileArray $fileArray + * Creates a File configuration from an array. + * + * @param FileArray $fileArray Configuration array */ public static function fromArray(mixed $fileArray): self { diff --git a/src/Module/Common/Config/Embed/Repository.php b/src/Module/Common/Config/Embed/Repository.php index e5e8ae4..947b637 100644 --- a/src/Module/Common/Config/Embed/Repository.php +++ b/src/Module/Common/Config/Embed/Repository.php @@ -7,6 +7,18 @@ use Internal\DLoad\Module\Common\Internal\Attribute\XPath; /** + * Repository configuration. + * + * Defines a source repository for software packages. + * + * ```php + * $repo = Repository::fromArray([ + * 'type' => 'github', + * 'uri' => 'roadrunner-server/roadrunner', + * 'asset-pattern' => '#^roadrunner-.*#' + * ]); + * ``` + * * @psalm-type RepositoryArray = array{ * type: non-empty-string, * uri: non-empty-string, @@ -15,17 +27,22 @@ */ final class Repository { + /** @var non-empty-string $type Repository type identifier */ #[XPath('@type')] public string $type = 'github'; + /** @var non-empty-string $uri Repository URI identifier */ #[XPath('@uri')] public string $uri; + /** @var non-empty-string $assetPattern Regular expression pattern to match assets */ #[XPath('@asset-pattern')] public string $assetPattern = '/^.*$/'; /** - * @param RepositoryArray $repositoryArray + * Creates a Repository configuration from an array. + * + * @param RepositoryArray $repositoryArray Configuration array */ public static function fromArray(mixed $repositoryArray): self { diff --git a/src/Module/Common/Config/Embed/Software.php b/src/Module/Common/Config/Embed/Software.php index e2fe547..8ade1ad 100644 --- a/src/Module/Common/Config/Embed/Software.php +++ b/src/Module/Common/Config/Embed/Software.php @@ -13,6 +13,17 @@ * Represents a software package that can be downloaded through the system. * Contains all necessary metadata for proper identification and retrieval. * + * ```php + * $software = Software::fromArray([ + * 'name' => 'RoadRunner', + * 'alias' => 'rr', + * 'description' => 'High performance PHP application server', + * 'repositories' => [ + * ['type' => 'github', 'uri' => 'roadrunner-server/roadrunner'] + * ] + * ]); + * ``` + * * @psalm-import-type RepositoryArray from Repository * @psalm-import-type FileArray from File * @psalm-type SoftwareArray = array{ @@ -56,17 +67,6 @@ final class Software /** * Creates a Software instance from array configuration. * - * ```php - * $software = Software::fromArray([ - * 'name' => 'RoadRunner', - * 'alias' => 'rr', - * 'description' => 'High performance PHP application server', - * 'repositories' => [ - * ['type' => 'github', 'uri' => 'roadrunner-server/roadrunner'] - * ] - * ]); - * ``` - * * @param SoftwareArray $softwareArray Configuration array */ public static function fromArray(mixed $softwareArray): self diff --git a/src/Module/Common/Config/GitHub.php b/src/Module/Common/Config/GitHub.php index 34706c8..8f4d549 100644 --- a/src/Module/Common/Config/GitHub.php +++ b/src/Module/Common/Config/GitHub.php @@ -7,10 +7,15 @@ use Internal\DLoad\Module\Common\Internal\Attribute\Env; /** + * GitHub API configuration. + * + * Contains authentication settings for GitHub API access. + * * @internal */ final class GitHub { + /** @var string|null $token API token for GitHub authentication */ #[Env('GITHUB_TOKEN')] public ?string $token = null; } diff --git a/src/Module/Common/Input/Build.php b/src/Module/Common/Input/Build.php index 41bc720..4582701 100644 --- a/src/Module/Common/Input/Build.php +++ b/src/Module/Common/Input/Build.php @@ -10,6 +10,11 @@ use Internal\DLoad\Module\Common\Stability; /** + * Build configuration options. + * + * Contains input options that define the build parameters for software selection. + * These values are typically provided via command-line options. + * * @internal */ final class Build diff --git a/src/Module/Common/Input/Destination.php b/src/Module/Common/Input/Destination.php index ad0dc09..1abdeb1 100644 --- a/src/Module/Common/Input/Destination.php +++ b/src/Module/Common/Input/Destination.php @@ -7,10 +7,15 @@ use Internal\DLoad\Module\Common\Internal\Attribute\InputOption; /** + * Destination configuration. + * + * Contains the destination path where downloaded software will be saved. + * * @internal */ final class Destination { + /** @var string|null $path Target path for downloaded files */ #[InputOption('path')] public ?string $path = null; } From a455ac10d9f03ee3dcffca810e15ba09bb95c077 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sat, 5 Apr 2025 17:24:31 +0400 Subject: [PATCH 09/30] docs: cover Downloader module with docs. --- resources/prompts/cover-class-by-docs.yaml | 10 ++-- .../Common/Config/CustomSoftwareRegistry.php | 2 +- src/Module/Downloader/Downloader.php | 56 +++++++++++++++++-- .../Downloader/Internal/DownloadContext.php | 20 +++++-- src/Module/Downloader/Progress.php | 9 +++ src/Module/Downloader/SoftwareCollection.php | 30 ++++++++-- src/Module/Downloader/Task/DownloadResult.php | 10 +++- src/Module/Downloader/Task/DownloadTask.php | 11 +++- 8 files changed, 125 insertions(+), 23 deletions(-) diff --git a/resources/prompts/cover-class-by-docs.yaml b/resources/prompts/cover-class-by-docs.yaml index e11b2be..28e58cf 100644 --- a/resources/prompts/cover-class-by-docs.yaml +++ b/resources/prompts/cover-class-by-docs.yaml @@ -23,12 +23,11 @@ prompts: - Use @link annotation if it's necessary to write some URL. - Use @see annotation if it's necessary to reference to a class or method. - Use extended types like class-string, non-empty-string, non-empty-array, non-empty-list, etc, if it's logically correct. - - Keep only essential and non-obvious documentation. - Don't use @inheritDoc annotations. - - Keep docs clean - - if the method has the same signature in the parent, don't comment the implementation if there is no additional things. + - Keep docs clean: + - If the method has the same signature in the parent, don't comment the implementation if there is no additional things. + - Keep only essential and non-obvious documentation. - remove trailing spaces even in empty comment lines - - keep 120 symbols limit - Use generic annotations if possible. Iven f.e. for int based on the logic. - Use inline annotations like {@see ClassName} if it needed to mention in a text block. - Add class or method usage example using markdown like: @@ -44,7 +43,8 @@ prompts: Don't skip internal classes. - Inner comments order: title, description, example, template annotations, params and return annotations, references and internal/deprecated annotations. - Property comments rules: If it possible, write inline comment without title like `/** @var non-empty-string $name User name */`. - - Multiline comments rules: if a comment starts with an annotation, the second line should start with whitespaces aligned with the annotation length: + - Break long lines into multiple lines. Maximum line length is 120 characters. + Multiline comments rules: if a comment starts with an annotation, the second line should start with whitespaces aligned with the annotation length: ```php /** * @var non-empty-string $name Comment on the first line diff --git a/src/Module/Common/Config/CustomSoftwareRegistry.php b/src/Module/Common/Config/CustomSoftwareRegistry.php index 7eff982..c4d6da9 100644 --- a/src/Module/Common/Config/CustomSoftwareRegistry.php +++ b/src/Module/Common/Config/CustomSoftwareRegistry.php @@ -14,7 +14,7 @@ */ final class CustomSoftwareRegistry { - /** @var bool $overwrite Whether to overwrite built-in software definitions */ + /** @var bool $overwrite Replace the built-in software collection with custom ones */ #[XPath('/dload/registry/@overwrite')] public bool $overwrite = false; diff --git a/src/Module/Downloader/Downloader.php b/src/Module/Downloader/Downloader.php index 0f4bf81..ca11a96 100644 --- a/src/Module/Downloader/Downloader.php +++ b/src/Module/Downloader/Downloader.php @@ -25,6 +25,22 @@ use function React\Async\await; use function React\Async\coroutine; +/** + * Core downloader service responsible for fetching software assets. + * + * Manages the entire download process from repository selection to asset downloading. + * Supports multiple repositories and provides fallback capability when a repository fails. + * + * ```php + * // Create a download task + * $task = $downloader->download($software, $config, function(Progress $progress) { + * echo sprintf("Downloaded: %d/%d bytes\n", $progress->current, $progress->total); + * }); + * + * // Execute the task + * $result = await($task->handler()); + * ``` + */ final class Downloader { public function __construct( @@ -38,10 +54,16 @@ public function __construct( ) {} /** - * Create task to download software. + * Creates a task to download software. * - * @param \Closure(Progress): mixed $onProgress Callback to report progress. + * Prepares a download task that can be executed to obtain the software asset. The task tries repositories + * sequentially until one succeeds. + * + * @param Software $software Software package configuration + * @param DownloadConfig $actionConfig Download action configuration + * @param \Closure(Progress): mixed $onProgress Callback to report download progress. * Exception thrown in this callback will stop and revert the task. + * @return DownloadTask Executable download task object */ public function download( Software $software, @@ -89,7 +111,13 @@ public function download( } /** - * @return \Closure(): ReleaseInterface + * Processes the repository to find suitable releases. + * + * Fetches and filters releases from the repository based on stability and version constraints. + * + * @param RepositoryInterface $repository Repository to process + * @param DownloadContext $context Download context information + * @return \Closure(): ReleaseInterface Closure that returns the selected release */ private function processRepository(RepositoryInterface $repository, DownloadContext $context): \Closure { @@ -130,7 +158,12 @@ private function processRepository(RepositoryInterface $repository, DownloadCont } /** - * @return \Closure(): AssetInterface + * Processes a release to find suitable assets. + * + * Filters assets from the release based on architecture, operating system, and name pattern. + * + * @param DownloadContext $context Download context information + * @return \Closure(): AssetInterface Closure that returns the selected asset */ private function processRelease(DownloadContext $context): \Closure { @@ -160,7 +193,12 @@ private function processRelease(DownloadContext $context): \Closure } /** - * @return \Closure(): \SplFileObject + * Downloads the selected asset to a temporary file. + * + * Creates a temporary file and downloads the asset content, reporting progress via callback. + * + * @param DownloadContext $context Download context information + * @return \Closure(): \SplFileObject Closure that returns the downloaded file */ private function processAsset(DownloadContext $context): \Closure { @@ -196,6 +234,14 @@ private function processAsset(DownloadContext $context): \Closure }; } + /** + * Returns the temporary directory path for file downloads. + * + * Uses the configured directory if available and writable, otherwise defaults to system temp directory. + * + * @return non-empty-string Path to temporary directory + * @throws \LogicException When configured directory is not writable + */ private function getTempDirectory(): string { $temp = $this->config->tmpDir; diff --git a/src/Module/Downloader/Internal/DownloadContext.php b/src/Module/Downloader/Internal/DownloadContext.php index 2ef349e..d1a9038 100644 --- a/src/Module/Downloader/Internal/DownloadContext.php +++ b/src/Module/Downloader/Internal/DownloadContext.php @@ -11,23 +11,35 @@ use Internal\DLoad\Module\Repository\AssetInterface; use Internal\DLoad\Module\Repository\ReleaseInterface; +/** + * Context object for download operations. + * + * Contains all contextual information needed during a download operation, including + * configurations, selected components, and callback functions. + * + * @internal + */ final class DownloadContext { - /** Current repository config */ + /** @var Repository Current repository configuration */ public Repository $repoConfig; - /** Downloaded file */ + /** @var \SplFileObject Downloaded file handle */ public \SplFileObject $file; - /** Current asset */ + /** @var AssetInterface Current asset being processed */ public AssetInterface $asset; - /** Current release */ + /** @var ReleaseInterface Current release being processed */ public ReleaseInterface $release; /** + * Creates a new download context. + * + * @param Software $software Software package configuration * @param \Closure(Progress): mixed $onProgress Callback to report progress. * Exception thrown in this callback will stop and revert the task. + * @param DownloadConfig $actionConfig Download action configuration */ public function __construct( public readonly Software $software, diff --git a/src/Module/Downloader/Progress.php b/src/Module/Downloader/Progress.php index 4fde937..1d9bb8b 100644 --- a/src/Module/Downloader/Progress.php +++ b/src/Module/Downloader/Progress.php @@ -4,11 +4,20 @@ namespace Internal\DLoad\Module\Downloader; +/** + * Represents download progress information. + * + * Immutable data object containing download progress metrics and status message. + * Used for reporting progress to listeners during download operations. + */ final class Progress { public function __construct( + /** @var int<0, max> Total size in bytes */ public readonly int $total = 100, + /** @var int<0, max> Current progress in bytes */ public readonly int $current = 0, + /** @var string Status message */ public readonly string $message = '', ) {} } diff --git a/src/Module/Downloader/SoftwareCollection.php b/src/Module/Downloader/SoftwareCollection.php index f27c50c..3f61d50 100644 --- a/src/Module/Downloader/SoftwareCollection.php +++ b/src/Module/Downloader/SoftwareCollection.php @@ -12,15 +12,23 @@ /** * Collection of software package configurations. * + * Manages both custom and default software registry entries. + * Provides lookup functionality to find software by name or alias. + * * @implements IteratorAggregate */ final class SoftwareCollection implements \IteratorAggregate, \Countable { - /** @var array */ + /** @var array Map of software ID to configuration */ private array $registry = []; /** * Creates a collection from registry and optionally loads default entries. + * + * Processes custom registry entries first, then loads default registry unless + * overwrite flag is set in the custom registry. + * + * @param CustomSoftwareRegistry $softwareRegistry Custom software configuration registry */ public function __construct(CustomSoftwareRegistry $softwareRegistry) { @@ -34,11 +42,10 @@ public function __construct(CustomSoftwareRegistry $softwareRegistry) /** * Finds software configuration by name. * + * Searches for software using exact name or alias match. + * * ```php - * $software = $collection->findSoftware('rr'); - * if ($software !== null) { - * // Process software configuration - * } + * $software = $collection->findSoftware('rr') ?? throw new \RuntimeException('Software not found'); * ``` * * @param non-empty-string $name Software name or alias @@ -50,6 +57,8 @@ public function findSoftware(string $name): ?Software } /** + * Returns iterator for all software configurations. + * * @return \Traversable */ public function getIterator(): \Traversable @@ -57,14 +66,23 @@ public function getIterator(): \Traversable yield from $this->registry; } + /** + * Returns the number of software packages in the collection. + * + * @return int<0, max> Number of software packages + */ public function count(): int { return \count($this->registry); } /** - * Loads default software registry from embedded JSON file. + * Loads default software registry from the embedded JSON file. + * + * Parses the default software.json file and adds entries to the registry + * without overwriting existing entries. * + * @link resources/software.json * @see Software::fromArray() For parsing logic */ private function loadDefaultRegistry(): void diff --git a/src/Module/Downloader/Task/DownloadResult.php b/src/Module/Downloader/Task/DownloadResult.php index c7ae061..15218ed 100644 --- a/src/Module/Downloader/Task/DownloadResult.php +++ b/src/Module/Downloader/Task/DownloadResult.php @@ -4,10 +4,18 @@ namespace Internal\DLoad\Module\Downloader\Task; +/** + * Represents the result of a successful download operation. + * + * Contains the downloaded file reference and version information. + */ final class DownloadResult { /** - * @param non-empty-string $version + * Creates a new download result. + * + * @param \SplFileInfo $file Downloaded file information + * @param non-empty-string $version Version of the downloaded software */ public function __construct( public readonly \SplFileInfo $file, diff --git a/src/Module/Downloader/Task/DownloadTask.php b/src/Module/Downloader/Task/DownloadTask.php index 4752021..8449394 100644 --- a/src/Module/Downloader/Task/DownloadTask.php +++ b/src/Module/Downloader/Task/DownloadTask.php @@ -8,12 +8,21 @@ use Internal\DLoad\Module\Downloader\Progress; use React\Promise\PromiseInterface; +/** + * Represents an executable download task. + * + * Contains all the information needed to execute a download operation, + * including handler function and progress callback. + */ final class DownloadTask { /** + * Creates a new download task. + * + * @param Software $software Software package configuration * @param \Closure(Progress): mixed $onProgress Callback to report progress. * Exception thrown in this callback will stop and revert the task. - * @param \Closure(): PromiseInterface $handler + * @param \Closure(): PromiseInterface $handler Function that executes the download */ public function __construct( public readonly Software $software, From d492be5466226f8c7a3ad149f4d0fdb419da2353 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sat, 5 Apr 2025 17:47:28 +0400 Subject: [PATCH 10/30] docs: cover Repository module with docs. --- resources/prompts/cover-class-by-docs.yaml | 9 +- src/Module/Repository/AssetInterface.php | 33 ++++++- .../Collection/AssetsCollection.php | 44 ++++++++- ...Collection.php => CompositeRepository.php} | 22 ++++- .../Collection/ReleasesCollection.php | 59 +++++++++--- src/Module/Repository/Internal/Collection.php | 96 +++++++++++++++---- src/Module/Repository/ReleaseInterface.php | 38 +++++++- src/Module/Repository/RepositoryInterface.php | 15 ++- src/Module/Repository/RepositoryProvider.php | 24 +++++ 9 files changed, 293 insertions(+), 47 deletions(-) rename src/Module/Repository/Collection/{RepositoriesCollection.php => CompositeRepository.php} (53%) diff --git a/resources/prompts/cover-class-by-docs.yaml b/resources/prompts/cover-class-by-docs.yaml index 28e58cf..11e6eab 100644 --- a/resources/prompts/cover-class-by-docs.yaml +++ b/resources/prompts/cover-class-by-docs.yaml @@ -15,7 +15,7 @@ prompts: content: | Read {{class-name}} class(es) or interface(s). - Cover it with comments using following instructions: + Cover it with comments using following important instructions: - Don't break signatures. You can only add or improve comments. - Don't remove existing @internal, @link, @see, @api, or @deprecated annotations. - Use passive forms instead of we or you. @@ -26,11 +26,12 @@ prompts: - Don't use @inheritDoc annotations. - Keep docs clean: - If the method has the same signature in the parent, don't comment the implementation if there is no additional things. - - Keep only essential and non-obvious documentation. + - Keep only essential and non-obvious documentation. It's important to avoid redundancy. - remove trailing spaces even in empty comment lines - - Use generic annotations if possible. Iven f.e. for int based on the logic. + - Use generic annotations if possible based on the logic. For example for a `count()` has `int<0, max>` return type because the amount of items can't be negative. - Use inline annotations like {@see ClassName} if it needed to mention in a text block. - - Add class or method usage example using markdown like: + - Add class or method usage example using markdown like. + Note that there is leading space before inside the code block: ```php // Comment for the following code some code diff --git a/src/Module/Repository/AssetInterface.php b/src/Module/Repository/AssetInterface.php index 8a61435..1f99f97 100644 --- a/src/Module/Repository/AssetInterface.php +++ b/src/Module/Repository/AssetInterface.php @@ -7,28 +7,53 @@ use Internal\DLoad\Module\Common\Architecture; use Internal\DLoad\Module\Common\OperatingSystem; +/** + * Represents a downloadable asset from a software release. + * + * Assets are individual binary files or archives associated with a specific release, + * often targeting particular operating systems or architectures. + */ interface AssetInterface { + /** + * Returns the release this asset belongs to. + * + * @return ReleaseInterface The parent release + */ public function getRelease(): ReleaseInterface; /** - * @return non-empty-string + * Returns the name of the asset. + * + * @return non-empty-string Asset name, typically the filename */ public function getName(): string; /** - * @return non-empty-string + * Returns the URI from which the asset can be downloaded. + * + * @return non-empty-string Download URI */ public function getUri(): string; + /** + * Returns the operating system this asset is compatible with, if specified. + * + * @return OperatingSystem|null The target OS or null if not OS-specific + */ public function getOperatingSystem(): ?OperatingSystem; + /** + * Returns the CPU architecture this asset is compatible with, if specified. + * + * @return Architecture|null The target architecture or null if not architecture-specific + */ public function getArchitecture(): ?Architecture; /** - * Load content from the asset. + * Downloads the asset content. * - * @return \Traversable + * @return \Traversable Stream of content chunks */ public function download(): \Traversable; } diff --git a/src/Module/Repository/Collection/AssetsCollection.php b/src/Module/Repository/Collection/AssetsCollection.php index e25637b..c950db1 100644 --- a/src/Module/Repository/Collection/AssetsCollection.php +++ b/src/Module/Repository/Collection/AssetsCollection.php @@ -10,12 +10,34 @@ use Internal\DLoad\Module\Repository\Internal\Collection; /** + * Collection of release assets with filtering capabilities. + * + * Provides methods to filter assets by architecture, operating system, + * file extension, and name patterns. + * + * ```php + * // Get Linux x86_64 assets with certain file extensions + * $assets = $release->getAssets() + * ->whereOperatingSystem(OperatingSystem::Linux) + * ->whereArchitecture(Architecture::X86_64) + * ->whereFileExtensions(['tar.gz', 'zip']) + * ->exceptDebPackages(); + * + * // Find assets matching a pattern + * $assets = $release->getAssets()->whereNameMatches('/^app-v\d+/'); + * ``` + * * @template-extends Collection * @internal * @psalm-internal Internal\DLoad\Module */ final class AssetsCollection extends Collection { + /** + * Filters out Debian package assets. + * + * @return self New filtered collection + */ public function exceptDebPackages(): self { return $this->except( @@ -24,6 +46,12 @@ public function exceptDebPackages(): self ); } + /** + * Filters assets to include only those matching the specified architecture. + * + * @param Architecture $arch Required architecture + * @return self New filtered collection + */ public function whereArchitecture(Architecture $arch): self { return $this->filter( @@ -31,6 +59,12 @@ public function whereArchitecture(Architecture $arch): self ); } + /** + * Filters assets to include only those matching the specified operating system. + * + * @param OperatingSystem $os Required operating system + * @return self New filtered collection + */ public function whereOperatingSystem(OperatingSystem $os): self { return $this->filter( @@ -39,7 +73,10 @@ public function whereOperatingSystem(OperatingSystem $os): self } /** - * @param list $extensions + * Filters assets to include only those with specified file extensions. + * + * @param list $extensions List of file extensions without leading dot + * @return self New filtered collection */ public function whereFileExtensions(array $extensions): self { @@ -58,9 +95,10 @@ static function (AssetInterface $asset) use ($extensions): bool { } /** - * Select all the assets with names that match the given pattern. + * Filters assets to include only those with names matching the given regex pattern. * - * @param non-empty-string $pattern + * @param non-empty-string $pattern Regular expression pattern + * @return self New filtered collection */ public function whereNameMatches(string $pattern): self { diff --git a/src/Module/Repository/Collection/RepositoriesCollection.php b/src/Module/Repository/Collection/CompositeRepository.php similarity index 53% rename from src/Module/Repository/Collection/RepositoriesCollection.php rename to src/Module/Repository/Collection/CompositeRepository.php index 2bb5c91..bdbdd60 100644 --- a/src/Module/Repository/Collection/RepositoriesCollection.php +++ b/src/Module/Repository/Collection/CompositeRepository.php @@ -7,10 +7,23 @@ use Internal\DLoad\Module\Repository\RepositoryInterface; /** + * Collection of repositories that also implements the repository interface. + * + * This allows treating multiple repositories as a single virtual repository, + * aggregating all their releases. + * + * ```php + * // Create a collection of repositories + * $collection = new RepositoriesCollection([$repo1, $repo2]); + * + * // Get all releases from all repositories + * $allReleases = $collection->getReleases(); + * ``` + * * @internal * @psalm-internal Internal\DLoad\Module */ -class RepositoriesCollection implements RepositoryInterface +class CompositeRepository implements RepositoryInterface { /** * @var array @@ -18,7 +31,7 @@ class RepositoriesCollection implements RepositoryInterface private array $repositories; /** - * @param array $repositories + * @param array $repositories List of repositories to include */ public function __construct(array $repositories) { @@ -33,6 +46,11 @@ public function getName(): string return 'unknown/unknown'; } + /** + * Returns a collection of all releases from all repositories. + * + * @return ReleasesCollection Combined collection of releases + */ public function getReleases(): ReleasesCollection { return ReleasesCollection::from(function () { diff --git a/src/Module/Repository/Collection/ReleasesCollection.php b/src/Module/Repository/Collection/ReleasesCollection.php index 7191d88..e1fca93 100644 --- a/src/Module/Repository/Collection/ReleasesCollection.php +++ b/src/Module/Repository/Collection/ReleasesCollection.php @@ -9,6 +9,23 @@ use Internal\DLoad\Module\Repository\ReleaseInterface; /** + * Collection of software releases with filtering and sorting capabilities. + * + * Provides methods to filter releases by version constraints, stability levels, + * and availability of assets. + * + * ```php + * // Get stable releases that satisfy a version constraint + * $releases = $repository->getReleases() + * ->stable() + * ->satisfies('^2.0.0') + * ->withAssets() + * ->sortByVersion(); + * + * // Get the most recent release + * $latestRelease = $releases->first(); + * ``` + * * @template-extends Collection * @internal * @psalm-internal Internal\DLoad\Module @@ -16,8 +33,10 @@ final class ReleasesCollection extends Collection { /** - * @param non-empty-string $constraint - * @return $this + * Filters releases to those that satisfy the given version constraint. + * + * @param non-empty-string $constraint Version constraint in Composer format + * @return $this New filtered collection */ public function satisfies(string $constraint): self { @@ -25,8 +44,10 @@ public function satisfies(string $constraint): self } /** - * @param non-empty-string $constraint - * @return $this + * Filters releases to those that do not satisfy the given version constraint. + * + * @param non-empty-string $constraint Version constraint in Composer format + * @return $this New filtered collection */ public function notSatisfies(string $constraint): self { @@ -34,18 +55,22 @@ public function notSatisfies(string $constraint): self } /** - * @return $this + * Filters releases to those that have at least one asset. + * + * @return $this New filtered collection */ public function withAssets(): self { return $this->filter( - static fn(ReleaseInterface $r): bool => ! $r->getAssets() + static fn(ReleaseInterface $r): bool => !$r->getAssets() ->empty(), ); } /** - * @return $this + * Sorts releases by version in descending order (newest first). + * + * @return $this New sorted collection */ public function sortByVersion(): self { @@ -61,7 +86,9 @@ public function sortByVersion(): self } /** - * @return $this + * Filters releases to only include stable versions. + * + * @return $this New filtered collection */ public function stable(): self { @@ -69,7 +96,10 @@ public function stable(): self } /** - * @return $this + * Filters releases to include only those matching the exact stability level. + * + * @param Stability $stability Required stability level + * @return $this New filtered collection */ public function stability(Stability $stability): self { @@ -77,7 +107,10 @@ public function stability(Stability $stability): self } /** - * @return $this + * Filters releases to include those with at least the specified minimum stability. + * + * @param Stability $stability Minimum stability level + * @return $this New filtered collection */ public function minimumStability(Stability $stability): self { @@ -88,7 +121,11 @@ public function minimumStability(Stability $stability): self } /** - * @return non-empty-string + * Converts version string to a format suitable for comparison. + * + * Normalizes version strings to handle stability suffixes properly. + * + * @return non-empty-string Normalized version string */ private function comparisonVersionString(ReleaseInterface $release): string { diff --git a/src/Module/Repository/Internal/Collection.php b/src/Module/Repository/Internal/Collection.php index b0c63c6..a03360d 100644 --- a/src/Module/Repository/Internal/Collection.php +++ b/src/Module/Repository/Internal/Collection.php @@ -5,6 +5,25 @@ namespace Internal\DLoad\Module\Repository\Internal; /** + * Generic collection base class with filtering, mapping, and traversal capabilities. + * + * This abstract class provides common collection functionality for specialized + * collections like ReleasesCollection and AssetsCollection. + * + * ```php + * // Creating a collection from an array + * $collection = SomeCollection::create($itemsArray); + * + * // Creating a collection from a generator function + * $collection = SomeCollection::from(function() { + * yield new Item('one'); + * yield new Item('two'); + * }); + * + * // Finding the first item matching a condition + * $item = $collection->first(fn($item) => $item->getName() === 'specific'); + * ``` + * * @template T * @template-implements \IteratorAggregate * @@ -19,7 +38,7 @@ abstract class Collection implements \IteratorAggregate, \Countable protected array $items; /** - * @param array $items + * @param array $items Collection items */ final public function __construct(array $items) { @@ -27,8 +46,14 @@ final public function __construct(array $items) } /** - * @param self|iterable|\Closure $items - * @return static + * Creates a new collection from various sources. + * + * Supports creating from an existing collection, a traversable, + * an array, or a generator function. + * + * @param self|iterable|\Closure $items Source of items + * @return static New collection instance + * @throws \InvalidArgumentException If the input cannot be converted to a collection */ public static function create(mixed $items): static { @@ -44,8 +69,10 @@ public static function create(mixed $items): static } /** - * @param \Closure $generator - * @return static + * Creates a collection from a generator function. + * + * @param \Closure $generator Function that yields collection items + * @return static New collection instance */ public static function from(\Closure $generator): static { @@ -53,8 +80,10 @@ public static function from(\Closure $generator): static } /** - * @param callable(T): bool $filter - * @return $this + * Filters the collection using the provided callback. + * + * @param callable(T): bool $filter Function that returns true for items to keep + * @return $this New filtered collection */ public function filter(callable $filter): static { @@ -62,8 +91,10 @@ public function filter(callable $filter): static } /** - * @param callable(T): mixed $map - * @return $this + * Maps each item in the collection using the provided callback. + * + * @param callable(T): mixed $map Function that transforms each item + * @return $this New collection with mapped items */ public function map(callable $map): static { @@ -71,8 +102,10 @@ public function map(callable $map): static } /** - * @param callable(T): bool $filter - * @return $this + * Creates a new collection excluding items that match the filter. + * + * @param callable(T): bool $filter Function that returns true for items to exclude + * @return $this New filtered collection * * @psalm-suppress MissingClosureParamType * @psalm-suppress MixedArgument @@ -85,8 +118,12 @@ public function except(callable $filter): static } /** - * @param null|callable(T): bool $filter - * @return T|null + * Returns the first item that matches the filter, or null if no matches. + * + * If no filter is provided, returns the first item in the collection. + * + * @param null|callable(T): bool $filter Optional filter function + * @return T|null First matching item or null */ public function first(callable $filter = null): ?object { @@ -96,28 +133,42 @@ public function first(callable $filter = null): ?object } /** - * @param callable(): T $otherwise - * @param null|callable(T): bool $filter - * @return T + * Returns the first matching item or a default value from the callback. + * + * @param callable(): T $otherwise Function to provide default value if no match found + * @param null|callable(T): bool $filter Optional filter function + * @return T First matching item or default value */ public function firstOr(callable $otherwise, callable $filter = null): object { return $this->first($filter) ?? $otherwise(); } + /** + * Returns an iterator for traversing the collection. + * + * @return \Traversable Collection iterator + */ public function getIterator(): \Traversable { return new \ArrayIterator($this->items); } + /** + * Returns the number of items in the collection. + * + * @return int<0, max> Item count + */ public function count(): int { return \count($this->items); } /** - * @param callable $then - * @return $this + * Executes a callback if the collection is empty. + * + * @param callable $then Function to execute if collection is empty + * @return $this This collection instance */ public function whenEmpty(callable $then): static { @@ -128,13 +179,20 @@ public function whenEmpty(callable $then): static return $this; } + /** + * Checks if the collection is empty. + * + * @return bool True if the collection has no items + */ public function empty(): bool { return $this->items === []; } /** - * @return array + * Converts the collection to an indexed array. + * + * @return array Array of collection items */ public function toArray(): array { diff --git a/src/Module/Repository/ReleaseInterface.php b/src/Module/Repository/ReleaseInterface.php index 1c29fe8..f0c0b84 100644 --- a/src/Module/Repository/ReleaseInterface.php +++ b/src/Module/Repository/ReleaseInterface.php @@ -7,29 +7,61 @@ use Internal\DLoad\Module\Common\Stability; use Internal\DLoad\Module\Repository\Collection\AssetsCollection; +/** + * Represents a single release of software from a repository. + * + * A release contains information about its version, stability and associated assets + * that can be downloaded. + */ interface ReleaseInterface { + /** + * Returns the repository this release belongs to. + * + * @return RepositoryInterface The parent repository + */ public function getRepository(): RepositoryInterface; /** * Returns Composer's compatible "pretty" release version. * - * @return non-empty-string + * This version is formatted for semantic versioning compatibility. + * + * @return non-empty-string Formatted version string (e.g. "1.2.3") */ public function getName(): string; /** * Returns internal release tag version. * - * @note this version may not be compatible with Composer's comparators. + * This version string may include prefixes or suffixes that aren't + * compatible with Composer's comparators. * - * @return non-empty-string + * @note This version may not be compatible with Composer's comparators + * + * @return non-empty-string Raw version string (e.g. "v1.2.3-beta") */ public function getVersion(): string; + /** + * Returns the stability level of this release. + * + * @return Stability Enum representing the stability level (Stable, RC, Beta, etc.) + */ public function getStability(): Stability; + /** + * Returns all assets associated with this release. + * + * @return AssetsCollection Collection of downloadable assets + */ public function getAssets(): AssetsCollection; + /** + * Checks if this release satisfies the given version constraint. + * + * @param string $constraint Version constraint in Composer format (e.g. "^1.0", ">2.5") + * @return bool True if the release satisfies the constraint + */ public function satisfies(string $constraint): bool; } diff --git a/src/Module/Repository/RepositoryInterface.php b/src/Module/Repository/RepositoryInterface.php index 7dc0a98..756b31d 100644 --- a/src/Module/Repository/RepositoryInterface.php +++ b/src/Module/Repository/RepositoryInterface.php @@ -6,12 +6,25 @@ use Internal\DLoad\Module\Repository\Collection\ReleasesCollection; +/** + * Represents a software repository that contains releases. + * + * This interface defines the contract for accessing repository information + * and retrieving all available releases. + */ interface RepositoryInterface { /** - * @return non-empty-string + * Returns the unique identifier of the repository. + * + * @return non-empty-string Repository identifier, typically in vendor/package format */ public function getName(): string; + /** + * Retrieves all available releases from this repository. + * + * @return ReleasesCollection Collection of releases available in this repository + */ public function getReleases(): ReleasesCollection; } diff --git a/src/Module/Repository/RepositoryProvider.php b/src/Module/Repository/RepositoryProvider.php index 78d51bf..a82890e 100644 --- a/src/Module/Repository/RepositoryProvider.php +++ b/src/Module/Repository/RepositoryProvider.php @@ -8,14 +8,38 @@ use Internal\DLoad\Module\Repository\Internal\GitHub\Factory as GithubFactory; /** + * Factory service for creating repository instances from configuration. + * + * Handles the creation of appropriate repository implementations based on + * the repository type specified in the configuration. + * + * ```php + * // Get the RepositoryProvider service + * $provider = $container->get(RepositoryProvider::class); + * + * // Create a repository instance from configuration + * $config = new Repository('github', 'vendor/package'); + * $repository = $provider->getByConfig($config); + * ``` + * * @internal */ final class RepositoryProvider { + /** + * @param GithubFactory $githubFactory Factory for creating GitHub repository instances + */ public function __construct( private readonly GithubFactory $githubFactory, ) {} + /** + * Creates a repository instance based on the provided configuration. + * + * @param Repository $config Repository configuration + * @return RepositoryInterface Created repository instance + * @throws \RuntimeException When an unknown repository type is specified + */ public function getByConfig(Repository $config): RepositoryInterface { return match (\strtolower($config->type)) { From 640107ffb3205d397626d8d77f99084073acc43e Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sat, 5 Apr 2025 18:12:58 +0400 Subject: [PATCH 11/30] docs: improve docs for all the existing classes --- src/Bootstrap.php | 47 ++++++++++++++++- src/Command/Get.php | 6 --- src/Command/ListSoftware.php | 6 --- src/DLoad.php | 50 +++++++++++++++++-- src/Module/Repository/AssetInterface.php | 15 +++++- src/Module/Repository/ReleaseInterface.php | 18 +++++-- src/Module/Repository/RepositoryInterface.php | 10 +++- src/Service/Container.php | 17 +++++-- src/Service/Factoriable.php | 15 +++--- 9 files changed, 149 insertions(+), 35 deletions(-) diff --git a/src/Bootstrap.php b/src/Bootstrap.php index 511e84d..ba8964f 100644 --- a/src/Bootstrap.php +++ b/src/Bootstrap.php @@ -12,7 +12,20 @@ use Internal\DLoad\Service\Container; /** - * Build the container based on the configuration. + * Bootstraps the application by configuring the dependency container. + * + * Initializes the application container with configuration values and core services. + * Serves as the entry point for the dependency injection setup. + * + * ```php + * // Initialize application with default container and XML config + * $container = Bootstrap::init() + * ->withConfig('dload.xml', $inputOptions, $inputArguments) + * ->finish(); + * + * // Use container to access services + * $downloader = $container->get(Downloader::class); + * ``` * * @internal */ @@ -22,11 +35,22 @@ private function __construct( private Container $container, ) {} + /** + * Creates a new bootstrap instance with the specified container. + * + * @param Container $container Dependency injection container (defaults to ObjectContainer) + * @return self Bootstrap instance + */ public static function init(Container $container = new ObjectContainer()): self { return new self($container); } + /** + * Finalizes the bootstrap process and returns the configured container. + * + * @return Container Fully configured dependency container + */ public function finish(): Container { $c = $this->container; @@ -36,7 +60,18 @@ public function finish(): Container } /** - * @param non-empty-string|null $xml File or XML content + * Configures the container with XML configuration and input values. + * + * Registers core services and bindings for system architecture, OS detection, + * and stability settings. + * + * @param non-empty-string|null $xml Path to XML file or raw XML content + * @param array $inputOptions Command-line options + * @param array $inputArguments Command-line arguments + * @param array $environment Environment variables + * @return self Configured bootstrap instance + * @throws \InvalidArgumentException When config file is not found + * @throws \RuntimeException When config file cannot be read */ public function withConfig( ?string $xml = null, @@ -62,6 +97,14 @@ public function withConfig( return $this; } + /** + * Reads XML configuration from file or direct content string. + * + * @param non-empty-string $fileOrContent Path to XML file or raw XML content + * @return string Parsed XML content + * @throws \InvalidArgumentException When config file is not found + * @throws \RuntimeException When config file cannot be read + */ private function readXml(string $fileOrContent): string { // Load content diff --git a/src/Command/Get.php b/src/Command/Get.php index 3f436fa..63c0e59 100644 --- a/src/Command/Get.php +++ b/src/Command/Get.php @@ -24,12 +24,6 @@ * system/architecture detection. Can work with both CLI arguments and * configuration file definitions. * - * ```php - * // Download software programmatically - * $command = new Get(); - * $command->run(new ArrayInput(['software' => 'rr']), new ConsoleOutput()); - * ``` - * * ```bash * # Download single software * ./vendor/bin/dload get rr diff --git a/src/Command/ListSoftware.php b/src/Command/ListSoftware.php index 27aa665..275b79d 100644 --- a/src/Command/ListSoftware.php +++ b/src/Command/ListSoftware.php @@ -17,12 +17,6 @@ * Shows all registered software packages with their IDs, names, * repository information, and descriptions. * - * ```php - * // List software programmatically - * $command = new ListSoftware(); - * $command->run(new ArrayInput([]), new ConsoleOutput()); - * ``` - * * ```bash * # List all available software packages * ./vendor/bin/dload software diff --git a/src/DLoad.php b/src/DLoad.php index 06c611b..cf48c24 100644 --- a/src/DLoad.php +++ b/src/DLoad.php @@ -22,12 +22,22 @@ use function React\Promise\resolve; /** - * To have a short syntax. + * Main application facade providing simplified access to download functionality. + * + * Acts as a high-level interface for downloading and extracting software packages + * based on configuration actions. + * + * ```php + * $dload = $container->get(DLoad::class); + * $dload->addTask(new DownloadConfig('rr', '^2.12.0')); + * $dload->run(); + * ``` * * @internal */ final class DLoad { + /** @var bool Flag to use mock data instead of actual downloads for testing */ public bool $useMock = false; public function __construct( @@ -41,6 +51,14 @@ public function __construct( private readonly StyleInterface $io, ) {} + /** + * Adds a download task to the execution queue. + * + * Creates and schedules a task to download and extract a software package based on the provided action. + * + * @param DownloadConfig $action Download configuration action + * @throws \RuntimeException When software package is not found + */ public function addTask(DownloadConfig $action): void { $this->taskManager->addTask(function () use ($action): void { @@ -57,11 +75,25 @@ public function addTask(DownloadConfig $action): void }); } + /** + * Executes all queued download tasks. + * + * Processes all scheduled tasks sequentially until completion. + */ public function run(): void { $this->taskManager->await(); } + /** + * Creates a download task for the specified software package. + * + * Either uses a mock task (for testing) or creates a real download task. + * + * @param Software $software Software package configuration + * @param DownloadConfig $action Download action configuration + * @return DownloadTask Task object for downloading the specified software + */ private function prepareDownloadTask(Software $software, DownloadConfig $action): DownloadTask { return $this->useMock @@ -79,7 +111,10 @@ private function prepareDownloadTask(Software $software, DownloadConfig $action) } /** - * @return \Closure(DownloadResult): void + * Creates a closure to handle extraction of downloaded files. + * + * @param Software $software Software package configuration + * @return \Closure(DownloadResult): void Function that extracts files from the downloaded archive */ private function prepareExtractTask(Software $software): \Closure { @@ -117,7 +152,10 @@ private function prepareExtractTask(Software $software): \Closure } /** - * @return bool True if the file should be extracted, false otherwise. + * Checks if a file already exists and prompts for confirmation to overwrite. + * + * @param \SplFileInfo $bin Target file information + * @return bool True if the file should be extracted, false otherwise */ private function checkExisting(\SplFileInfo $bin): bool { @@ -133,7 +171,11 @@ private function checkExisting(\SplFileInfo $bin): bool } /** - * @param list $mapping + * Determines the target path for an extracted file based on file mapping configurations. + * + * @param \SplFileInfo $source Source file from the archive + * @param list $mapping File mapping configurations + * @return \SplFileInfo|null Target file path or null if file should not be extracted */ private function shouldBeExtracted(\SplFileInfo $source, array $mapping): ?\SplFileInfo { diff --git a/src/Module/Repository/AssetInterface.php b/src/Module/Repository/AssetInterface.php index 1f99f97..c663ae0 100644 --- a/src/Module/Repository/AssetInterface.php +++ b/src/Module/Repository/AssetInterface.php @@ -11,7 +11,20 @@ * Represents a downloadable asset from a software release. * * Assets are individual binary files or archives associated with a specific release, - * often targeting particular operating systems or architectures. + * often targeting particular operating systems or architectures. They represent + * the actual files that will be downloaded and extracted by the application. + * + * ```php + * $assets = $release->getAssets() + * ->whereArchitecture($architecture) + * ->whereOperatingSystem($operatingSystem) + * ->whereNameMatches($pattern); + * + * foreach ($assets as $asset) { + * $chunks = $asset->download(); + * // Process downloaded chunks + * } + * ``` */ interface AssetInterface { diff --git a/src/Module/Repository/ReleaseInterface.php b/src/Module/Repository/ReleaseInterface.php index f0c0b84..7cfa6f3 100644 --- a/src/Module/Repository/ReleaseInterface.php +++ b/src/Module/Repository/ReleaseInterface.php @@ -11,7 +11,18 @@ * Represents a single release of software from a repository. * * A release contains information about its version, stability and associated assets - * that can be downloaded. + * that can be downloaded. Release objects provide access to all distributable files + * for a particular software version. + * + * ```php + * $releases = $repository->getReleases() + * ->minimumStability(Stability::Stable) + * ->satisfies('^2.0.0') + * ->sortByVersion(); + * + * $latestRelease = $releases->first(); + * $assets = $latestRelease->getAssets(); + * ``` */ interface ReleaseInterface { @@ -37,8 +48,6 @@ public function getName(): string; * This version string may include prefixes or suffixes that aren't * compatible with Composer's comparators. * - * @note This version may not be compatible with Composer's comparators - * * @return non-empty-string Raw version string (e.g. "v1.2.3-beta") */ public function getVersion(): string; @@ -60,6 +69,9 @@ public function getAssets(): AssetsCollection; /** * Checks if this release satisfies the given version constraint. * + * Uses Composer's version comparison logic to determine if this release + * satisfies the specified constraint. + * * @param string $constraint Version constraint in Composer format (e.g. "^1.0", ">2.5") * @return bool True if the release satisfies the constraint */ diff --git a/src/Module/Repository/RepositoryInterface.php b/src/Module/Repository/RepositoryInterface.php index 756b31d..de72362 100644 --- a/src/Module/Repository/RepositoryInterface.php +++ b/src/Module/Repository/RepositoryInterface.php @@ -9,8 +9,14 @@ /** * Represents a software repository that contains releases. * - * This interface defines the contract for accessing repository information - * and retrieving all available releases. + * Defines the contract for accessing repository information and retrieving all available releases. + * Repositories act as the primary source for software packages that can be downloaded. + * + * ```php + * $repository = $repositoryProvider->getByConfig($repoConfig); + * $releases = $repository->getReleases(); + * $filteredReleases = $releases->satisfies('^2.0.0')->sortByVersion(); + * ``` */ interface RepositoryInterface { diff --git a/src/Service/Container.php b/src/Service/Container.php index 08cc503..f39b318 100644 --- a/src/Service/Container.php +++ b/src/Service/Container.php @@ -7,11 +7,18 @@ /** * Application dependency injection container. * - * Manages service instances throughout the application lifecycle. + * Manages service instances throughout the application lifecycle. Services are lazy-loaded + * and cached for reuse. The container handles dependencies between services and + * provides a way to customize service instantiation through bindings. * * ```php * // Retrieving a service instance * $downloader = $container->get(Downloader::class); + * + * // Binding a factory for service creation + * $container->bind(Logger::class, function (Container $c) { + * return new Logger($c->get(OutputInterface::class)); + * }); * ``` * * @internal @@ -25,7 +32,7 @@ interface Container extends Destroyable * * @template T * @param class-string $id Service identifier - * @param array $arguments Constructor arguments used only on first instantiation + * @param array $arguments Constructor arguments used only on first instantiation * @return T The requested service instance * * @psalm-suppress MoreSpecificImplementedParamType, InvalidReturnType @@ -58,7 +65,7 @@ public function set(object $service, ?string $id = null): void; * * @template T * @param class-string $class Class to instantiate - * @param array $arguments Constructor arguments + * @param array $arguments Constructor arguments * @return T Newly created instance */ public function make(string $class, array $arguments = []): object; @@ -68,9 +75,9 @@ public function make(string $class, array $arguments = []): object; * * Configures how a service should be instantiated. * - * @template T of object + * @template T * @param class-string $id Service identifier - * @param null|array|\Closure(Container): T $binding Factory function or constructor arguments + * @param null|array|\Closure(Container): T $binding Factory function or constructor arguments */ public function bind(string $id, \Closure|array|null $binding = null): void; } diff --git a/src/Service/Factoriable.php b/src/Service/Factoriable.php index bd07024..085de08 100644 --- a/src/Service/Factoriable.php +++ b/src/Service/Factoriable.php @@ -7,24 +7,27 @@ /** * Interface for classes that can create instances of themselves. * - * Implementing classes should provide a static factory method for instantiation. + * Implementing classes should provide the static factory method `create()` for instantiation. + * This pattern is useful for objects that require complex initialization logic or dependency resolution. * * ```php * class Config implements Factoriable * { * private function __construct( - * private string $path + * private Logger $logger, * ) {} * - * public static function create(string $path): self + * public static function create(Logger $logger): self * { - * return new self($path); + * return new self($logger); * } * } + * + * $container->get(Config::class); // Will be created via the `create()` method with autowiring * ``` * - * @method static create - * Method creates new instance of the class. May contain any injectable parameters. + * @method static self create + * Method creates new instance of the class with injectable parameters * * @internal */ From 0db310962f74a546b94298a814ab0d8bae5cf036 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sat, 5 Apr 2025 18:31:17 +0400 Subject: [PATCH 12/30] chore(ctx): add modules API context --- context.yaml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/context.yaml b/context.yaml index 1e843e1..f5f117b 100644 --- a/context.yaml +++ b/context.yaml @@ -18,3 +18,19 @@ documents: showSize: true - type: file sourcePaths: ['README.md'] + + # Modules API + - description: 'Modules API allowed to be used in the project' + outputPath: modules-api.md + overwrite: true + sources: + - type: file + sourcePaths: ['src/Module', 'src/Service'] + filePattern: '*.php' + excludePatterns: ['/Internal/'] + modifiers: + - name: php-content-filter + options: + method_visibility: + - public + keep_method_bodies: false From 24f2804679a08fb133e7b86921fa2d20dd7d7a49 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sun, 6 Apr 2025 12:52:15 +0400 Subject: [PATCH 13/30] maintenance: update dependencies; remove pest, use phpunit instead; remove phpstan; update CI actions --- .gitattributes | 3 - .github/workflows/apply-labels.yml | 2 +- .github/workflows/build-phar-release.yml | 37 +- .github/workflows/coding-standards.yml | 140 +- .github/workflows/security.yml | 21 +- .github/workflows/static-analysis.yml | 23 +- .github/workflows/testing.yml | 95 +- .php-cs-fixer.dist.php | 22 +- Makefile | 4 +- bin/dload | 4 - box.json.dist | 2 + composer.json | 22 +- composer.lock | 1994 ++++++++++------- pest.xml.dist | 30 - phpunit.xml.dist | 13 +- psalm-baseline.xml | 125 +- psalm.xml | 13 + src/Info.php | 5 +- src/Module/Archive/ArchiveFactory.php | 1 - .../Archive/Exception/ArchiveException.php | 2 +- src/Module/Archive/Internal/PharArchive.php | 1 - .../Archive/Internal/PharAwareArchive.php | 1 - .../Archive/Internal/TarPharArchive.php | 1 - .../Archive/Internal/ZipPharArchive.php | 1 - .../Internal/Injection/ConfigLoader.php | 1 - .../Collection/CompositeRepository.php | 2 +- src/Module/Repository/Internal/Collection.php | 4 +- .../Internal/GitHub/GitHubAsset.php | 2 +- .../Internal/GitHub/GitHubRelease.php | 1 - .../Internal/GitHub/GitHubRepository.php | 10 +- src/Module/Repository/Internal/Release.php | 2 - tests/Arch/ArchTest.php | 43 + tests/Arch/DebugTest.php | 8 - 33 files changed, 1439 insertions(+), 1196 deletions(-) delete mode 100644 pest.xml.dist create mode 100644 tests/Arch/ArchTest.php delete mode 100644 tests/Arch/DebugTest.php diff --git a/.gitattributes b/.gitattributes index 56f46df..1f2ca4f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -10,9 +10,6 @@ composer-require-* export-ignore docker-compose.yaml export-ignore Makefile export-ignore phpunit.xml* export-ignore -pest.xml* export-ignore -phpstan.* export-ignore -phpstan-baseline.neon export-ignore psalm.* export-ignore psalm-baseline.xml export-ignore infection.* export-ignore diff --git a/.github/workflows/apply-labels.yml b/.github/workflows/apply-labels.yml index f2b6f59..d40e673 100644 --- a/.github/workflows/apply-labels.yml +++ b/.github/workflows/apply-labels.yml @@ -14,7 +14,7 @@ name: 🏷️ Add labels jobs: label: - uses: wayofdev/gh-actions/.github/workflows/apply-labels.yml@v3.1.0 + uses: wayofdev/gh-actions/.github/workflows/apply-labels.yml@v3.2.0 with: os: ubuntu-latest secrets: diff --git a/.github/workflows/build-phar-release.yml b/.github/workflows/build-phar-release.yml index 956feff..a75e403 100644 --- a/.github/workflows/build-phar-release.yml +++ b/.github/workflows/build-phar-release.yml @@ -24,16 +24,15 @@ jobs: GPG_KEYS_ENCRYPTED: ".github/phar/keys.asc.gpg" steps: - name: 📦 Check out the codebase - uses: actions/checkout@v4.1.5 + uses: actions/checkout@v4 - name: 🛠️ Setup PHP - uses: shivammathur/setup-php@2.30.4 + uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-version }} - extensions: none, ctype, dom, json, mbstring, phar, simplexml, tokenizer, xml, xmlwriter, sockets ini-values: error_reporting=E_ALL coverage: none - tools: phive + tools: composer, box - name: 🛠️ Setup problem matchers run: | @@ -42,35 +41,19 @@ jobs: - name: 🤖 Validate composer.json and composer.lock run: composer validate --ansi --strict - - name: 🔍 Get composer cache directory - uses: wayofdev/gh-actions/actions/composer/get-cache-directory@v3.1.0 - - - name: ♻️ Restore cached dependencies installed with composer - uses: actions/cache@v4.0.2 - with: - path: ${{ env.COMPOSER_CACHE_DIR }} - key: php-${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-${{ hashFiles('composer.lock') }} - restore-keys: php-${{ matrix.php-version }}-composer-${{ matrix.dependencies }}- - - - name: 📥 Install "${{ matrix.dependencies }}" dependencies with composer - uses: wayofdev/gh-actions/actions/composer/install@v3.1.0 - with: - dependencies: ${{ matrix.dependencies }} - - - name: 📥 Install dependencies with phive - uses: wayofdev/gh-actions/actions/phive/install@v3.1.0 + - name: 📥 Install dependencies with composer + uses: ramsey/composer-install@v3 with: - phive-home: '.phive' - trust-gpg-keys: '0xC00543248C87FB13,0x033E5F8D801A2F8D,0x2DF45277AEF09A2F' + composer-options: "--no-dev" - name: 🔍 Validate configuration for box-project/box - run: .phive/box validate box.json.dist --ansi + run: box info validate box.json.dist --ansi - - name: 🤖 Compile dload.phar with box-project/box - run: .phive/box compile --ansi + - name: 📦 Build PHAR + run: box compile - name: 💥 Show info about dload.phar with box-project/box - run: .phive/box info ${{ env.DLOAD_PHAR }} --ansi + run: box info info ${{ env.DLOAD_PHAR }} --ansi - name: 🤔 Run dload.phar help command run: ${{ env.DLOAD_PHAR }} --help diff --git a/.github/workflows/coding-standards.yml b/.github/workflows/coding-standards.yml index c47afa0..11fcb2c 100644 --- a/.github/workflows/coding-standards.yml +++ b/.github/workflows/coding-standards.yml @@ -19,7 +19,7 @@ jobs: pull-requests: read steps: - name: 📦 Check out the codebase - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4 - name: 🧐 Lint commits using "commitlint" uses: wagoid/commitlint-github-action@v6.0.1 @@ -37,10 +37,10 @@ jobs: pull-requests: read steps: - name: 📦 Check out the codebase - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4 - name: 🧐 Lint YAML files - uses: ibiqlik/action-yamllint@v3.1.1 + uses: ibiqlik/action-yamllint@v3 with: config_file: .github/.yamllint.yaml file_or_dir: '.' @@ -54,143 +54,17 @@ jobs: group: markdown-linting-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} steps: - name: 📦 Check out the codebase - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4 - name: 🧐 Lint Markdown files uses: DavidAnson/markdownlint-cli2-action@v16.0.0 with: globs: | - **/*.md + *.md + docs/**/*.md !CHANGELOG.md - composer-linting: - timeout-minutes: 4 - runs-on: ${{ matrix.os }} - concurrency: - cancel-in-progress: true - group: composer-linting-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - strategy: - matrix: - os: - - ubuntu-latest - php-version: - - '8.1' - dependencies: - - locked - permissions: - contents: write - steps: - - name: 🛠️ Setup PHP - uses: shivammathur/setup-php@2.30.4 - with: - php-version: ${{ matrix.php-version }} - extensions: none, ctype, dom, json, mbstring, phar, simplexml, tokenizer, xml, xmlwriter, phar, sockets - ini-values: error_reporting=E_ALL - coverage: none - tools: phive - - - name: 📦 Check out the codebase - uses: actions/checkout@v4.1.6 - - - name: 🛠️ Setup problem matchers - run: | - echo "::add-matcher::${{ runner.tool_cache }}/php.json" - - - name: 🤖 Validate composer.json and composer.lock - run: composer validate --ansi --strict - - - name: 🔍 Get composer cache directory - uses: wayofdev/gh-actions/actions/composer/get-cache-directory@v3.1.0 - - - name: ♻️ Restore cached dependencies installed with composer - uses: actions/cache@v4.0.2 - with: - path: ${{ env.COMPOSER_CACHE_DIR }} - key: php-${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-${{ hashFiles('composer.lock') }} - restore-keys: php-${{ matrix.php-version }}-composer-${{ matrix.dependencies }}- - - - name: 📥 Install "${{ matrix.dependencies }}" dependencies with composer - uses: wayofdev/gh-actions/actions/composer/install@v3.1.0 - with: - dependencies: ${{ matrix.dependencies }} - - - name: 📥 Install dependencies with phive - uses: wayofdev/gh-actions/actions/phive/install@v3.1.0 - with: - phive-home: '.phive' - trust-gpg-keys: '0xC00543248C87FB13,0x033E5F8D801A2F8D,0x2DF45277AEF09A2F' - - - name: 🔍 Run ergebnis/composer-normalize - run: .phive/composer-normalize --ansi --dry-run - coding-standards: - timeout-minutes: 4 - runs-on: ${{ matrix.os }} - concurrency: - cancel-in-progress: true - group: coding-standards-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - strategy: - matrix: - os: - - ubuntu-latest - php-version: - - '8.1' - dependencies: - - locked permissions: contents: write - steps: - - name: ⚙️ Set git to use LF line endings - run: | - git config --global core.autocrlf false - git config --global core.eol lf - - - name: 🛠️ Setup PHP - uses: shivammathur/setup-php@2.30.4 - with: - php-version: ${{ matrix.php-version }} - extensions: none, ctype, dom, json, mbstring, phar, simplexml, tokenizer, xml, xmlwriter, sockets - ini-values: error_reporting=E_ALL - coverage: none - - - name: 📦 Check out the codebase - uses: actions/checkout@v4.1.6 - - - name: 🛠️ Setup problem matchers - run: | - echo "::add-matcher::${{ runner.tool_cache }}/php.json" - - - name: 🤖 Validate composer.json and composer.lock - run: composer validate --ansi --strict - - - name: 🔍 Get composer cache directory - uses: wayofdev/gh-actions/actions/composer/get-cache-directory@v3.1.0 - - - name: ♻️ Restore cached dependencies installed with composer - uses: actions/cache@v4.0.2 - with: - path: ${{ env.COMPOSER_CACHE_DIR }} - key: php-${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-${{ hashFiles('composer.lock') }} - restore-keys: php-${{ matrix.php-version }}-composer-${{ matrix.dependencies }}- - - - name: 📥 Install "${{ matrix.dependencies }}" dependencies with composer - uses: wayofdev/gh-actions/actions/composer/install@v3.1.0 - with: - dependencies: ${{ matrix.dependencies }} - - - name: 🛠️ Prepare environment - run: make prepare - - - name: 🚨 Run coding standards task - run: composer cs:fix - env: - PHP_CS_FIXER_IGNORE_ENV: true - - - name: 📤 Commit and push changed files back to GitHub - uses: stefanzweifel/git-auto-commit-action@v5.0.1 - with: - commit_message: 'style(php-cs-fixer): lint php files and fix coding standards' - branch: ${{ github.head_ref }} - commit_author: 'github-actions ' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: spiral/gh-actions/.github/workflows/cs-fix.yml@master diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 7c92c9d..fe984dc 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -24,13 +24,12 @@ jobs: - locked steps: - name: 📦 Check out the codebase - uses: actions/checkout@v4.1.5 + uses: actions/checkout@v4 - name: 🛠️ Setup PHP - uses: shivammathur/setup-php@2.30.4 + uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-version }} - extensions: none, ctype, dom, json, mbstring, phar, simplexml, tokenizer, xml, xmlwriter, sockets ini-values: error_reporting=E_ALL coverage: none @@ -40,20 +39,10 @@ jobs: - name: 🤖 Validate composer.json and composer.lock run: composer validate --ansi --strict - - name: 🔍 Get composer cache directory - uses: wayofdev/gh-actions/actions/composer/get-cache-directory@v3.1.0 - - - name: ♻️ Restore cached dependencies installed with composer - uses: actions/cache@v4.0.2 - with: - path: ${{ env.COMPOSER_CACHE_DIR }} - key: php-${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-${{ hashFiles('composer.lock') }} - restore-keys: php-${{ matrix.php-version }}-composer-${{ matrix.dependencies }}- - - - name: 📥 Install "${{ matrix.dependencies }}" dependencies - uses: wayofdev/gh-actions/actions/composer/install@v3.1.0 + - name: 📥 Install dependencies with composer + uses: ramsey/composer-install@v3 with: - dependencies: ${{ matrix.dependencies }} + dependency-versions: ${{ matrix.dependencies }} - name: 🐛 Check installed packages for security vulnerability advisories run: composer audit --ansi diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 4c9dcfb..a064654 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -7,7 +7,6 @@ on: # yamllint disable-line rule:truthy - 'bin/dload' - '.php-cs-fixer.dist.php' - 'psalm*' - - 'phpstan*' - 'composer.*' push: paths: @@ -15,7 +14,6 @@ on: # yamllint disable-line rule:truthy - 'bin/dload' - '.php-cs-fixer.dist.php' - 'psalm*' - - 'phpstan*' - 'composer.*' name: 🔍 Static analysis @@ -38,13 +36,12 @@ jobs: - locked steps: - name: 📦 Check out the codebase - uses: actions/checkout@v4.1.5 + uses: actions/checkout@v4 - name: 🛠️ Setup PHP - uses: shivammathur/setup-php@2.30.4 + uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-version }} - extensions: none, ctype, curl, dom, json, mbstring, phar, simplexml, tokenizer, xml, xmlwriter, sockets, opcache, pcntl, posix ini-values: error_reporting=E_ALL coverage: none @@ -54,20 +51,10 @@ jobs: - name: 🤖 Validate composer.json and composer.lock run: composer validate --ansi --strict - - name: 🔍 Get composer cache directory - uses: wayofdev/gh-actions/actions/composer/get-cache-directory@v3.1.0 - - - name: ♻️ Restore cached dependencies installed with composer - uses: actions/cache@v4.0.2 - with: - path: ${{ env.COMPOSER_CACHE_DIR }} - key: php-${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-${{ hashFiles('composer.lock') }} - restore-keys: php-${{ matrix.php-version }}-composer-${{ matrix.dependencies }}- - - - name: 📥 Install "${{ matrix.dependencies }}" dependencies - uses: wayofdev/gh-actions/actions/composer/install@v3.1.0 + - name: 📥 Install dependencies with composer + uses: ramsey/composer-install@v3 with: - dependencies: ${{ matrix.dependencies }} + dependency-versions: ${{ matrix.dependencies }} - name: 🔍 Run static analysis using vimeo/psalm run: composer psalm:ci diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 1d42212..09ef77f 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -28,13 +28,12 @@ jobs: - locked steps: - name: 📦 Check out the codebase - uses: actions/checkout@v4.1.5 + uses: actions/checkout@v4 - name: 🛠️ Setup PHP - uses: shivammathur/setup-php@2.30.4 + uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-version }} - extensions: none, ctype, curl, dom, json, mbstring, phar, simplexml, tokenizer, xml, xmlwriter, sockets, opcache, pcntl, posix ini-values: error_reporting=E_ALL coverage: xdebug @@ -46,28 +45,18 @@ jobs: - name: 🤖 Validate composer.json and composer.lock run: composer validate --ansi --strict - - name: 🔍 Get composer cache directory - uses: wayofdev/gh-actions/actions/composer/get-cache-directory@v3.1.0 - - - name: ♻️ Restore cached dependencies installed with composer - uses: actions/cache@v4.0.2 - with: - path: ${{ env.COMPOSER_CACHE_DIR }} - key: php-${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-${{ hashFiles('composer.lock') }} - restore-keys: php-${{ matrix.php-version }}-composer-${{ matrix.dependencies }}- - - - name: 📥 Install "${{ matrix.dependencies }}" dependencies - uses: wayofdev/gh-actions/actions/composer/install@v3.1.0 + - name: 📥 Install dependencies with composer + uses: ramsey/composer-install@v3 with: - dependencies: ${{ matrix.dependencies }} + dependency-versions: ${{ matrix.dependencies }} - - name: 🧪 Collect code coverage with Xdebug and pestphp/pest + - name: 🧪 Collect code coverage with Xdebug and PhpUnit run: composer test:cc - name: 📤 Upload code coverage report to Codecov - uses: codecov/codecov-action@v4.3.0 + uses: codecov/codecov-action@v4 with: - files: .build/phpunit/logs/clover.xml + files: runtime/phpunit/logs/clover.xml token: ${{ secrets.CODECOV_TOKEN }} verbose: true @@ -92,13 +81,12 @@ jobs: - highest steps: - name: 📦 Check out the codebase - uses: actions/checkout@v4.1.5 + uses: actions/checkout@v4 - name: 🛠️ Setup PHP - uses: shivammathur/setup-php@2.30.4 + uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-version }} - extensions: none, ctype, curl, dom, json, mbstring, phar, simplexml, tokenizer, xml, xmlwriter, sockets, opcache, pcntl, posix ini-values: error_reporting=E_ALL coverage: xdebug @@ -110,31 +98,14 @@ jobs: - name: 🤖 Validate composer.json and composer.lock run: composer validate --ansi --strict - - name: 🔍 Get composer cache directory - uses: wayofdev/gh-actions/actions/composer/get-cache-directory@v3.1.0 - - - name: ♻️ Restore cached dependencies installed with composer - uses: actions/cache@v4.0.2 - with: - path: ${{ env.COMPOSER_CACHE_DIR }} - key: php-${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-${{ hashFiles('composer.lock') }} - restore-keys: php-${{ matrix.php-version }}-composer-${{ matrix.dependencies }}- - - - name: ⚙️ Remove platform configuration with composer - if: matrix.dependencies != 'locked' - run: composer config platform.php --ansi --unset - - - name: 📥 Install "${{ matrix.dependencies }}" dependencies - uses: wayofdev/gh-actions/actions/composer/install@v3.1.0 + - name: 📥 Install dependencies with composer + uses: ramsey/composer-install@v3 with: - dependencies: ${{ matrix.dependencies }} + dependency-versions: ${{ matrix.dependencies }} - - name: 🧪 Run unit tests using phpunit/phpunit + - name: 🧪 Run tests run: composer test - - name: 🧪 Run arch tests using pestphp/pest - run: composer test:arch - compile-phar: timeout-minutes: 4 runs-on: ${{ matrix.os }} @@ -155,52 +126,36 @@ jobs: DLOAD_PHAR_SIGNATURE: ".build/phar/dload.phar.asc" steps: - name: 📦 Check out the codebase - uses: actions/checkout@v4.1.5 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: 🛠️ Setup PHP - uses: shivammathur/setup-php@2.30.4 + uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-version }} - extensions: none, ctype, dom, json, mbstring, phar, simplexml, tokenizer, xml, xmlwriter, sockets ini-values: 'error_reporting=E_ALL, memory_limit=-1, phar.readonly=0' + tools: composer, box coverage: none - tools: phive - name: 🛠️ Setup problem matchers run: | echo "::add-matcher::${{ runner.tool_cache }}/php.json" - - name: 🔍 Get composer cache directory - uses: wayofdev/gh-actions/actions/composer/get-cache-directory@v3.1.0 - - - name: ♻️ Restore cached dependencies installed with composer - uses: actions/cache@v4.0.2 - with: - path: ${{ env.COMPOSER_CACHE_DIR }} - key: php-${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-${{ hashFiles('composer.lock') }} - restore-keys: php-${{ matrix.php-version }}-composer-${{ matrix.dependencies }}- - - - name: 📥 Install "${{ matrix.dependencies }}" dependencies with composer - uses: wayofdev/gh-actions/actions/composer/install@v3.1.0 - with: - dependencies: ${{ matrix.dependencies }} - - - name: 📥 Install dependencies with phive - uses: wayofdev/gh-actions/actions/phive/install@v3.1.0 + - name: 📥 Install dependencies with composer + uses: ramsey/composer-install@v3 with: - phive-home: '.phive' - trust-gpg-keys: '0xC00543248C87FB13,0x033E5F8D801A2F8D,0x2DF45277AEF09A2F' + composer-options: "--no-dev" + dependency-versions: ${{ matrix.dependencies }} - name: 🔍 Validate configuration for box-project/box - run: .phive/box validate box.json.dist --ansi + run: box validate box.json.dist --ansi - - name: 🤖 Compile dload.phar with box-project/box - run: .phive/box compile --ansi + - name: 📦 Build PHAR + run: box compile - name: 💥 Show info about dload.phar with box-project/box - run: .phive/box info ${{ env.DLOAD_PHAR }} --ansi + run: box info ${{ env.DLOAD_PHAR }} --ansi - name: 🤔 Run dload.phar help command run: ${{ env.DLOAD_PHAR }} --help diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 56f7620..5200938 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -2,21 +2,11 @@ declare(strict_types=1); -use WayOfDev\PhpCsFixer\Config\ConfigBuilder; -use WayOfDev\PhpCsFixer\Config\RuleSets\ExtendedPERSet; - require_once 'vendor/autoload.php'; -$config = ConfigBuilder::createFromRuleSet(new ExtendedPERSet()) - ->inDir(__DIR__ . '/bin') - ->inDir(__DIR__ . '/src') - ->inDir(__DIR__ . '/tests') - ->exclude([ - __DIR__ . '/src/Test/Proto', - ]) - ->addFiles([__FILE__]) - ->getConfig(); - -$config->setCacheFile(__DIR__ . '/.build/php-cs-fixer/php-cs-fixer.cache'); - -return $config; +return \Spiral\CodeStyle\Builder::create() + ->include(__DIR__ . '/bin') + ->include(__DIR__ . '/src') + ->include(__DIR__ . '/tests') + ->include(__FILE__) + ->build(); diff --git a/Makefile b/Makefile index c873bfa..12ba0ca 100644 --- a/Makefile +++ b/Makefile @@ -264,11 +264,11 @@ infect-ci: ## Runs infection – mutation testing framework with github output ( $(APP_COMPOSER) infect:ci .PHONY: lint-infect-ci -test: ## Run project php-unit and pest tests +test: ## Run project tests $(APP_COMPOSER) test .PHONY: test -test-cc: ## Run project php-unit and pest tests in coverage mode and build report +test-cc: ## Run project tests in coverage mode and build report $(APP_COMPOSER) test:cc .PHONY: test-cc diff --git a/bin/dload b/bin/dload index 8378262..f6c9b5c 100644 --- a/bin/dload +++ b/bin/dload @@ -8,10 +8,6 @@ use Internal\DLoad\Info; use Symfony\Component\Console\Application; use Symfony\Component\Console\CommandLoader\FactoryCommandLoader; -if ('cli' !== PHP_SAPI) { - throw new Exception('This script must be run from the command line.'); -} - (static function () { $cwd = \getcwd(); diff --git a/box.json.dist b/box.json.dist index 05c31ce..583c3b8 100644 --- a/box.json.dist +++ b/box.json.dist @@ -4,6 +4,8 @@ "KevinGH\\Box\\Compactor\\Json", "KevinGH\\Box\\Compactor\\Php" ], + "check-requirements": false, + "dump-autoload": false, "compression": "GZ", "git": "git", "directories": [ diff --git a/composer.json b/composer.json index 9249bdf..33e22a2 100644 --- a/composer.json +++ b/composer.json @@ -32,11 +32,10 @@ "buggregator/trap": "^1.10", "dereuromark/composer-prefer-lowest": "^0.1.10", "ergebnis/phpunit-slow-test-detector": "^2.14", - "friendsofphp/php-cs-fixer": "^3.54", - "pestphp/pest": "^2.34", "phpunit/phpunit": "^10.5", - "vimeo/psalm": "^5.11", - "wayofdev/cs-fixer-config": "^1.4" + "spiral/code-style": "^2.2.2", + "ta-tikoma/phpunit-architecture-test": "^0.8.4", + "vimeo/psalm": "^6.10" }, "suggest": { "ext-simplexml": "to support XML configs parsing" @@ -59,15 +58,11 @@ "config": { "allow-plugins": { "ergebnis/composer-normalize": true, - "infection/extension-installer": true, - "pestphp/pest-plugin": true + "infection/extension-installer": true }, "audit": { "abandoned": "report" }, - "platform": { - "php": "8.1.27" - }, "sort-packages": true }, "scripts": { @@ -90,7 +85,14 @@ "@putenv XDEBUG_MODE=coverage", "phpunit --color=always" ], - "test:arch": "pest --color=always --configuration pest.xml.dist", + "test:unit": [ + "@putenv XDEBUG_MODE=coverage", + "phpunit --color=always --testsuite=Unit" + ], + "test:arch": [ + "@putenv XDEBUG_MODE=coverage", + "phpunit --color=always --testsuite=Arch" + ], "test:cc": [ "@putenv XDEBUG_MODE=coverage", "phpunit --coverage-clover=.build/phpunit/logs/clover.xml --color=always" diff --git a/composer.lock b/composer.lock index 55722d7..2281141 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8bf4dbc45a6c90ea713d6adb7f38ae5d", + "content-hash": "42e791fb2bb9b34d63eda84606615e2e", "packages": [ { "name": "psr/container", @@ -1222,43 +1222,36 @@ "packages-dev": [ { "name": "amphp/amp", - "version": "v2.6.4", + "version": "v3.1.0", "source": { "type": "git", "url": "https://github.com/amphp/amp.git", - "reference": "ded3d9be08f526089eb7ee8d9f16a9768f9dec2d" + "reference": "7cf7fef3d667bfe4b2560bc87e67d5387a7bcde9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/amp/zipball/ded3d9be08f526089eb7ee8d9f16a9768f9dec2d", - "reference": "ded3d9be08f526089eb7ee8d9f16a9768f9dec2d", + "url": "https://api.github.com/repos/amphp/amp/zipball/7cf7fef3d667bfe4b2560bc87e67d5387a7bcde9", + "reference": "7cf7fef3d667bfe4b2560bc87e67d5387a7bcde9", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" }, "require-dev": { - "amphp/php-cs-fixer-config": "dev-master", - "amphp/phpunit-util": "^1", - "ext-json": "*", - "jetbrains/phpstorm-stubs": "^2019.3", - "phpunit/phpunit": "^7 | ^8 | ^9", - "react/promise": "^2", - "vimeo/psalm": "^3.12" + "amphp/php-cs-fixer-config": "^2", + "phpunit/phpunit": "^9", + "psalm/phar": "5.23.1" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.x-dev" - } - }, "autoload": { "files": [ - "lib/functions.php", - "lib/Internal/functions.php" + "src/functions.php", + "src/Future/functions.php", + "src/Internal/functions.php" ], "psr-4": { - "Amp\\": "lib" + "Amp\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1266,10 +1259,6 @@ "MIT" ], "authors": [ - { - "name": "Daniel Lowrey", - "email": "rdlowrey@php.net" - }, { "name": "Aaron Piotrowski", "email": "aaron@trowski.com" @@ -1281,6 +1270,10 @@ { "name": "Niklas Keller", "email": "me@kelunik.com" + }, + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" } ], "description": "A non-blocking concurrency framework for PHP applications.", @@ -1297,9 +1290,8 @@ "promise" ], "support": { - "irc": "irc://irc.freenode.org/amphp", "issues": "https://github.com/amphp/amp/issues", - "source": "https://github.com/amphp/amp/tree/v2.6.4" + "source": "https://github.com/amphp/amp/tree/v3.1.0" }, "funding": [ { @@ -1307,41 +1299,45 @@ "type": "github" } ], - "time": "2024-03-21T18:52:26+00:00" + "time": "2025-01-26T16:07:39+00:00" }, { "name": "amphp/byte-stream", - "version": "v1.8.2", + "version": "v2.1.2", "source": { "type": "git", "url": "https://github.com/amphp/byte-stream.git", - "reference": "4f0e968ba3798a423730f567b1b50d3441c16ddc" + "reference": "55a6bd071aec26fa2a3e002618c20c35e3df1b46" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/byte-stream/zipball/4f0e968ba3798a423730f567b1b50d3441c16ddc", - "reference": "4f0e968ba3798a423730f567b1b50d3441c16ddc", + "url": "https://api.github.com/repos/amphp/byte-stream/zipball/55a6bd071aec26fa2a3e002618c20c35e3df1b46", + "reference": "55a6bd071aec26fa2a3e002618c20c35e3df1b46", "shasum": "" }, "require": { - "amphp/amp": "^2", - "php": ">=7.1" + "amphp/amp": "^3", + "amphp/parser": "^1.1", + "amphp/pipeline": "^1", + "amphp/serialization": "^1", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2.3" }, "require-dev": { - "amphp/php-cs-fixer-config": "dev-master", - "amphp/phpunit-util": "^1.4", - "friendsofphp/php-cs-fixer": "^2.3", - "jetbrains/phpstorm-stubs": "^2019.3", - "phpunit/phpunit": "^6 || ^7 || ^8", - "psalm/phar": "^3.11.4" + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "5.22.1" }, "type": "library", "autoload": { "files": [ - "lib/functions.php" + "src/functions.php", + "src/Internal/functions.php" ], "psr-4": { - "Amp\\ByteStream\\": "lib" + "Amp\\ByteStream\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1369,8 +1365,585 @@ "stream" ], "support": { - "issues": "https://github.com/amphp/byte-stream/issues", - "source": "https://github.com/amphp/byte-stream/tree/v1.8.2" + "issues": "https://github.com/amphp/byte-stream/issues", + "source": "https://github.com/amphp/byte-stream/tree/v2.1.2" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2025-03-16T17:10:27+00:00" + }, + { + "name": "amphp/cache", + "version": "v2.0.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/cache.git", + "reference": "46912e387e6aa94933b61ea1ead9cf7540b7797c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/cache/zipball/46912e387e6aa94933b61ea1ead9cf7540b7797c", + "reference": "46912e387e6aa94933b61ea1ead9cf7540b7797c", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/serialization": "^1", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Amp\\Cache\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + } + ], + "description": "A fiber-aware cache API based on Amp and Revolt.", + "homepage": "https://amphp.org/cache", + "support": { + "issues": "https://github.com/amphp/cache/issues", + "source": "https://github.com/amphp/cache/tree/v2.0.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-04-19T03:38:06+00:00" + }, + { + "name": "amphp/dns", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/amphp/dns.git", + "reference": "78eb3db5fc69bf2fc0cb503c4fcba667bc223c71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/dns/zipball/78eb3db5fc69bf2fc0cb503c4fcba667bc223c71", + "reference": "78eb3db5fc69bf2fc0cb503c4fcba667bc223c71", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/cache": "^2", + "amphp/parser": "^1", + "amphp/process": "^2", + "daverandom/libdns": "^2.0.2", + "ext-filter": "*", + "ext-json": "*", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "5.20" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Dns\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Wright", + "email": "addr@daverandom.com" + }, + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + }, + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + } + ], + "description": "Async DNS resolution for Amp.", + "homepage": "https://github.com/amphp/dns", + "keywords": [ + "amp", + "amphp", + "async", + "client", + "dns", + "resolve" + ], + "support": { + "issues": "https://github.com/amphp/dns/issues", + "source": "https://github.com/amphp/dns/tree/v2.4.0" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2025-01-19T15:43:40+00:00" + }, + { + "name": "amphp/parallel", + "version": "v2.3.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/parallel.git", + "reference": "5113111de02796a782f5d90767455e7391cca190" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/parallel/zipball/5113111de02796a782f5d90767455e7391cca190", + "reference": "5113111de02796a782f5d90767455e7391cca190", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/cache": "^2", + "amphp/parser": "^1", + "amphp/pipeline": "^1", + "amphp/process": "^2", + "amphp/serialization": "^1", + "amphp/socket": "^2", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.18" + }, + "type": "library", + "autoload": { + "files": [ + "src/Context/functions.php", + "src/Context/Internal/functions.php", + "src/Ipc/functions.php", + "src/Worker/functions.php" + ], + "psr-4": { + "Amp\\Parallel\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Stephen Coakley", + "email": "me@stephencoakley.com" + } + ], + "description": "Parallel processing component for Amp.", + "homepage": "https://github.com/amphp/parallel", + "keywords": [ + "async", + "asynchronous", + "concurrent", + "multi-processing", + "multi-threading" + ], + "support": { + "issues": "https://github.com/amphp/parallel/issues", + "source": "https://github.com/amphp/parallel/tree/v2.3.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-12-21T01:56:09+00:00" + }, + { + "name": "amphp/parser", + "version": "v1.1.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/parser.git", + "reference": "3cf1f8b32a0171d4b1bed93d25617637a77cded7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/parser/zipball/3cf1f8b32a0171d4b1bed93d25617637a77cded7", + "reference": "3cf1f8b32a0171d4b1bed93d25617637a77cded7", + "shasum": "" + }, + "require": { + "php": ">=7.4" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Amp\\Parser\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A generator parser to make streaming parsers simple.", + "homepage": "https://github.com/amphp/parser", + "keywords": [ + "async", + "non-blocking", + "parser", + "stream" + ], + "support": { + "issues": "https://github.com/amphp/parser/issues", + "source": "https://github.com/amphp/parser/tree/v1.1.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-03-21T19:16:53+00:00" + }, + { + "name": "amphp/pipeline", + "version": "v1.2.3", + "source": { + "type": "git", + "url": "https://github.com/amphp/pipeline.git", + "reference": "7b52598c2e9105ebcddf247fc523161581930367" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/pipeline/zipball/7b52598c2e9105ebcddf247fc523161581930367", + "reference": "7b52598c2e9105ebcddf247fc523161581930367", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "php": ">=8.1", + "revolt/event-loop": "^1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.18" + }, + "type": "library", + "autoload": { + "psr-4": { + "Amp\\Pipeline\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Asynchronous iterators and operators.", + "homepage": "https://amphp.org/pipeline", + "keywords": [ + "amp", + "amphp", + "async", + "io", + "iterator", + "non-blocking" + ], + "support": { + "issues": "https://github.com/amphp/pipeline/issues", + "source": "https://github.com/amphp/pipeline/tree/v1.2.3" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2025-03-16T16:33:53+00:00" + }, + { + "name": "amphp/process", + "version": "v2.0.3", + "source": { + "type": "git", + "url": "https://github.com/amphp/process.git", + "reference": "52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/process/zipball/52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d", + "reference": "52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.4" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Process\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A fiber-aware process manager based on Amp and Revolt.", + "homepage": "https://amphp.org/process", + "support": { + "issues": "https://github.com/amphp/process/issues", + "source": "https://github.com/amphp/process/tree/v2.0.3" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-04-19T03:13:44+00:00" + }, + { + "name": "amphp/serialization", + "version": "v1.0.0", + "source": { + "type": "git", + "url": "https://github.com/amphp/serialization.git", + "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/serialization/zipball/693e77b2fb0b266c3c7d622317f881de44ae94a1", + "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "dev-master", + "phpunit/phpunit": "^9 || ^8 || ^7" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Serialization\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Serialization tools for IPC and data storage in PHP.", + "homepage": "https://github.com/amphp/serialization", + "keywords": [ + "async", + "asynchronous", + "serialization", + "serialize" + ], + "support": { + "issues": "https://github.com/amphp/serialization/issues", + "source": "https://github.com/amphp/serialization/tree/master" + }, + "time": "2020-03-25T21:39:07+00:00" + }, + { + "name": "amphp/socket", + "version": "v2.3.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/socket.git", + "reference": "58e0422221825b79681b72c50c47a930be7bf1e1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/socket/zipball/58e0422221825b79681b72c50c47a930be7bf1e1", + "reference": "58e0422221825b79681b72c50c47a930be7bf1e1", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/dns": "^2", + "ext-openssl": "*", + "kelunik/certificate": "^1.1", + "league/uri": "^6.5 | ^7", + "league/uri-interfaces": "^2.3 | ^7", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "amphp/process": "^2", + "phpunit/phpunit": "^9", + "psalm/phar": "5.20" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php", + "src/Internal/functions.php", + "src/SocketAddress/functions.php" + ], + "psr-4": { + "Amp\\Socket\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Lowrey", + "email": "rdlowrey@gmail.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Non-blocking socket connection / server implementations based on Amp and Revolt.", + "homepage": "https://github.com/amphp/socket", + "keywords": [ + "amp", + "async", + "encryption", + "non-blocking", + "sockets", + "tcp", + "tls" + ], + "support": { + "issues": "https://github.com/amphp/socket/issues", + "source": "https://github.com/amphp/socket/tree/v2.3.1" }, "funding": [ { @@ -1378,61 +1951,42 @@ "type": "github" } ], - "time": "2024-04-13T18:00:56+00:00" + "time": "2024-04-21T14:33:03+00:00" }, { - "name": "brianium/paratest", - "version": "v7.3.1", + "name": "amphp/sync", + "version": "v2.3.0", "source": { "type": "git", - "url": "https://github.com/paratestphp/paratest.git", - "reference": "551f46f52a93177d873f3be08a1649ae886b4a30" + "url": "https://github.com/amphp/sync.git", + "reference": "217097b785130d77cfcc58ff583cf26cd1770bf1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paratestphp/paratest/zipball/551f46f52a93177d873f3be08a1649ae886b4a30", - "reference": "551f46f52a93177d873f3be08a1649ae886b4a30", + "url": "https://api.github.com/repos/amphp/sync/zipball/217097b785130d77cfcc58ff583cf26cd1770bf1", + "reference": "217097b785130d77cfcc58ff583cf26cd1770bf1", "shasum": "" }, "require": { - "ext-dom": "*", - "ext-pcre": "*", - "ext-reflection": "*", - "ext-simplexml": "*", - "fidry/cpu-core-counter": "^0.5.1 || ^1.0.0", - "jean85/pretty-package-versions": "^2.0.5", - "php": "~8.1.0 || ~8.2.0 || ~8.3.0", - "phpunit/php-code-coverage": "^10.1.7", - "phpunit/php-file-iterator": "^4.1.0", - "phpunit/php-timer": "^6.0", - "phpunit/phpunit": "^10.4.2", - "sebastian/environment": "^6.0.1", - "symfony/console": "^6.3.4 || ^7.0.0", - "symfony/process": "^6.3.4 || ^7.0.0" + "amphp/amp": "^3", + "amphp/pipeline": "^1", + "amphp/serialization": "^1", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" }, "require-dev": { - "doctrine/coding-standard": "^12.0.0", - "ext-pcov": "*", - "ext-posix": "*", - "infection/infection": "^0.27.6", - "phpstan/phpstan": "^1.10.40", - "phpstan/phpstan-deprecation-rules": "^1.1.4", - "phpstan/phpstan-phpunit": "^1.3.15", - "phpstan/phpstan-strict-rules": "^1.5.2", - "squizlabs/php_codesniffer": "^3.7.2", - "symfony/filesystem": "^6.3.1 || ^7.0.0" + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "5.23" }, - "bin": [ - "bin/paratest", - "bin/paratest.bat", - "bin/paratest_for_phpstorm" - ], "type": "library", "autoload": { + "files": [ + "src/functions.php" + ], "psr-4": { - "ParaTest\\": [ - "src/" - ] + "Amp\\Sync\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1441,39 +1995,38 @@ ], "authors": [ { - "name": "Brian Scaturro", - "email": "scaturrob@gmail.com", - "role": "Developer" + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" }, { - "name": "Filippo Tessarotto", - "email": "zoeslam@gmail.com", - "role": "Developer" + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Stephen Coakley", + "email": "me@stephencoakley.com" } ], - "description": "Parallel testing for PHP", - "homepage": "https://github.com/paratestphp/paratest", + "description": "Non-blocking synchronization primitives for PHP based on Amp and Revolt.", + "homepage": "https://github.com/amphp/sync", "keywords": [ - "concurrent", - "parallel", - "phpunit", - "testing" + "async", + "asynchronous", + "mutex", + "semaphore", + "synchronization" ], "support": { - "issues": "https://github.com/paratestphp/paratest/issues", - "source": "https://github.com/paratestphp/paratest/tree/v7.3.1" + "issues": "https://github.com/amphp/sync/issues", + "source": "https://github.com/amphp/sync/tree/v2.3.0" }, "funding": [ { - "url": "https://github.com/sponsors/Slamdunk", + "url": "https://github.com/amphp", "type": "github" - }, - { - "url": "https://paypal.me/filippotessarotto", - "type": "paypal" } ], - "time": "2023-10-31T09:24:17+00:00" + "time": "2024-08-03T19:31:26+00:00" }, { "name": "buggregator/trap", @@ -1933,6 +2486,102 @@ ], "time": "2024-05-06T16:37:16+00:00" }, + { + "name": "danog/advanced-json-rpc", + "version": "v3.2.2", + "source": { + "type": "git", + "url": "https://github.com/danog/php-advanced-json-rpc.git", + "reference": "aadb1c4068a88c3d0530cfe324b067920661efcb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/danog/php-advanced-json-rpc/zipball/aadb1c4068a88c3d0530cfe324b067920661efcb", + "reference": "aadb1c4068a88c3d0530cfe324b067920661efcb", + "shasum": "" + }, + "require": { + "netresearch/jsonmapper": "^5", + "php": ">=8.1", + "phpdocumentor/reflection-docblock": "^4.3.4 || ^5.0.0" + }, + "replace": { + "felixfbecker/php-advanced-json-rpc": "^3" + }, + "require-dev": { + "phpunit/phpunit": "^9" + }, + "type": "library", + "autoload": { + "psr-4": { + "AdvancedJsonRpc\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Felix Becker", + "email": "felix.b@outlook.com" + }, + { + "name": "Daniil Gentili", + "email": "daniil@daniil.it" + } + ], + "description": "A more advanced JSONRPC implementation", + "support": { + "issues": "https://github.com/danog/php-advanced-json-rpc/issues", + "source": "https://github.com/danog/php-advanced-json-rpc/tree/v3.2.2" + }, + "time": "2025-02-14T10:55:15+00:00" + }, + { + "name": "daverandom/libdns", + "version": "v2.1.0", + "source": { + "type": "git", + "url": "https://github.com/DaveRandom/LibDNS.git", + "reference": "b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/DaveRandom/LibDNS/zipball/b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a", + "reference": "b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "Required for IDN support" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "LibDNS\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "DNS protocol implementation written in pure PHP", + "keywords": [ + "dns" + ], + "support": { + "issues": "https://github.com/DaveRandom/LibDNS/issues", + "source": "https://github.com/DaveRandom/LibDNS/tree/v2.1.0" + }, + "time": "2024-04-12T12:12:48+00:00" + }, { "name": "dereuromark/composer-prefer-lowest", "version": "0.1.10", @@ -2184,51 +2833,6 @@ }, "time": "2023-08-08T05:53:35+00:00" }, - { - "name": "felixfbecker/advanced-json-rpc", - "version": "v3.2.1", - "source": { - "type": "git", - "url": "https://github.com/felixfbecker/php-advanced-json-rpc.git", - "reference": "b5f37dbff9a8ad360ca341f3240dc1c168b45447" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/felixfbecker/php-advanced-json-rpc/zipball/b5f37dbff9a8ad360ca341f3240dc1c168b45447", - "reference": "b5f37dbff9a8ad360ca341f3240dc1c168b45447", - "shasum": "" - }, - "require": { - "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0", - "php": "^7.1 || ^8.0", - "phpdocumentor/reflection-docblock": "^4.3.4 || ^5.0.0" - }, - "require-dev": { - "phpunit/phpunit": "^7.0 || ^8.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "AdvancedJsonRpc\\": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "ISC" - ], - "authors": [ - { - "name": "Felix Becker", - "email": "felix.b@outlook.com" - } - ], - "description": "A more advanced JSONRPC implementation", - "support": { - "issues": "https://github.com/felixfbecker/php-advanced-json-rpc/issues", - "source": "https://github.com/felixfbecker/php-advanced-json-rpc/tree/v3.2.1" - }, - "time": "2021-06-11T22:34:44+00:00" - }, { "name": "felixfbecker/language-server-protocol", "version": "v1.5.3", @@ -2346,77 +2950,6 @@ ], "time": "2024-08-06T10:04:20+00:00" }, - { - "name": "filp/whoops", - "version": "2.18.0", - "source": { - "type": "git", - "url": "https://github.com/filp/whoops.git", - "reference": "a7de6c3c6c3c022f5cfc337f8ede6a14460cf77e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/filp/whoops/zipball/a7de6c3c6c3c022f5cfc337f8ede6a14460cf77e", - "reference": "a7de6c3c6c3c022f5cfc337f8ede6a14460cf77e", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0", - "psr/log": "^1.0.1 || ^2.0 || ^3.0" - }, - "require-dev": { - "mockery/mockery": "^1.0", - "phpunit/phpunit": "^7.5.20 || ^8.5.8 || ^9.3.3", - "symfony/var-dumper": "^4.0 || ^5.0" - }, - "suggest": { - "symfony/var-dumper": "Pretty print complex values better with var-dumper available", - "whoops/soap": "Formats errors as SOAP responses" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.7-dev" - } - }, - "autoload": { - "psr-4": { - "Whoops\\": "src/Whoops/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Filipe Dobreira", - "homepage": "https://github.com/filp", - "role": "Developer" - } - ], - "description": "php error handling for cool kids", - "homepage": "https://filp.github.io/whoops/", - "keywords": [ - "error", - "exception", - "handling", - "library", - "throwable", - "whoops" - ], - "support": { - "issues": "https://github.com/filp/whoops/issues", - "source": "https://github.com/filp/whoops/tree/2.18.0" - }, - "funding": [ - { - "url": "https://github.com/denis-sokolov", - "type": "github" - } - ], - "time": "2025-03-15T12:00:00+00:00" - }, { "name": "friendsofphp/php-cs-fixer", "version": "v3.75.0", @@ -2519,43 +3052,192 @@ "type": "github" } ], - "time": "2025-03-31T18:40:42+00:00" + "time": "2025-03-31T18:40:42+00:00" + }, + { + "name": "kelunik/certificate", + "version": "v1.1.3", + "source": { + "type": "git", + "url": "https://github.com/kelunik/certificate.git", + "reference": "7e00d498c264d5eb4f78c69f41c8bd6719c0199e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/kelunik/certificate/zipball/7e00d498c264d5eb4f78c69f41c8bd6719c0199e", + "reference": "7e00d498c264d5eb4f78c69f41c8bd6719c0199e", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "php": ">=7.0" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "phpunit/phpunit": "^6 | 7 | ^8 | ^9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Kelunik\\Certificate\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Access certificate details and transform between different formats.", + "keywords": [ + "DER", + "certificate", + "certificates", + "openssl", + "pem", + "x509" + ], + "support": { + "issues": "https://github.com/kelunik/certificate/issues", + "source": "https://github.com/kelunik/certificate/tree/v1.1.3" + }, + "time": "2023-02-03T21:26:53+00:00" + }, + { + "name": "league/uri", + "version": "7.5.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri.git", + "reference": "81fb5145d2644324614cc532b28efd0215bda430" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/81fb5145d2644324614cc532b28efd0215bda430", + "reference": "81fb5145d2644324614cc532b28efd0215bda430", + "shasum": "" + }, + "require": { + "league/uri-interfaces": "^7.5", + "php": "^8.1" + }, + "conflict": { + "league/uri-schemes": "^1.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-fileinfo": "to create Data URI from file contennts", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "jeremykendall/php-domain-parser": "to resolve Public Suffix and Top Level Domain", + "league/uri-components": "Needed to easily manipulate URI objects components", + "php-64bit": "to improve IPV4 host parsing", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "URI manipulation library", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "middleware", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc3986", + "rfc3987", + "rfc6570", + "uri", + "uri-template", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri/tree/7.5.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2024-12-08T08:40:02+00:00" }, { - "name": "jean85/pretty-package-versions", - "version": "2.1.1", + "name": "league/uri-interfaces", + "version": "7.5.0", "source": { "type": "git", - "url": "https://github.com/Jean85/pretty-package-versions.git", - "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a" + "url": "https://github.com/thephpleague/uri-interfaces.git", + "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/4d7aa5dab42e2a76d99559706022885de0e18e1a", - "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", + "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", "shasum": "" }, "require": { - "composer-runtime-api": "^2.1.0", - "php": "^7.4|^8.0" + "ext-filter": "*", + "php": "^8.1", + "psr/http-factory": "^1", + "psr/http-message": "^1.1 || ^2.0" }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^3.2", - "jean85/composer-provided-replaced-stub-package": "^1.0", - "phpstan/phpstan": "^2.0", - "phpunit/phpunit": "^7.5|^8.5|^9.6", - "rector/rector": "^2.0", - "vimeo/psalm": "^4.3 || ^5.0" + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "php-64bit": "to improve IPV4 host parsing", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.x-dev" + "dev-master": "7.x-dev" } }, "autoload": { "psr-4": { - "Jean85\\": "src/" + "League\\Uri\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -2564,22 +3246,45 @@ ], "authors": [ { - "name": "Alessandro Lai", - "email": "alessandro.lai85@gmail.com" + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" } ], - "description": "A library to get pretty versions strings of installed dependencies", + "description": "Common interfaces and classes for URI representation and interaction", + "homepage": "https://uri.thephpleague.com", "keywords": [ - "composer", - "package", - "release", - "versions" + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc3986", + "rfc3987", + "rfc6570", + "uri", + "url", + "ws" ], "support": { - "issues": "https://github.com/Jean85/pretty-package-versions/issues", - "source": "https://github.com/Jean85/pretty-package-versions/tree/2.1.1" + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.5.0" }, - "time": "2025-03-19T14:43:43+00:00" + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2024-12-08T08:18:47+00:00" }, { "name": "myclabs/deep-copy", @@ -2643,16 +3348,16 @@ }, { "name": "netresearch/jsonmapper", - "version": "v4.5.0", + "version": "v5.0.0", "source": { "type": "git", "url": "https://github.com/cweiske/jsonmapper.git", - "reference": "8e76efb98ee8b6afc54687045e1b8dba55ac76e5" + "reference": "8c64d8d444a5d764c641ebe97e0e3bc72b25bf6c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/8e76efb98ee8b6afc54687045e1b8dba55ac76e5", - "reference": "8e76efb98ee8b6afc54687045e1b8dba55ac76e5", + "url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/8c64d8d444a5d764c641ebe97e0e3bc72b25bf6c", + "reference": "8c64d8d444a5d764c641ebe97e0e3bc72b25bf6c", "shasum": "" }, "require": { @@ -2688,31 +3393,33 @@ "support": { "email": "cweiske@cweiske.de", "issues": "https://github.com/cweiske/jsonmapper/issues", - "source": "https://github.com/cweiske/jsonmapper/tree/v4.5.0" + "source": "https://github.com/cweiske/jsonmapper/tree/v5.0.0" }, - "time": "2024-09-08T10:13:13+00:00" + "time": "2024-09-08T10:20:00+00:00" }, { "name": "nikic/php-parser", - "version": "v4.19.4", + "version": "v5.4.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "715f4d25e225bc47b293a8b997fe6ce99bf987d2" + "reference": "447a020a1f875a434d62f2a401f53b82a396e494" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/715f4d25e225bc47b293a8b997fe6ce99bf987d2", - "reference": "715f4d25e225bc47b293a8b997fe6ce99bf987d2", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494", + "reference": "447a020a1f875a434d62f2a401f53b82a396e494", "shasum": "" }, "require": { + "ext-ctype": "*", + "ext-json": "*", "ext-tokenizer": "*", - "php": ">=7.1" + "php": ">=7.4" }, "require-dev": { "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" + "phpunit/phpunit": "^9.0" }, "bin": [ "bin/php-parse" @@ -2720,7 +3427,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.9-dev" + "dev-master": "5.0-dev" } }, "autoload": { @@ -2744,105 +3451,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.19.4" - }, - "time": "2024-09-29T15:01:53+00:00" - }, - { - "name": "nunomaduro/collision", - "version": "v7.12.0", - "source": { - "type": "git", - "url": "https://github.com/nunomaduro/collision.git", - "reference": "995245421d3d7593a6960822063bdba4f5d7cf1a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/collision/zipball/995245421d3d7593a6960822063bdba4f5d7cf1a", - "reference": "995245421d3d7593a6960822063bdba4f5d7cf1a", - "shasum": "" - }, - "require": { - "filp/whoops": "^2.17.0", - "nunomaduro/termwind": "^1.17.0", - "php": "^8.1.0", - "symfony/console": "^6.4.17" - }, - "conflict": { - "laravel/framework": ">=11.0.0" - }, - "require-dev": { - "brianium/paratest": "^7.4.8", - "laravel/framework": "^10.48.29", - "laravel/pint": "^1.21.2", - "laravel/sail": "^1.41.0", - "laravel/sanctum": "^3.3.3", - "laravel/tinker": "^2.10.1", - "nunomaduro/larastan": "^2.10.0", - "orchestra/testbench-core": "^8.35.0", - "pestphp/pest": "^2.36.0", - "phpunit/phpunit": "^10.5.36", - "sebastian/environment": "^6.1.0", - "spatie/laravel-ignition": "^2.9.1" - }, - "type": "library", - "extra": { - "laravel": { - "providers": [ - "NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider" - ] - } - }, - "autoload": { - "files": [ - "./src/Adapters/Phpunit/Autoload.php" - ], - "psr-4": { - "NunoMaduro\\Collision\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nuno Maduro", - "email": "enunomaduro@gmail.com" - } - ], - "description": "Cli error handling for console/command-line PHP applications.", - "keywords": [ - "artisan", - "cli", - "command-line", - "console", - "error", - "handling", - "laravel", - "laravel-zero", - "php", - "symfony" - ], - "support": { - "issues": "https://github.com/nunomaduro/collision/issues", - "source": "https://github.com/nunomaduro/collision" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0" }, - "funding": [ - { - "url": "https://www.paypal.com/paypalme/enunomaduro", - "type": "custom" - }, - { - "url": "https://github.com/nunomaduro", - "type": "github" - }, - { - "url": "https://www.patreon.com/nunomaduro", - "type": "patreon" - } - ], - "time": "2025-03-14T22:35:49+00:00" + "time": "2024-12-30T11:07:19+00:00" }, { "name": "nunomaduro/termwind", @@ -2949,313 +3560,63 @@ "psr/http-message": "^1.1 || ^2.0" }, "provide": { - "php-http/message-factory-implementation": "1.0", - "psr/http-factory-implementation": "1.0", - "psr/http-message-implementation": "1.0" - }, - "require-dev": { - "http-interop/http-factory-tests": "^0.9", - "php-http/message-factory": "^1.0", - "php-http/psr7-integration-tests": "^1.0", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.4", - "symfony/error-handler": "^4.4" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.8-dev" - } - }, - "autoload": { - "psr-4": { - "Nyholm\\Psr7\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Tobias Nyholm", - "email": "tobias.nyholm@gmail.com" - }, - { - "name": "Martijn van der Ven", - "email": "martijn@vanderven.se" - } - ], - "description": "A fast PHP7 implementation of PSR-7", - "homepage": "https://tnyholm.se", - "keywords": [ - "psr-17", - "psr-7" - ], - "support": { - "issues": "https://github.com/Nyholm/psr7/issues", - "source": "https://github.com/Nyholm/psr7/tree/1.8.2" - }, - "funding": [ - { - "url": "https://github.com/Zegnat", - "type": "github" - }, - { - "url": "https://github.com/nyholm", - "type": "github" - } - ], - "time": "2024-09-09T07:06:30+00:00" - }, - { - "name": "pestphp/pest", - "version": "v2.36.0", - "source": { - "type": "git", - "url": "https://github.com/pestphp/pest.git", - "reference": "f8c88bd14dc1772bfaf02169afb601ecdf2724cd" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest/zipball/f8c88bd14dc1772bfaf02169afb601ecdf2724cd", - "reference": "f8c88bd14dc1772bfaf02169afb601ecdf2724cd", - "shasum": "" - }, - "require": { - "brianium/paratest": "^7.3.1", - "nunomaduro/collision": "^7.11.0|^8.4.0", - "nunomaduro/termwind": "^1.16.0|^2.1.0", - "pestphp/pest-plugin": "^2.1.1", - "pestphp/pest-plugin-arch": "^2.7.0", - "php": "^8.1.0", - "phpunit/phpunit": "^10.5.36" - }, - "conflict": { - "filp/whoops": "<2.16.0", - "phpunit/phpunit": ">10.5.36", - "sebastian/exporter": "<5.1.0", - "webmozart/assert": "<1.11.0" - }, - "require-dev": { - "pestphp/pest-dev-tools": "^2.17.0", - "pestphp/pest-plugin-type-coverage": "^2.8.7", - "symfony/process": "^6.4.0|^7.1.5" - }, - "bin": [ - "bin/pest" - ], - "type": "library", - "extra": { - "pest": { - "plugins": [ - "Pest\\Plugins\\Bail", - "Pest\\Plugins\\Cache", - "Pest\\Plugins\\Coverage", - "Pest\\Plugins\\Init", - "Pest\\Plugins\\Environment", - "Pest\\Plugins\\Help", - "Pest\\Plugins\\Memory", - "Pest\\Plugins\\Only", - "Pest\\Plugins\\Printer", - "Pest\\Plugins\\ProcessIsolation", - "Pest\\Plugins\\Profile", - "Pest\\Plugins\\Retry", - "Pest\\Plugins\\Snapshot", - "Pest\\Plugins\\Verbose", - "Pest\\Plugins\\Version", - "Pest\\Plugins\\Parallel" - ] - }, - "phpstan": { - "includes": [ - "extension.neon" - ] - } - }, - "autoload": { - "files": [ - "src/Functions.php", - "src/Pest.php" - ], - "psr-4": { - "Pest\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nuno Maduro", - "email": "enunomaduro@gmail.com" - } - ], - "description": "The elegant PHP Testing Framework.", - "keywords": [ - "framework", - "pest", - "php", - "test", - "testing", - "unit" - ], - "support": { - "issues": "https://github.com/pestphp/pest/issues", - "source": "https://github.com/pestphp/pest/tree/v2.36.0" - }, - "funding": [ - { - "url": "https://www.paypal.com/paypalme/enunomaduro", - "type": "custom" - }, - { - "url": "https://github.com/nunomaduro", - "type": "github" - } - ], - "time": "2024-10-15T15:30:56+00:00" - }, - { - "name": "pestphp/pest-plugin", - "version": "v2.1.1", - "source": { - "type": "git", - "url": "https://github.com/pestphp/pest-plugin.git", - "reference": "e05d2859e08c2567ee38ce8b005d044e72648c0b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest-plugin/zipball/e05d2859e08c2567ee38ce8b005d044e72648c0b", - "reference": "e05d2859e08c2567ee38ce8b005d044e72648c0b", - "shasum": "" - }, - "require": { - "composer-plugin-api": "^2.0.0", - "composer-runtime-api": "^2.2.2", - "php": "^8.1" - }, - "conflict": { - "pestphp/pest": "<2.2.3" - }, - "require-dev": { - "composer/composer": "^2.5.8", - "pestphp/pest": "^2.16.0", - "pestphp/pest-dev-tools": "^2.16.0" - }, - "type": "composer-plugin", - "extra": { - "class": "Pest\\Plugin\\Manager" - }, - "autoload": { - "psr-4": { - "Pest\\Plugin\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "The Pest plugin manager", - "keywords": [ - "framework", - "manager", - "pest", - "php", - "plugin", - "test", - "testing", - "unit" - ], - "support": { - "source": "https://github.com/pestphp/pest-plugin/tree/v2.1.1" - }, - "funding": [ - { - "url": "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=66BYDWAT92N6L", - "type": "custom" - }, - { - "url": "https://github.com/nunomaduro", - "type": "github" - }, - { - "url": "https://www.patreon.com/nunomaduro", - "type": "patreon" - } - ], - "time": "2023-08-22T08:40:06+00:00" - }, - { - "name": "pestphp/pest-plugin-arch", - "version": "v2.7.0", - "source": { - "type": "git", - "url": "https://github.com/pestphp/pest-plugin-arch.git", - "reference": "d23b2d7498475354522c3818c42ef355dca3fcda" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest-plugin-arch/zipball/d23b2d7498475354522c3818c42ef355dca3fcda", - "reference": "d23b2d7498475354522c3818c42ef355dca3fcda", - "shasum": "" - }, - "require": { - "nunomaduro/collision": "^7.10.0|^8.1.0", - "pestphp/pest-plugin": "^2.1.1", - "php": "^8.1", - "ta-tikoma/phpunit-architecture-test": "^0.8.4" + "php-http/message-factory-implementation": "1.0", + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" }, "require-dev": { - "pestphp/pest": "^2.33.0", - "pestphp/pest-dev-tools": "^2.16.0" + "http-interop/http-factory-tests": "^0.9", + "php-http/message-factory": "^1.0", + "php-http/psr7-integration-tests": "^1.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.4", + "symfony/error-handler": "^4.4" }, "type": "library", "extra": { - "pest": { - "plugins": [ - "Pest\\Arch\\Plugin" - ] + "branch-alias": { + "dev-master": "1.8-dev" } }, "autoload": { - "files": [ - "src/Autoload.php" - ], "psr-4": { - "Pest\\Arch\\": "src/" + "Nyholm\\Psr7\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "description": "The Arch plugin for Pest PHP.", + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + }, + { + "name": "Martijn van der Ven", + "email": "martijn@vanderven.se" + } + ], + "description": "A fast PHP7 implementation of PSR-7", + "homepage": "https://tnyholm.se", "keywords": [ - "arch", - "architecture", - "framework", - "pest", - "php", - "plugin", - "test", - "testing", - "unit" + "psr-17", + "psr-7" ], "support": { - "source": "https://github.com/pestphp/pest-plugin-arch/tree/v2.7.0" + "issues": "https://github.com/Nyholm/psr7/issues", + "source": "https://github.com/Nyholm/psr7/tree/1.8.2" }, "funding": [ { - "url": "https://www.paypal.com/paypalme/enunomaduro", - "type": "custom" + "url": "https://github.com/Zegnat", + "type": "github" }, { - "url": "https://github.com/nunomaduro", + "url": "https://github.com/nyholm", "type": "github" } ], - "time": "2024-01-26T09:46:42+00:00" + "time": "2024-09-09T07:06:30+00:00" }, { "name": "phar-io/manifest", @@ -3989,16 +4350,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.5.36", + "version": "10.5.45", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "aa0a8ce701ea7ee314b0dfaa8970dc94f3f8c870" + "reference": "bd68a781d8e30348bc297449f5234b3458267ae8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/aa0a8ce701ea7ee314b0dfaa8970dc94f3f8c870", - "reference": "aa0a8ce701ea7ee314b0dfaa8970dc94f3f8c870", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/bd68a781d8e30348bc297449f5234b3458267ae8", + "reference": "bd68a781d8e30348bc297449f5234b3458267ae8", "shasum": "" }, "require": { @@ -4008,7 +4369,7 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.12.0", + "myclabs/deep-copy": "^1.12.1", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.1", @@ -4019,7 +4380,7 @@ "phpunit/php-timer": "^6.0.0", "sebastian/cli-parser": "^2.0.1", "sebastian/code-unit": "^2.0.0", - "sebastian/comparator": "^5.0.2", + "sebastian/comparator": "^5.0.3", "sebastian/diff": "^5.1.1", "sebastian/environment": "^6.1.0", "sebastian/exporter": "^5.1.2", @@ -4070,7 +4431,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.36" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.45" }, "funding": [ { @@ -4086,7 +4447,7 @@ "type": "tidelift" } ], - "time": "2024-10-08T15:36:51+00:00" + "time": "2025-02-06T16:08:12+00:00" }, { "name": "psr/event-dispatcher", @@ -4627,6 +4988,78 @@ ], "time": "2024-06-11T12:45:25+00:00" }, + { + "name": "revolt/event-loop", + "version": "v1.0.7", + "source": { + "type": "git", + "url": "https://github.com/revoltphp/event-loop.git", + "reference": "09bf1bf7f7f574453efe43044b06fafe12216eb3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/revoltphp/event-loop/zipball/09bf1bf7f7f574453efe43044b06fafe12216eb3", + "reference": "09bf1bf7f7f574453efe43044b06fafe12216eb3", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "ext-json": "*", + "jetbrains/phpstorm-stubs": "^2019.3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.15" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Revolt\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "ceesjank@gmail.com" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Rock-solid event loop for concurrent PHP applications.", + "keywords": [ + "async", + "asynchronous", + "concurrency", + "event", + "event-loop", + "non-blocking", + "scheduler" + ], + "support": { + "issues": "https://github.com/revoltphp/event-loop/issues", + "source": "https://github.com/revoltphp/event-loop/tree/v1.0.7" + }, + "time": "2025-01-25T19:27:39+00:00" + }, { "name": "sebastian/cli-parser", "version": "2.0.1", @@ -5611,6 +6044,62 @@ ], "time": "2024-12-16T12:45:15+00:00" }, + { + "name": "spiral/code-style", + "version": "v2.2.2", + "source": { + "type": "git", + "url": "https://github.com/spiral/code-style.git", + "reference": "3803c38baf6cda714e9ebbc7e515622b22ea798d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spiral/code-style/zipball/3803c38baf6cda714e9ebbc7e515622b22ea798d", + "reference": "3803c38baf6cda714e9ebbc7e515622b22ea798d", + "shasum": "" + }, + "require": { + "friendsofphp/php-cs-fixer": "^3.64", + "php": ">=8.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.5", + "spiral/dumper": "^3.3", + "vimeo/psalm": "^5.26" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spiral\\CodeStyle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aleksandr Novikov", + "email": "aleksandr.novikov@spiralscout.com" + }, + { + "name": "Aleksei Gagarin", + "email": "alexey.gagarin@spiralscout.com" + } + ], + "description": "Code style and static analysis tools rulesets collection", + "homepage": "https://github.com/spiral/code-style", + "support": { + "source": "https://github.com/spiral/code-style/tree/v2.2.2" + }, + "funding": [ + { + "url": "https://github.com/sponsors/spiral", + "type": "github" + } + ], + "time": "2025-01-24T07:31:21+00:00" + }, { "name": "symfony/event-dispatcher", "version": "v6.4.13", @@ -6120,6 +6609,82 @@ ], "time": "2024-09-09T11:45:10+00:00" }, + { + "name": "symfony/polyfill-php84", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "e5493eb51311ab0b1cc2243416613f06ed8f18bd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/e5493eb51311ab0b1cc2243416613f06ed8f18bd", + "reference": "e5493eb51311ab0b1cc2243416613f06ed8f18bd", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.31.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-09T12:04:04+00:00" + }, { "name": "symfony/process", "version": "v6.4.20", @@ -6439,24 +7004,26 @@ }, { "name": "vimeo/psalm", - "version": "5.26.1", + "version": "6.10.0", "source": { "type": "git", "url": "https://github.com/vimeo/psalm.git", - "reference": "d747f6500b38ac4f7dfc5edbcae6e4b637d7add0" + "reference": "9c0add4eb88d4b169ac04acb7c679918cbb9c252" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vimeo/psalm/zipball/d747f6500b38ac4f7dfc5edbcae6e4b637d7add0", - "reference": "d747f6500b38ac4f7dfc5edbcae6e4b637d7add0", + "url": "https://api.github.com/repos/vimeo/psalm/zipball/9c0add4eb88d4b169ac04acb7c679918cbb9c252", + "reference": "9c0add4eb88d4b169ac04acb7c679918cbb9c252", "shasum": "" }, "require": { - "amphp/amp": "^2.4.2", - "amphp/byte-stream": "^1.5", + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/parallel": "^2.3", "composer-runtime-api": "^2", "composer/semver": "^1.4 || ^2.0 || ^3.0", "composer/xdebug-handler": "^2.0 || ^3.0", + "danog/advanced-json-rpc": "^3.1", "dnoegel/php-xdg-base-dir": "^0.1.1", "ext-ctype": "*", "ext-dom": "*", @@ -6465,27 +7032,26 @@ "ext-mbstring": "*", "ext-simplexml": "*", "ext-tokenizer": "*", - "felixfbecker/advanced-json-rpc": "^3.1", - "felixfbecker/language-server-protocol": "^1.5.2", + "felixfbecker/language-server-protocol": "^1.5.3", "fidry/cpu-core-counter": "^0.4.1 || ^0.5.1 || ^1.0.0", - "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0", - "nikic/php-parser": "^4.17", - "php": "^7.4 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0", - "sebastian/diff": "^4.0 || ^5.0 || ^6.0", + "netresearch/jsonmapper": "^5.0", + "nikic/php-parser": "^5.0.0", + "php": "~8.1.31 || ~8.2.27 || ~8.3.16 || ~8.4.3", + "sebastian/diff": "^4.0 || ^5.0 || ^6.0 || ^7.0", "spatie/array-to-xml": "^2.17.0 || ^3.0", - "symfony/console": "^4.1.6 || ^5.0 || ^6.0 || ^7.0", - "symfony/filesystem": "^5.4 || ^6.0 || ^7.0" - }, - "conflict": { - "nikic/php-parser": "4.17.0" + "symfony/console": "^6.0 || ^7.0", + "symfony/filesystem": "~6.3.12 || ~6.4.3 || ^7.0.3", + "symfony/polyfill-php84": "^1.31.0" }, "provide": { "psalm/psalm": "self.version" }, "require-dev": { - "amphp/phpunit-util": "^2.0", + "amphp/phpunit-util": "^3", "bamarni/composer-bin-plugin": "^1.4", "brianium/paratest": "^6.9", + "danog/class-finder": "^0.4.8", + "dg/bypass-finals": "^1.5", "ext-curl": "*", "mockery/mockery": "^1.5", "nunomaduro/mock-final-classes": "^1.1", @@ -6493,10 +7059,10 @@ "phpstan/phpdoc-parser": "^1.6", "phpunit/phpunit": "^9.6", "psalm/plugin-mockery": "^1.1", - "psalm/plugin-phpunit": "^0.18", + "psalm/plugin-phpunit": "^0.19", "slevomat/coding-standard": "^8.4", "squizlabs/php_codesniffer": "^3.6", - "symfony/process": "^4.4 || ^5.0 || ^6.0 || ^7.0" + "symfony/process": "^6.0 || ^7.0" }, "suggest": { "ext-curl": "In order to send data to shepherd", @@ -6507,6 +7073,7 @@ "psalm-language-server", "psalm-plugin", "psalm-refactor", + "psalm-review", "psalter" ], "type": "project", @@ -6516,7 +7083,9 @@ "dev-2.x": "2.x-dev", "dev-3.x": "3.x-dev", "dev-4.x": "4.x-dev", - "dev-master": "5.x-dev" + "dev-5.x": "5.x-dev", + "dev-6.x": "6.x-dev", + "dev-master": "7.x-dev" } }, "autoload": { @@ -6531,6 +7100,10 @@ "authors": [ { "name": "Matthew Brown" + }, + { + "name": "Daniil Gentili", + "email": "daniil@daniil.it" } ], "description": "A static analysis tool for finding errors in PHP applications", @@ -6545,87 +7118,7 @@ "issues": "https://github.com/vimeo/psalm/issues", "source": "https://github.com/vimeo/psalm" }, - "time": "2024-09-08T18:53:08+00:00" - }, - { - "name": "wayofdev/cs-fixer-config", - "version": "v1.5.3", - "source": { - "type": "git", - "url": "https://github.com/wayofdev/php-cs-fixer-config.git", - "reference": "aa0aae244772672f617de6e74d85ff81532f7e82" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/wayofdev/php-cs-fixer-config/zipball/aa0aae244772672f617de6e74d85ff81532f7e82", - "reference": "aa0aae244772672f617de6e74d85ff81532f7e82", - "shasum": "" - }, - "require": { - "friendsofphp/php-cs-fixer": "^3.57", - "php": "^8.1" - }, - "require-dev": { - "ergebnis/phpunit-slow-test-detector": "^2.14", - "pestphp/pest": "^2.34", - "phpstan/extension-installer": "^1.3", - "phpstan/phpstan": "^1.11", - "phpstan/phpstan-deprecation-rules": "^1.2", - "phpstan/phpstan-phpunit": "^1.4", - "phpstan/phpstan-strict-rules": "^1.6", - "phpunit/phpunit": "^10.5", - "psalm/plugin-phpunit": "^0.19", - "rector/rector": "^1.1", - "roave/infection-static-analysis-plugin": "^1.35", - "vimeo/psalm": "^5.24" - }, - "type": "library", - "extra": { - "composer-normalize": { - "indent-size": 4, - "indent-style": "space" - } - }, - "autoload": { - "psr-4": { - "WayOfDev\\PhpCsFixer\\Config\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Andrij Orlenko", - "email": "the@wayof.dev" - } - ], - "description": "🧹 Adds custom rule-sets to PHP CS Fixer for consistent coding standards.", - "homepage": "https://wayof.dev", - "keywords": [ - "code-quality", - "code-standards", - "code-style", - "configuration", - "php", - "php-cs-fixer", - "php-cs-fixer-config", - "php-cs-fixer-rules", - "static-analysis" - ], - "support": { - "issues": "https://github.com/wayofdev/php-cs-fixer-config/issues", - "security": "https://github.com/wayofdev/php-cs-fixer-config/blob/master/.github/SECURITY.md", - "source": "https://github.com/wayofdev/php-cs-fixer-config" - }, - "funding": [ - { - "url": "https://github.com/wayofdev", - "type": "github" - } - ], - "time": "2024-06-18T09:13:20+00:00" + "time": "2025-03-31T10:12:50+00:00" }, { "name": "webmozart/assert", @@ -6695,8 +7188,5 @@ "php": ">=8.1" }, "platform-dev": {}, - "platform-overrides": { - "php": "8.1.27" - }, "plugin-api-version": "2.6.0" } diff --git a/pest.xml.dist b/pest.xml.dist deleted file mode 100644 index 2d40f52..0000000 --- a/pest.xml.dist +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - tests/Arch - - - - - src - - - tests - src/Test - - - diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 447a7e3..c96b7ed 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -3,7 +3,7 @@ xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd" bootstrap="vendor/autoload.php" colors="true" - cacheResultFile=".build/phpunit/result.cache" + cacheResultFile="runtime/phpunit/result.cache" failOnWarning="true" failOnRisky="true" executionOrder="random" @@ -17,16 +17,19 @@ tests/Unit + + tests/Arch + - - - + + + - + diff --git a/psalm-baseline.xml b/psalm-baseline.xml index f03abfa..b99872d 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,9 +1,13 @@ - + + + getArguments()]]> + getOptions()]]> + @@ -37,31 +41,21 @@ - - - - - type]]> - uri]]> - - - repositories]]> - homepage]]> - pattern]]> - - files]]> - + + + + @@ -87,34 +81,10 @@ - - - - File::fromArray($fileArray), - $softwareArray['files'] ?? [], - )]]> - Repository::fromArray($repositoryArray), - $softwareArray['repositories'] ?? [], - )]]> - - - - - - - - factory]]> - - - - - class()]]> @@ -123,33 +93,56 @@ + + + + + + + + + factory]]> + + + + + + + cache]]> + + + + + + cache]]> + + + + + - repoConfig->assetPattern]]> + + - - - - - - + + + + ($context->onProgress)(]]> - - repoConfig]]> - - - repoConfig]]> - + + + @@ -170,6 +163,9 @@ + + + @@ -206,6 +202,9 @@ createClient())]]> + + + @@ -288,15 +287,6 @@ name]]> releases]]> - - - - - getName())]]> - - - - @@ -314,22 +304,9 @@ - - - uri]]> - - - - - - - - - - diff --git a/psalm.xml b/psalm.xml index 61df208..48f9878 100644 --- a/psalm.xml +++ b/psalm.xml @@ -6,10 +6,12 @@ findUnusedBaselineEntry="false" findUnusedCode="false" errorBaseline="psalm-baseline.xml" + phpVersion="8.1" > + @@ -18,4 +20,15 @@ + + + + + + + + + + + diff --git a/src/Info.php b/src/Info.php index 4983905..7719c1c 100644 --- a/src/Info.php +++ b/src/Info.php @@ -7,14 +7,11 @@ /** * @internal */ -class Info +final class Info { public const NAME = 'DLoad'; - public const LOGO_CLI_COLOR = ''; - public const ROOT_DIR = __DIR__ . '/..'; - private const VERSION = 'experimental'; /** diff --git a/src/Module/Archive/ArchiveFactory.php b/src/Module/Archive/ArchiveFactory.php index c02bb61..dbbf0f0 100644 --- a/src/Module/Archive/ArchiveFactory.php +++ b/src/Module/Archive/ArchiveFactory.php @@ -126,7 +126,6 @@ private function bootDefaultMatchers(): void * * @param string $extension File extension to match * @param ArchiveMatcher $then Function to create archive handler - * @return ArchiveMatcher */ private function matcher(string $extension, \Closure $then): \Closure { diff --git a/src/Module/Archive/Exception/ArchiveException.php b/src/Module/Archive/Exception/ArchiveException.php index a6c1d26..a72c151 100644 --- a/src/Module/Archive/Exception/ArchiveException.php +++ b/src/Module/Archive/Exception/ArchiveException.php @@ -4,4 +4,4 @@ namespace Internal\DLoad\Module\Archive\Exception; -class ArchiveException extends \RuntimeException {} +final class ArchiveException extends \RuntimeException {} diff --git a/src/Module/Archive/Internal/PharArchive.php b/src/Module/Archive/Internal/PharArchive.php index b386ca3..b47fc0b 100644 --- a/src/Module/Archive/Internal/PharArchive.php +++ b/src/Module/Archive/Internal/PharArchive.php @@ -25,7 +25,6 @@ final class PharArchive extends PharAwareArchive * Opens PHAR archive for reading * * @param \SplFileInfo $file Archive file - * @return \PharData */ protected function open(\SplFileInfo $file): \PharData { diff --git a/src/Module/Archive/Internal/PharAwareArchive.php b/src/Module/Archive/Internal/PharAwareArchive.php index 04e9c25..d0190b7 100644 --- a/src/Module/Archive/Internal/PharAwareArchive.php +++ b/src/Module/Archive/Internal/PharAwareArchive.php @@ -71,7 +71,6 @@ public function extract(): \Generator * Opens archive with specific format * * @param \SplFileInfo $file Archive file - * @return \PharData */ abstract protected function open(\SplFileInfo $file): \PharData; } diff --git a/src/Module/Archive/Internal/TarPharArchive.php b/src/Module/Archive/Internal/TarPharArchive.php index d30c917..fbc3783 100644 --- a/src/Module/Archive/Internal/TarPharArchive.php +++ b/src/Module/Archive/Internal/TarPharArchive.php @@ -24,7 +24,6 @@ final class TarPharArchive extends PharAwareArchive * Opens TAR.GZ archive for reading * * @param \SplFileInfo $file Archive file - * @return \PharData */ protected function open(\SplFileInfo $file): \PharData { diff --git a/src/Module/Archive/Internal/ZipPharArchive.php b/src/Module/Archive/Internal/ZipPharArchive.php index ec96d12..76bbef1 100644 --- a/src/Module/Archive/Internal/ZipPharArchive.php +++ b/src/Module/Archive/Internal/ZipPharArchive.php @@ -30,7 +30,6 @@ final class ZipPharArchive extends PharAwareArchive * Uses ZIP and GZ formats to properly handle ZIP archives. * * @param \SplFileInfo $file Archive file - * @return \PharData */ protected function open(\SplFileInfo $file): \PharData { diff --git a/src/Module/Common/Internal/Injection/ConfigLoader.php b/src/Module/Common/Internal/Injection/ConfigLoader.php index 64c1763..7b2d3da 100644 --- a/src/Module/Common/Internal/Injection/ConfigLoader.php +++ b/src/Module/Common/Internal/Injection/ConfigLoader.php @@ -67,7 +67,6 @@ public function hydrate(object $config): void /** * Injects values into a property based on its configuration attributes. * - * @param \ReflectionProperty $property * @param list<\ReflectionAttribute> $attributes */ private function injectValue(object $config, \ReflectionProperty $property, array $attributes): void diff --git a/src/Module/Repository/Collection/CompositeRepository.php b/src/Module/Repository/Collection/CompositeRepository.php index bdbdd60..2629491 100644 --- a/src/Module/Repository/Collection/CompositeRepository.php +++ b/src/Module/Repository/Collection/CompositeRepository.php @@ -23,7 +23,7 @@ * @internal * @psalm-internal Internal\DLoad\Module */ -class CompositeRepository implements RepositoryInterface +final class CompositeRepository implements RepositoryInterface { /** * @var array diff --git a/src/Module/Repository/Internal/Collection.php b/src/Module/Repository/Internal/Collection.php index a03360d..ef94abb 100644 --- a/src/Module/Repository/Internal/Collection.php +++ b/src/Module/Repository/Internal/Collection.php @@ -125,7 +125,7 @@ public function except(callable $filter): static * @param null|callable(T): bool $filter Optional filter function * @return T|null First matching item or null */ - public function first(callable $filter = null): ?object + public function first(?callable $filter = null): ?object { $self = $filter === null ? $this : $this->filter($filter); @@ -139,7 +139,7 @@ public function first(callable $filter = null): ?object * @param null|callable(T): bool $filter Optional filter function * @return T First matching item or default value */ - public function firstOr(callable $otherwise, callable $filter = null): object + public function firstOr(callable $otherwise, ?callable $filter = null): object { return $this->first($filter) ?? $otherwise(); } diff --git a/src/Module/Repository/Internal/GitHub/GitHubAsset.php b/src/Module/Repository/Internal/GitHub/GitHubAsset.php index d005823..a1ed32e 100644 --- a/src/Module/Repository/Internal/GitHub/GitHubAsset.php +++ b/src/Module/Repository/Internal/GitHub/GitHubAsset.php @@ -67,7 +67,7 @@ public static function fromApiResponse(HttpClientInterface $client, GitHubReleas * * @throws ExceptionInterface */ - public function download(\Closure $progress = null): \Traversable + public function download(?\Closure $progress = null): \Traversable { $response = $this->client->request('GET', $this->getUri(), [ 'on_progress' => $progress, diff --git a/src/Module/Repository/Internal/GitHub/GitHubRelease.php b/src/Module/Repository/Internal/GitHub/GitHubRelease.php index a23539f..d3c41b5 100644 --- a/src/Module/Repository/Internal/GitHub/GitHubRelease.php +++ b/src/Module/Repository/Internal/GitHub/GitHubRelease.php @@ -89,7 +89,6 @@ public function destroy(): void * @note The return value is "pretty", but that does not mean that the tag physically exists. * * @param array{tag_name: string|null, name: string|null} $data - * @return string */ private static function getTagName(array $data): string { diff --git a/src/Module/Repository/Internal/GitHub/GitHubRepository.php b/src/Module/Repository/Internal/GitHub/GitHubRepository.php index 98e296d..096bc72 100644 --- a/src/Module/Repository/Internal/GitHub/GitHubRepository.php +++ b/src/Module/Repository/Internal/GitHub/GitHubRepository.php @@ -23,7 +23,6 @@ final class GitHubRepository implements RepositoryInterface, Destroyable private const URL_RELEASES = 'https://api.github.com/repos/%s/releases'; private HttpClientInterface $client; - private ?ReleasesCollection $releases = null; /** @@ -44,7 +43,7 @@ final class GitHubRepository implements RepositoryInterface, Destroyable * @param non-empty-string $org * @param non-empty-string $repo */ - public function __construct(string $org, string $repo, HttpClientInterface $client = null) + public function __construct(string $org, string $repo, ?HttpClientInterface $client = null) { $this->name = $org . '/' . $repo; $this->client = $client ?? HttpClient::create(); @@ -70,9 +69,6 @@ public function getReleases(): ReleasesCollection }); } - /** - * @return string - */ public function getName(): string { return $this->name; @@ -88,10 +84,6 @@ public function destroy(): void } /** - * @param string $method - * @param string $uri - * @param array $options - * @return ResponseInterface * @throws TransportExceptionInterface * @see HttpClientInterface::request() */ diff --git a/src/Module/Repository/Internal/Release.php b/src/Module/Repository/Internal/Release.php index 854cfa0..a7f3c7c 100644 --- a/src/Module/Repository/Internal/Release.php +++ b/src/Module/Repository/Internal/Release.php @@ -25,13 +25,11 @@ abstract class Release implements ReleaseInterface protected string $name; protected Stability $stability; - protected AssetsCollection $assets; /** * @param non-empty-string $name * @param non-empty-string $version - * @param iterable $assets */ public function __construct( protected RepositoryInterface $repository, diff --git a/tests/Arch/ArchTest.php b/tests/Arch/ArchTest.php new file mode 100644 index 0000000..2599b16 --- /dev/null +++ b/tests/Arch/ArchTest.php @@ -0,0 +1,43 @@ +layer(); + + foreach ($layer as $object) { + foreach ($object->uses as $use) { + foreach ($functions as $function) { + $function === $use and throw new \Exception( + \sprintf( + 'Function `%s()` is used in %s.', + $function, + $object->name, + ), + ); + } + } + } + + $this->assertTrue(true); + } +} diff --git a/tests/Arch/DebugTest.php b/tests/Arch/DebugTest.php deleted file mode 100644 index 8c12935..0000000 --- a/tests/Arch/DebugTest.php +++ /dev/null @@ -1,8 +0,0 @@ -expect(['dd', 'exit', 'die', 'var_dump', 'echo', 'print', 'trap', 'td', 'tr', 'error_log']) - ->not - ->toBeUsed(); From d0ef83e5ee79aa561d700acf3851372d72122667 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sun, 6 Apr 2025 14:12:17 +0400 Subject: [PATCH 14/30] chore(ctx): add testing guideline --- context.yaml | 15 + docs/guidelines/how-to-write-tests.md | 466 ++++++++++++++++++++++++++ 2 files changed, 481 insertions(+) create mode 100644 docs/guidelines/how-to-write-tests.md diff --git a/context.yaml b/context.yaml index f5f117b..098bfda 100644 --- a/context.yaml +++ b/context.yaml @@ -34,3 +34,18 @@ documents: method_visibility: - public keep_method_bodies: false + + # Guidelines + - description: 'Guidelines and instructions' + outputPath: guidelines.md + overwrite: true + sources: + - type: text + tag: instruction + content: | + There are all the guidelines about how to do some things in the project. + Feel free to load any related guideline to the current context to make the work more efficient. + - type: tree + sourcePaths: ['docs/guidelines'] + showCharCount: true + showSize: true diff --git a/docs/guidelines/how-to-write-tests.md b/docs/guidelines/how-to-write-tests.md new file mode 100644 index 0000000..58c421b --- /dev/null +++ b/docs/guidelines/how-to-write-tests.md @@ -0,0 +1,466 @@ +# PHP Unit Testing Guidelines + +This document outlines the standards and best practices for writing unit tests for the project. + +## Test Structure + +- Unit tests should be in `tests/Unit`, integration tests in `tests/Integration`, acceptance tests in `tests/Acceptance`, architecture tests in `tests/Arch`, etc. +- Tests should be organized to mirror the project structure. +- Each PHP class should have a corresponding test class +- Place tests in the appropriate namespace matching the source code structure +- Use `final` keyword for test classes + +``` +Source: src/ExternalContext/LocalExternalContextSource.php +Test: tests/Unit/ExternalContext/LocalExternalContextSourceTest.php +``` + +## Module Testing + +Modules located in `src/Module` are treated as independent units with their own test structure: + +- Each module should have its own test directory with the following structure: + ``` + tests/Unit/Module/{ModuleName}/ + ├── Stub/ (Contains stubs for the module's dependencies) + ├── API/ (Tests for the module's public API) + └── Internal/ (Tests for module's internal implementations) + ``` + +- Internal implementations of a module are located in the `Internal` folder of each module +- Everything outside the `Internal` folder is considered part of the module's API +- Each module's tests should be structured as independent areas with their own stubs + +## Arrange-Act-Assert (AAA) Pattern + +All tests should follow the AAA pattern: + +1. **Arrange**: Set up the test environment and prepare inputs +2. **Act**: Execute the code being tested +3. **Assert**: Verify the results are as expected + +Use comments to separate these sections for clarity: + +```php +public function testFetchContextReturnsDecodedJson(): void +{ + // Arrange + $filePath = '/path/to/context.json'; + $fileContent = '{"key":"value"}'; + $this->fileSystem->method('exists')->willReturn(true); + $this->fileSystem->method('readFile')->willReturn($fileContent); + + // Act + $result = $this->contextSource->fetchContext($filePath); + + // Assert + self::assertSame(['key' => 'value'], $result); +} +``` + +## Naming Conventions + +- Test classes should be named with the pattern `{ClassUnderTest}Test` +- Test methods should follow the pattern `test{MethodName}{Scenario}` +- Use descriptive method names that explain what is being tested + +```php +#[CoversClass(LocalExternalContextSource::class)] +final class LocalExternalContextSourceTest extends TestCase +{ + public function testFetchContextReturnsValidData(): void + { + // Test implementation + } + + public function testFetchContextThrowsExceptionWhenFileNotFound(): void + { + // Test implementation + } +} +``` + +## Test Implementation + +- Use strict typing: `declare(strict_types=1);` +- Use namespaces consistent with the project structure +- Extend `PHPUnit\Framework\TestCase` +- Use assertion methods with descriptive error messages +- Test one behavior per test method +- Use data providers with PHP 8.1+ attributes and generators + +```php +#[DataProvider('provideValidFilePaths')] +public function testFetchContextWithVariousPaths(string $path, array $expectedData): void +{ + // Arrange + $this->fileSystem->method('exists')->willReturn(true); + $this->fileSystem->method('readFile')->willReturn(\json_encode($expectedData)); + + // Act + $result = $this->contextSource->fetchContext($path); + + // Assert + self::assertSame($expectedData, $result); +} + +public static function provideValidFilePaths(): Generator +{ + yield 'relative path' => ['relative/path.json', ['expected' => 'data']]; + yield 'absolute path' => ['/absolute/path.json', ['expected' => 'data']]; +} +``` + +## Test Isolation + +- Each test should be independent of others +- Use setUp() and tearDown() methods for common test preparation and cleanup +- Use test doubles (mocks, stubs) for isolating the code under test from dependencies +- Reset global/static state between tests +- For module tests, use the dedicated Stub directory to store all stub implementations + +```php +protected function setUp(): void +{ + // Arrange (common setup) + $this->fileSystem = $this->createMock(FileSystemInterface::class); + $this->contextSource = new LocalExternalContextSource($this->fileSystem); +} +``` + +### Module-Specific Test Isolation + +When testing modules from `src/Module`: + +- Module tests should use stubs from their dedicated `Stub` directory +- API tests should only rely on the public interfaces of the module, not internal implementations +- Internal tests can have additional stubs specific to internal components +- Cross-module dependencies should be stubbed if possible (for interfaces), treating each module as an independent unit + +```php +// Example of module test setup with stubs +namespace Tests\Unit\Module\Payment\API; + +use Tests\Unit\Module\Payment\Stub\PaymentGatewayStub; +use Tests\Unit\Module\Payment\Stub\LoggerStub; + +final class PaymentProcessorTest extends TestCase +{ + private PaymentGatewayStub $paymentGateway; + private LoggerStub $logger; + + protected function setUp(): void + { + $this->paymentGateway = new PaymentGatewayStub(); + $this->logger = new LoggerStub(); + $this->processor = new PaymentProcessor($this->paymentGateway, $this->logger); + } +} +``` + +## Assertions + +- Use specific assertion methods instead of generic ones +- Provide meaningful failure messages in assertions +- Test both positive and negative scenarios +- Assert state changes and side effects, not just return values + +```php +// Good +self::assertSame('expected', $actual, 'Context data should match the expected format'); + +// Instead of +self::assertTrue($expected === $actual); +``` + +## Error Handling Tests + +When testing exceptions, the AAA pattern is slightly modified: + +```php +public function testFetchContextThrowsExceptionWhenFileNotFound(): void +{ + // Arrange + $filePath = '/non-existent/path.json'; + $this->fileSystem->method('exists')->willReturn(false); + + // Assert (before Act for exceptions) + $this->expectException(ContextSourceException::class); + $this->expectExceptionMessage('Cannot read context from file'); + + // Act + $this->contextSource->fetchContext($filePath); +} +``` + +## Mock Objects + +- Only mock direct dependencies of the class under test +- Mock only what is necessary for the test +- **Never mock final classes** - instead use real instances or create test alternatives +- Prefer typed mock method returns +- Verify critical interactions with mocks + +```php +// Arrange +$this->fileSystem->expects(self::once()) + ->method('readFile') + ->with('/path/to/file.json') + ->willReturn('{"key": "value"}'); +``` + +### Dealing with Final Classes + +When a class under test depends on a final class: + +1. **Use real instances** when possible +2. **Create test doubles** by implementing the same interface +3. **Use wrapper/adapter pattern** to create a non-final class that delegates to the final class +4. **Refactor dependencies** to use interfaces where appropriate + +```php +// Instead of mocking a final class: +final class FileReader +{ + public function readFile(string $path): string {...} +} + +// Create an interface: +interface FileReaderInterface +{ + public function readFile(string $path): string; +} + +// Create a test implementation: +class TestFileReader implements FileReaderInterface +{ + public function readFile(string $path): string + { + return '{"test":"data"}'; + } +} + +// Use in tests: +$fileReader = new TestFileReader(); +$myService = new MyService($fileReader); +``` + +## Additional Modern PHPUnit Features + +### PHP 8.1+ Attributes + +Replace annotations with attributes throughout your tests: + +- `#[CoversClass(ClassUnderTest::class)]` - Specify which class is being tested +- `#[CoversMethod('methodName')]` - Specify which method is being tested +- `#[DataProvider('provideTestData')]` - Link to data provider method +- `#[Group('slow')]` - Categorize tests +- `#[TestDox('Class should handle errors gracefully')]` - Better test documentation + +### Using depends with Attributes + +```php +public function testBasicFunctionality(): SomeClass +{ + // Arrange + $object = new SomeClass(); + + // Act & Assert + self::assertInstanceOf(SomeClass::class, $object); + return $object; +} + +#[Depends('testBasicFunctionality')] +public function testAdvancedFeature(SomeClass $object): void +{ + // Arrange is handled by the dependency + + // Act + $result = $object->advancedMethod(); + + // Assert + self::assertTrue($result); +} +``` + +### Test Extension with Traits + +Use traits to share test functionality: + +```php +trait CreatesTempFilesTrait +{ + private string $tempFilePath; + + protected function createTempFile(string $content): string + { + // Arrange test environment + $this->tempFilePath = sys_get_temp_dir() . '/' . uniqid('test_', true); + file_put_contents($this->tempFilePath, $content); + return $this->tempFilePath; + } + + protected function tearDown(): void + { + // Clean up test environment + if (isset($this->tempFilePath) && file_exists($this->tempFilePath)) { + unlink($this->tempFilePath); + } + parent::tearDown(); + } +} + +final class MyTest extends TestCase +{ + use CreatesTempFilesTrait; + + public function testFileProcessing(): void + { + // Arrange + $path = $this->createTempFile('{"data":"value"}'); + + // Act + $result = $this->processor->process($path); + + // Assert + self::assertNotEmpty($result); + } +} +``` + +## Running Tests + +Run tests using Composer scripts from the project root: + +```bash +# Run all tests with code coverage +composer test + +# Run architecture tests +composer test:arch + +# Run tests with clover coverage report +composer test:cc +``` + +To run specific tests with PHPUnit directly: + +```bash +# Run specific test class +vendor/bin/phpunit tests/Unit/ExternalContext/LocalExternalContextSourceTest.php + +# Run specific test method +vendor/bin/phpunit --filter testFetchContextReturnsValidData tests/Unit/ExternalContext/LocalExternalContextSourceTest.php + +# Run tests by group +vendor/bin/phpunit --group slow +``` + +## Test Coverage + +Code coverage is automatically enabled in the test commands through the `XDEBUG_MODE=coverage` environment variable. + +```bash +# Generate coverage report with clover XML format +composer test:cc +# The report will be available at .build/phpunit/logs/clover.xml + +# Run tests with coverage +composer test +``` + +For local development, you can generate HTML coverage reports: + +```bash +XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-html .build/coverage +``` + +## Static Analysis & Code Quality + +The project includes additional tools to maintain code quality: + +```bash +# Run PHP CS Fixer to check code style +composer cs:diff + +# Fix code style issues +composer cs:fix + +# Run Psalm static analysis +composer psalm + +# Run Infection for mutation testing +composer infect +``` + +## Example Test Class + +```php +fileSystem = $this->createMock(FileSystem::class); + $this->contextSource = new LocalExternalContextSource($this->fileSystem); + } + + public function testFetchContextReturnsDecodedJsonWhenFileExists(): void + { + // Arrange + $filePath = '/path/to/context.json'; + $fileContent = '{"key":"value","nested":{"data":true}}'; + $expectedData = ['key' => 'value', 'nested' => ['data' => true]]; + + $this->fileSystem->method('exists')->with($filePath)->willReturn(true); + $this->fileSystem->method('readFile')->with($filePath)->willReturn($fileContent); + + // Act + $result = $this->contextSource->fetchContext($filePath); + + // Assert + self::assertSame($expectedData, $result); + } + + #[DataProvider('provideInvalidPaths')] + public function testFetchContextThrowsExceptionForInvalidPaths( + string $path, + string $expectedExceptionMessage + ): void { + // Arrange + $this->fileSystem->method('exists')->willReturn(false); + + // Assert (before Act for exceptions) + $this->expectException(ContextSourceException::class); + $this->expectExceptionMessage($expectedExceptionMessage); + + // Act + $this->contextSource->fetchContext($path); + } + + public static function provideInvalidPaths(): Generator + { + yield 'empty path' => ['', 'Cannot read context from empty path']; + yield 'non-existent file' => ['/missing.json', 'File not found: /missing.json']; + } +} +``` From 765b67c492ce8f8eddff8156b535ecb4958e6ba5 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sun, 6 Apr 2025 14:43:27 +0400 Subject: [PATCH 15/30] tests(archive): cover Archive module API --- .../Archive/API/ArchiveExceptionTest.php | 58 ++++++ .../Module/Archive/API/ArchiveFactoryTest.php | 197 ++++++++++++++++++ tests/Unit/Module/Archive/API/ArchiveTest.php | 119 +++++++++++ .../Archive/Stub/ArchiveFixtureGenerator.php | 129 ++++++++++++ .../Unit/Module/Archive/Stub/TestArchive.php | 64 ++++++ 5 files changed, 567 insertions(+) create mode 100644 tests/Unit/Module/Archive/API/ArchiveExceptionTest.php create mode 100644 tests/Unit/Module/Archive/API/ArchiveFactoryTest.php create mode 100644 tests/Unit/Module/Archive/API/ArchiveTest.php create mode 100644 tests/Unit/Module/Archive/Stub/ArchiveFixtureGenerator.php create mode 100644 tests/Unit/Module/Archive/Stub/TestArchive.php diff --git a/tests/Unit/Module/Archive/API/ArchiveExceptionTest.php b/tests/Unit/Module/Archive/API/ArchiveExceptionTest.php new file mode 100644 index 0000000..2bb5423 --- /dev/null +++ b/tests/Unit/Module/Archive/API/ArchiveExceptionTest.php @@ -0,0 +1,58 @@ +getMessage()); + } + + public function testExceptionCanHaveCustomCode(): void + { + // Arrange + $code = 123; + + // Act + $exception = new ArchiveException('Test message', $code); + + // Assert + self::assertSame($code, $exception->getCode()); + } + + public function testExceptionCanHavePreviousException(): void + { + // Arrange + $previous = new \Exception('Previous error'); + + // Act + $exception = new ArchiveException('Test message', 0, $previous); + + // Assert + self::assertSame($previous, $exception->getPrevious()); + } +} diff --git a/tests/Unit/Module/Archive/API/ArchiveFactoryTest.php b/tests/Unit/Module/Archive/API/ArchiveFactoryTest.php new file mode 100644 index 0000000..c4bab0f --- /dev/null +++ b/tests/Unit/Module/Archive/API/ArchiveFactoryTest.php @@ -0,0 +1,197 @@ + ['zip']; + yield 'tar.gz extension' => ['tar.gz']; + yield 'phar extension' => ['phar']; + } + + public static function provideArchiveFiles(): \Generator + { + yield 'zip file' => ['archive.zip', ZipPharArchive::class]; + yield 'tar.gz file' => ['archive.tar.gz', TarPharArchive::class]; + yield 'phar file' => ['archive.phar', PharArchive::class]; + } + + #[DataProvider('provideDefaultSupportedExtensions')] + public function testGetSupportedExtensionsReturnsDefaultExtensions(string $extension): void + { + // Act + $extensions = $this->factory->getSupportedExtensions(); + + // Assert + self::assertContains($extension, $extensions); + } + + #[DataProvider('provideArchiveFiles')] + public function testCreateReturnsCorrectArchiveTypeForExtension( + string $filename, + string $expectedClass, + ): void { + // Skip test if the fixture wasn't created + $extension = \pathinfo($filename, PATHINFO_EXTENSION); + if ($extension === 'gz') { + $extension = 'tar.gz'; + } + + if (!isset(self::$archiveFixtures[$extension])) { + self::markTestSkipped("Archive fixture for {$extension} could not be created"); + } + + // Arrange - use actual file + $filePath = self::$archiveFixtures[$extension]; + $file = new \SplFileInfo($filePath); + + // Act + $archive = $this->factory->create($file); + + // Assert + self::assertInstanceOf($expectedClass, $archive); + } + + public function testExtendAddsCustomMatcher(): void + { + // Arrange + $mockArchive = $this->createMock(Archive::class); + $customExtension = 'custom'; + + $this->factory->extend( + static fn(\SplFileInfo $file) => + \str_ends_with($file->getFilename(), '.custom') ? $mockArchive : null, + [$customExtension], + ); + + $file = $this->createFileInfoMock('test.custom'); + + // Act + $result = $this->factory->create($file); + $extensions = $this->factory->getSupportedExtensions(); + + // Assert + self::assertSame($mockArchive, $result); + self::assertContains($customExtension, $extensions); + } + + public function testCreateThrowsExceptionWhenNoMatcherFound(): void + { + // Arrange + $file = $this->createFileInfoMock('unsupported.format'); + + // Assert + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Can not open the archive "unsupported.format"'); + + // Act + $this->factory->create($file); + } + + public function testExtendPrioritizesNewMatchersOverExisting(): void + { + // Arrange + $mockArchive = $this->createMock(Archive::class); + $zipFile = $this->createFileInfoMock('test.zip'); + + // Override the default zip handler + $this->factory->extend( + static fn(\SplFileInfo $file) => + \str_ends_with($file->getFilename(), '.zip') ? $mockArchive : null, + [], + ); + + // Act + $result = $this->factory->create($zipFile); + + // Assert + self::assertSame($mockArchive, $result); + } + + public function testCreateCollectsErrorsFromFailedMatchers(): void + { + // Arrange + $file = $this->createFileInfoMock('test.zip'); + + // Add matchers that throw exceptions + $this->factory->extend( + static function (): never { + throw new \RuntimeException('First matcher error'); + }, + ); + + $this->factory->extend( + static function (): never { + throw new \RuntimeException('Second matcher error'); + }, + ); + + // Assert + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('First matcher error'); + $this->expectExceptionMessage('Second matcher error'); + + // Act + $this->factory->create($file); + } + + public static function setUpBeforeClass(): void + { + // Define project's test runtime directory + $projectRoot = \dirname(__DIR__, 5); // Five levels up from this file + self::$fixturesDir = $projectRoot . '/runtime/tests/archive-fixtures'; + + // Create archive fixtures + self::$fixtureGenerator = new ArchiveFixtureGenerator(self::$fixturesDir); + self::$archiveFixtures = self::$fixtureGenerator->generateArchives(); + } + + public static function tearDownAfterClass(): void + { + // Clean up fixtures + if (self::$fixtureGenerator !== null) { + self::$fixtureGenerator->cleanup(); + } + } + + protected function setUp(): void + { + // Arrange + $this->factory = new ArchiveFactory(); + } + + /** + * Creates a mock SplFileInfo that returns the given filename + * and is configured as a valid, readable file + */ + private function createFileInfoMock(string $filename): \SplFileInfo + { + $file = $this->createMock(\SplFileInfo::class); + $file->method('getFilename')->willReturn($filename); + $file->method('isFile')->willReturn(true); + $file->method('isReadable')->willReturn(true); + $file->method('getPathname')->willReturn('/path/to/' . $filename); + + return $file; + } +} diff --git a/tests/Unit/Module/Archive/API/ArchiveTest.php b/tests/Unit/Module/Archive/API/ArchiveTest.php new file mode 100644 index 0000000..6927bff --- /dev/null +++ b/tests/Unit/Module/Archive/API/ArchiveTest.php @@ -0,0 +1,119 @@ + new \SplFileInfo('/path/to/file1.txt'), + 'file2.php' => new \SplFileInfo('/path/to/file2.php'), + 'file3.php' => new \SplFileInfo('/path/to/file3.php'), + 'file4.json' => new \SplFileInfo('/path/to/file4.json'), + ]; + + yield 'txt files' => [$files, 'txt', 1]; + yield 'php files' => [$files, 'php', 2]; + yield 'json files' => [$files, 'json', 1]; + yield 'non-existent extension' => [$files, 'jpg', 0]; + } + + public function testExtractYieldsFilesFromArchive(): void + { + // Arrange + $file1 = new \SplFileInfo('/path/to/file1.txt'); + $file2 = new \SplFileInfo('/path/to/file2.txt'); + + $archive = new TestArchive($this->archiveFile); + $archive->addFile('file1.txt', $file1); + $archive->addFile('file2.txt', $file2); + + // Act + $result = []; + foreach ($archive->extract() as $path => $fileInfo) { + $result[$path] = $fileInfo; + } + + // Assert + self::assertCount(2, $result); + self::assertSame($file1, $result['file1.txt']); + self::assertSame($file2, $result['file2.txt']); + } + + public function testExtractThrowsArchiveException(): void + { + // Arrange + $archive = new TestArchive($this->archiveFile); + $archive->throwExceptionOnExtract('Custom error message'); + + // Assert + $this->expectException(ArchiveException::class); + $this->expectExceptionMessage('Custom error message'); + + // Act + \iterator_to_array($archive->extract()); + } + + public function testExtractReturnsDestinationFileWhenRequested(): void + { + // Arrange + $sourceFile = new \SplFileInfo('/path/to/source.txt'); + $destinationFile = new \SplFileInfo('/path/to/destination.txt'); + + $archive = new TestArchive($this->archiveFile); + $archive->addFile('source.txt', $sourceFile); + + // Act + $generator = $archive->extract(); + $path = $generator->key(); + $info = $generator->current(); + $result = $generator->send($destinationFile); + + // Assert + self::assertSame('source.txt', $path); + self::assertSame($sourceFile, $info); + self::assertSame($destinationFile, $result); + } + + #[DataProvider('provideFileTypes')] + public function testExtractFilteringByFileType(array $files, string $extension, int $expectedCount): void + { + // Arrange + $archive = new TestArchive($this->archiveFile); + + foreach ($files as $path => $fileInfo) { + $archive->addFile($path, $fileInfo); + } + + // Act + $extracted = []; + foreach ($archive->extract() as $path => $fileInfo) { + // Filter files by extension + if (\pathinfo($path, PATHINFO_EXTENSION) === $extension) { + $extracted[$path] = $fileInfo; + } + } + + // Assert + self::assertCount($expectedCount, $extracted); + } + + protected function setUp(): void + { + // Arrange + $this->archiveFile = new \SplFileInfo(__FILE__); // Use this file as a valid file + } +} diff --git a/tests/Unit/Module/Archive/Stub/ArchiveFixtureGenerator.php b/tests/Unit/Module/Archive/Stub/ArchiveFixtureGenerator.php new file mode 100644 index 0000000..b305dec --- /dev/null +++ b/tests/Unit/Module/Archive/Stub/ArchiveFixtureGenerator.php @@ -0,0 +1,129 @@ +fixturesDir = $fixturesDir; + + if (!\is_dir($fixturesDir)) { + \mkdir($fixturesDir, 0777, true); + } + } + + /** + * Create test archive files of different formats + * + * Creates a fixture file for each supported archive format + * + * @return array Map of archive type to file path + */ + public function generateArchives(): array + { + $fixtures = []; + + // Create a simple text file for content + $contentFile = $this->fixturesDir . '/test-content.txt'; + \file_put_contents($contentFile, 'This is test content for archive'); + + // Create ZIP archive + $zipPath = $this->fixturesDir . '/archive.zip'; + if ($this->canCreateZip()) { + $zip = new \ZipArchive(); + if ($zip->open($zipPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) === true) { + $zip->addFile($contentFile, 'test-content.txt'); + $zip->close(); + $fixtures['zip'] = $zipPath; + } + } + + // Create TAR.GZ archive + $tarGzPath = $this->fixturesDir . '/archive.tar.gz'; + if ($this->canCreatePhar()) { + try { + $phar = new \PharData($this->fixturesDir . '/archive.tar'); + $phar->addFile($contentFile, 'test-content.txt'); + $phar->compress(\Phar::GZ); + $fixtures['tar.gz'] = $tarGzPath; + } catch (\Exception $e) { + // Skip if creating tar.gz fails + } + } + + // Create PHAR archive + $pharPath = $this->fixturesDir . '/archive.phar'; + if ($this->canCreatePhar() && \ini_get('phar.readonly') == 0) { + try { + $phar = new \Phar($pharPath); + $phar->addFile($contentFile, 'test-content.txt'); + $fixtures['phar'] = $pharPath; + } catch (\Exception $e) { + // Skip if creating phar fails + } + } + + return $fixtures; + } + + /** + * Clean up generated fixtures + */ + public function cleanup(): void + { + $this->removeDirectory($this->fixturesDir); + } + + /** + * Check if ZIP archive creation is possible + */ + private function canCreateZip(): bool + { + return \class_exists(\ZipArchive::class); + } + + /** + * Check if PHAR archive creation is possible + */ + private function canCreatePhar(): bool + { + return \class_exists(\PharData::class); + } + + /** + * Recursively remove a directory and its contents + */ + private function removeDirectory(string $dir): void + { + if (!\is_dir($dir)) { + return; + } + + $items = \scandir($dir); + foreach ($items as $item) { + if ($item === '.' || $item === '..') { + continue; + } + + $path = $dir . '/' . $item; + if (\is_dir($path)) { + $this->removeDirectory($path); + } else { + \unlink($path); + } + } + + \rmdir($dir); + } +} diff --git a/tests/Unit/Module/Archive/Stub/TestArchive.php b/tests/Unit/Module/Archive/Stub/TestArchive.php new file mode 100644 index 0000000..efd6341 --- /dev/null +++ b/tests/Unit/Module/Archive/Stub/TestArchive.php @@ -0,0 +1,64 @@ +throwsException = true; + if ($message !== null) { + $this->exceptionMessage = $message; + } + return $this; + } + + /** + * Add a test file to be returned during extraction + */ + public function addFile(string $path, \SplFileInfo $fileInfo): self + { + $this->files[$path] = $fileInfo; + return $this; + } + + public function extract(): \Generator + { + if ($this->throwsException) { + throw new ArchiveException($this->exceptionMessage); + } + + foreach ($this->files as $path => $fileInfo) { + $fileTo = yield $path => $fileInfo; + + // Simulate file extraction if destination is specified + if ($fileTo instanceof \SplFileInfo) { + // In a real implementation, this would copy the file + // For testing, we just record the extraction occurred + yield $path => $fileTo; + } + } + } +} From 257718332309790d25847ed4715802b42fa7ff4e Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sun, 6 Apr 2025 14:45:34 +0400 Subject: [PATCH 16/30] tests(archive): add integration tests --- phpunit.xml.dist | 3 + .../Module/Archive/ArchiveIntegrationTest.php | 134 ++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 tests/Integration/Module/Archive/ArchiveIntegrationTest.php diff --git a/phpunit.xml.dist b/phpunit.xml.dist index c96b7ed..3d3008e 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -17,6 +17,9 @@ tests/Unit + + tests/Integration + tests/Arch diff --git a/tests/Integration/Module/Archive/ArchiveIntegrationTest.php b/tests/Integration/Module/Archive/ArchiveIntegrationTest.php new file mode 100644 index 0000000..fe3c1d6 --- /dev/null +++ b/tests/Integration/Module/Archive/ArchiveIntegrationTest.php @@ -0,0 +1,134 @@ + ['zip', 'Internal\DLoad\Module\Archive\Internal\ZipPharArchive']; + yield 'tar.gz' => ['tar.gz', 'Internal\DLoad\Module\Archive\Internal\TarPharArchive']; + yield 'phar' => ['phar', 'Internal\DLoad\Module\Archive\Internal\PharArchive']; + } + + #[DataProvider('provideArchiveTypes')] + public function testFactoryCreateReturnsCorrectImplementation( + string $extension, + string $className, + ): void { + // Skip if we can't verify the implementation type + if (!\class_exists($className)) { + self::markTestSkipped("Class $className not available"); + } + + // Arrange - create mock file with extension + $file = $this->createMock(\SplFileInfo::class); + $file->method('getFilename')->willReturn('test.' . $extension); + $file->method('isFile')->willReturn(true); + $file->method('isReadable')->willReturn(true); + + // Act - create archive handler + try { + $archive = $this->factory->create($file); + + // Assert - check implementation type + self::assertInstanceOf($className, $archive); + } catch (\InvalidArgumentException $e) { + // If creation fails due to real file requirements, just verify supported extensions + $extensions = $this->factory->getSupportedExtensions(); + self::assertContains($extension, $extensions); + } + } + + public function testFactoryExtendWithCustomImplementation(): void + { + // Arrange - create custom archive mock + $customArchive = $this->createMock('Internal\DLoad\Module\Archive\Archive'); + + // Register custom implementation for .custom extension + $this->factory->extend( + static fn(\SplFileInfo $file) => + \str_ends_with($file->getFilename(), '.custom') ? $customArchive : null, + ['custom'], + ); + + // Create mock file with custom extension + $file = $this->createMock(\SplFileInfo::class); + $file->method('getFilename')->willReturn('test.custom'); + $file->method('isFile')->willReturn(true); + $file->method('isReadable')->willReturn(true); + + // Act + $archive = $this->factory->create($file); + + // Assert + self::assertSame($customArchive, $archive); + } + + protected function setUp(): void + { + // Skip tests if phar extension is not available + if (!\class_exists(\PharData::class)) { + self::markTestSkipped('Phar extension is not available'); + } + + // Create temporary directory for test files in project runtime + $projectRoot = \dirname(__DIR__, 4); // Four levels up from this file + $this->tempDir = $projectRoot . '/runtime/tests/archive-integration-' . \uniqid(); + \mkdir($this->tempDir, 0777, true); + + // Create factory + $this->factory = new ArchiveFactory(); + } + + protected function tearDown(): void + { + // Clean up temporary directory + if (\is_dir($this->tempDir)) { + $this->removeDirectory($this->tempDir); + } + } + + /** + * Recursively remove a directory and its contents + */ + private function removeDirectory(string $dir): void + { + if (!\is_dir($dir)) { + return; + } + + $items = \scandir($dir); + foreach ($items as $item) { + if ($item === '.' || $item === '..') { + continue; + } + + $path = $dir . '/' . $item; + if (\is_dir($path)) { + $this->removeDirectory($path); + } else { + \unlink($path); + } + } + + \rmdir($dir); + } +} From 7c4d75a0735258a72ab2fd66d215f10a02a67e46 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sun, 6 Apr 2025 14:55:20 +0400 Subject: [PATCH 17/30] tests(archive): add unit tests for internal --- context.yaml | 2 + .../Module/Archive/Internal/ArchiveTest.php | 72 +++++++++++++++++++ .../Archive/Internal/PharAwareArchiveTest.php | 62 ++++++++++++++++ .../Archive/Internal/TarPharArchiveTest.php | 29 ++++++++ .../Archive/Internal/ZipPharArchiveTest.php | 28 ++++++++ 5 files changed, 193 insertions(+) create mode 100644 tests/Unit/Module/Archive/Internal/ArchiveTest.php create mode 100644 tests/Unit/Module/Archive/Internal/PharAwareArchiveTest.php create mode 100644 tests/Unit/Module/Archive/Internal/TarPharArchiveTest.php create mode 100644 tests/Unit/Module/Archive/Internal/ZipPharArchiveTest.php diff --git a/context.yaml b/context.yaml index 098bfda..9d73d63 100644 --- a/context.yaml +++ b/context.yaml @@ -1,3 +1,5 @@ +--- + $schema: 'https://raw.githubusercontent.com/context-hub/generator/refs/heads/main/json-schema.json' import: diff --git a/tests/Unit/Module/Archive/Internal/ArchiveTest.php b/tests/Unit/Module/Archive/Internal/ArchiveTest.php new file mode 100644 index 0000000..2569c3c --- /dev/null +++ b/tests/Unit/Module/Archive/Internal/ArchiveTest.php @@ -0,0 +1,72 @@ +createMock(\SplFileInfo::class); + $file->method('isFile')->willReturn(false); + $file->method('getFilename')->willReturn('non-existent.zip'); + + // Assert + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Archive "non-existent.zip" is not a file.'); + + // Act + $this->createArchiveInstance($file); + } + + public function testConstructorThrowsExceptionWhenFileIsNotReadable(): void + { + // Arrange + $file = $this->createMock(\SplFileInfo::class); + $file->method('isFile')->willReturn(true); + $file->method('isReadable')->willReturn(false); + $file->method('getFilename')->willReturn('unreadable.zip'); + + // Assert + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Archive file "unreadable.zip" is not readable.'); + + // Act + $this->createArchiveInstance($file); + } + + public function testConstructorSucceedsWithValidFile(): void + { + // Arrange + $file = $this->createMock(\SplFileInfo::class); + $file->method('isFile')->willReturn(true); + $file->method('isReadable')->willReturn(true); + + // Act + $archive = $this->createArchiveInstance($file); + + // Assert + self::assertInstanceOf(Archive::class, $archive); + } + + /** + * Creates a concrete implementation of the abstract Archive class for testing + */ + private function createArchiveInstance(\SplFileInfo $file): Archive + { + return new class($file) extends Archive { + public function extract(): \Generator + { + // Minimal implementation for testing the constructor + yield 'test' => new \SplFileInfo('test'); + } + }; + } +} diff --git a/tests/Unit/Module/Archive/Internal/PharAwareArchiveTest.php b/tests/Unit/Module/Archive/Internal/PharAwareArchiveTest.php new file mode 100644 index 0000000..c5cabbf --- /dev/null +++ b/tests/Unit/Module/Archive/Internal/PharAwareArchiveTest.php @@ -0,0 +1,62 @@ +createMock(\PharData::class); + $pharData->method('isReadable')->willReturn(false); + $pharData->method('getPathname')->willReturn('unreadable.phar'); + + $archive = $this->createPharAwareArchive($pharData); + + // Assert + $this->expectException(ArchiveException::class); + $this->expectExceptionMessage('Could not open "unreadable.phar" for reading.'); + + // Act + \iterator_to_array($archive->extract()); + } + + protected function setUp(): void + { + // Arrange + $this->fileInfo = $this->createMock(\SplFileInfo::class); + $this->fileInfo->method('isFile')->willReturn(true); + $this->fileInfo->method('isReadable')->willReturn(true); + } + + /** + * Creates a concrete implementation of the abstract PharAwareArchive for testing + */ + private function createPharAwareArchive(\PharData $pharData): PharAwareArchive + { + return new class($this->fileInfo, $pharData) extends PharAwareArchive { + private \PharData $testPharData; + + public function __construct(\SplFileInfo $archive, \PharData $pharData) + { + $this->testPharData = $pharData; + parent::__construct($archive); + } + + protected function open(\SplFileInfo $file): \PharData + { + return $this->testPharData; + } + }; + } +} diff --git a/tests/Unit/Module/Archive/Internal/TarPharArchiveTest.php b/tests/Unit/Module/Archive/Internal/TarPharArchiveTest.php new file mode 100644 index 0000000..b6c0097 --- /dev/null +++ b/tests/Unit/Module/Archive/Internal/TarPharArchiveTest.php @@ -0,0 +1,29 @@ +createMock(\SplFileInfo::class); + $file->method('isFile')->willReturn(true); + $file->method('isReadable')->willReturn(false); + $file->method('getFilename')->willReturn('unreadable.tar.gz'); + + // Assert + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Archive file "unreadable.tar.gz" is not readable.'); + + // Act + new TarPharArchive($file); + } +} diff --git a/tests/Unit/Module/Archive/Internal/ZipPharArchiveTest.php b/tests/Unit/Module/Archive/Internal/ZipPharArchiveTest.php new file mode 100644 index 0000000..4211987 --- /dev/null +++ b/tests/Unit/Module/Archive/Internal/ZipPharArchiveTest.php @@ -0,0 +1,28 @@ +createMock(\SplFileInfo::class); + $file->method('isFile')->willReturn(false); + $file->method('getFilename')->willReturn('invalid.zip'); + + // Assert + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Archive "invalid.zip" is not a file.'); + + // Act + new ZipPharArchive($file); + } +} From 2a1df0e9d60585f42c4bc6fa88c24fc419c111d2 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sun, 6 Apr 2025 16:12:00 +0400 Subject: [PATCH 18/30] tests(repository): cover ReleasesCollection class --- docs/guidelines/how-to-write-tests.md | 81 +------ .../Collection/ReleasesCollection.php | 2 +- .../Repository/ReleasesCollectionTest.php | 217 ++++++++++++++++++ .../Unit/Module/Repository/Stub/AssetStub.php | 59 +++++ .../Module/Repository/Stub/ReleaseStub.php | 67 ++++++ .../Module/Repository/Stub/RepositoryStub.php | 69 ++++++ 6 files changed, 424 insertions(+), 71 deletions(-) create mode 100644 tests/Unit/Module/Repository/ReleasesCollectionTest.php create mode 100644 tests/Unit/Module/Repository/Stub/AssetStub.php create mode 100644 tests/Unit/Module/Repository/Stub/ReleaseStub.php create mode 100644 tests/Unit/Module/Repository/Stub/RepositoryStub.php diff --git a/docs/guidelines/how-to-write-tests.md b/docs/guidelines/how-to-write-tests.md index 58c421b..51c336a 100644 --- a/docs/guidelines/how-to-write-tests.md +++ b/docs/guidelines/how-to-write-tests.md @@ -6,7 +6,7 @@ This document outlines the standards and best practices for writing unit tests f - Unit tests should be in `tests/Unit`, integration tests in `tests/Integration`, acceptance tests in `tests/Acceptance`, architecture tests in `tests/Arch`, etc. - Tests should be organized to mirror the project structure. -- Each PHP class should have a corresponding test class +- Each concrete PHP class should have a corresponding test class - Place tests in the appropriate namespace matching the source code structure - Use `final` keyword for test classes @@ -15,6 +15,13 @@ Source: src/ExternalContext/LocalExternalContextSource.php Test: tests/Unit/ExternalContext/LocalExternalContextSourceTest.php ``` +## What to Test + +- **Test Concrete Implementations, Not Interfaces**: Interfaces define contracts but don't contain actual logic to test. Only test concrete implementations of interfaces. +- **Focus on Behavior**: Test the behavior of classes rather than their internal implementation details. +- **Test Public API**: Focus on testing the public methods and functionality that clients of the class will use. +- **Test Edge Cases**: Include tests for boundary conditions, invalid inputs, and error scenarios. + ## Module Testing Modules located in `src/Module` are treated as independent units with their own test structure: @@ -23,12 +30,11 @@ Modules located in `src/Module` are treated as independent units with their own ``` tests/Unit/Module/{ModuleName}/ ├── Stub/ (Contains stubs for the module's dependencies) - ├── API/ (Tests for the module's public API) └── Internal/ (Tests for module's internal implementations) ``` - Internal implementations of a module are located in the `Internal` folder of each module -- Everything outside the `Internal` folder is considered part of the module's API +- Tests for public classes of the module should be placed directly in the module's test directory corresponding to the source code structure - Each module's tests should be structured as independent areas with their own stubs ## Arrange-Act-Assert (AAA) Pattern @@ -133,13 +139,13 @@ protected function setUp(): void When testing modules from `src/Module`: - Module tests should use stubs from their dedicated `Stub` directory -- API tests should only rely on the public interfaces of the module, not internal implementations +- Tests should only rely on the public interfaces of the module, not internal implementations - Internal tests can have additional stubs specific to internal components - Cross-module dependencies should be stubbed if possible (for interfaces), treating each module as an independent unit ```php // Example of module test setup with stubs -namespace Tests\Unit\Module\Payment\API; +namespace Tests\Unit\Module\Payment; use Tests\Unit\Module\Payment\Stub\PaymentGatewayStub; use Tests\Unit\Module\Payment\Stub\LoggerStub; @@ -328,71 +334,6 @@ final class MyTest extends TestCase } ``` -## Running Tests - -Run tests using Composer scripts from the project root: - -```bash -# Run all tests with code coverage -composer test - -# Run architecture tests -composer test:arch - -# Run tests with clover coverage report -composer test:cc -``` - -To run specific tests with PHPUnit directly: - -```bash -# Run specific test class -vendor/bin/phpunit tests/Unit/ExternalContext/LocalExternalContextSourceTest.php - -# Run specific test method -vendor/bin/phpunit --filter testFetchContextReturnsValidData tests/Unit/ExternalContext/LocalExternalContextSourceTest.php - -# Run tests by group -vendor/bin/phpunit --group slow -``` - -## Test Coverage - -Code coverage is automatically enabled in the test commands through the `XDEBUG_MODE=coverage` environment variable. - -```bash -# Generate coverage report with clover XML format -composer test:cc -# The report will be available at .build/phpunit/logs/clover.xml - -# Run tests with coverage -composer test -``` - -For local development, you can generate HTML coverage reports: - -```bash -XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-html .build/coverage -``` - -## Static Analysis & Code Quality - -The project includes additional tools to maintain code quality: - -```bash -# Run PHP CS Fixer to check code style -composer cs:diff - -# Fix code style issues -composer cs:fix - -# Run Psalm static analysis -composer psalm - -# Run Infection for mutation testing -composer infect -``` - ## Example Test Class ```php diff --git a/src/Module/Repository/Collection/ReleasesCollection.php b/src/Module/Repository/Collection/ReleasesCollection.php index e1fca93..9dcc330 100644 --- a/src/Module/Repository/Collection/ReleasesCollection.php +++ b/src/Module/Repository/Collection/ReleasesCollection.php @@ -68,7 +68,7 @@ public function withAssets(): self } /** - * Sorts releases by version in descending order (newest first). + * Sorts releases by version in descending order (newest first) and maintain index association. * * @return $this New sorted collection */ diff --git a/tests/Unit/Module/Repository/ReleasesCollectionTest.php b/tests/Unit/Module/Repository/ReleasesCollectionTest.php new file mode 100644 index 0000000..53f6e34 --- /dev/null +++ b/tests/Unit/Module/Repository/ReleasesCollectionTest.php @@ -0,0 +1,217 @@ +collection->satisfies('^1.0.0'); + + // Assert + self::assertCount(2, $result, 'Should only include 1.0.0 and 1.5.0 versions'); + + $versions = \array_map( + static fn(ReleaseInterface $release): string => $release->getName(), + \iterator_to_array($result), + ); + + self::assertContains('1.0.0', $versions); + self::assertContains('1.5.0', $versions); + self::assertNotContains('2.0.0', $versions); + } + + public function testNotSatisfiesFiltersOutReleasesByVersionConstraint(): void + { + // Act + $result = $this->collection->notSatisfies('^1.0.0'); + + // Assert + self::assertGreaterThan(0, $result->count()); + self::assertNotContains('1.0.0', $this->getVersionsFromCollection($result)); + self::assertNotContains('1.5.0', $this->getVersionsFromCollection($result)); + self::assertContains('2.0.0', $this->getVersionsFromCollection($result)); + } + + public function testStabilityFiltersReleasesByExactStability(): void + { + // Act + $result = $this->collection->stability(Stability::Beta); + + // Assert + self::assertCount(1, $result); + self::assertSame('2.1.0-beta', $result->first()->getName()); + } + + public function testStableFiltersToOnlyStableReleases(): void + { + // Act + $result = $this->collection->stable(); + + // Assert + self::assertCount(3, $result, 'Should only include stable versions'); + + foreach ($result as $release) { + self::assertSame(Stability::Stable, $release->getStability()); + } + } + + public function testMinimumStabilityFiltersReleasesByMinimumStabilityLevel(): void + { + // Act + $result = $this->collection->minimumStability(Stability::RC); + + // Assert + $stabilities = []; + foreach ($result as $release) { + $stabilities[] = $release->getStability(); + } + + self::assertContains(Stability::Stable, $stabilities); + self::assertContains(Stability::RC, $stabilities); + self::assertNotContains(Stability::Beta, $stabilities); + self::assertNotContains(Stability::Alpha, $stabilities); + } + + public function testSortByVersionSortsReleasesByVersionDescending(): void + { + // Act + $result = $this->collection->sortByVersion(); + $versions = $this->getVersionsFromCollection($result); + + // Assert + self::assertSame([ + // Expected order by semantic version, newest first + '2.1.0-beta', + '2.1.0-alpha', + '2.0.1-rc1', + '2.0.0', + '1.5.0', + '1.0.0', + ], \array_values($versions)); + } + + public function testChainedFiltersWorkCorrectly(): void + { + // Act - Get stable releases that satisfy version constraint and sort them + $result = $this->collection + ->stable() + ->satisfies('^1.0.0') + ->sortByVersion(); + + // Assert + $versions = $this->getVersionsFromCollection($result); + self::assertSame(['1.5.0', '1.0.0'], \array_values($versions)); + } + + public function testWithAssetsFiltersReleasesWithAssets(): void + { + // Arrange - Set up assets for the second release (1.5.0) + $release = $this->releases[1]; // 1.5.0 release + + $assets = [ + new AssetStub( + $release, + 'package-1.5.0-linux-x64.tar.gz', + 'https://example.com/downloads/package-1.5.0-linux-x64.tar.gz', + OperatingSystem::Linux, + Architecture::X86_64, + ) + ]; + + $this->repository->setAssets($assets, $release); + + // Act + $result = $this->collection->withAssets(); + + // Assert + self::assertCount(1, $result); + self::assertSame('1.5.0', $result->first()->getName()); + self::assertCount(1, $result->first()->getAssets()); + } + + protected function setUp(): void + { + // Arrange + $this->repository = new RepositoryStub('vendor/package', []); + + // Create a series of releases with different versions and stabilities + $this->releases = [ + new ReleaseStub( + $this->repository, + '2.0.0', + 'v2.0.0', + Stability::Stable, + [], + ), + new ReleaseStub( + $this->repository, + '1.5.0', + 'v1.5.0', + Stability::Stable, + [], + ), + new ReleaseStub( + $this->repository, + '1.0.0', + 'v1.0.0', + Stability::Stable, + [], + ), + new ReleaseStub( + $this->repository, + '2.1.0-beta', + 'v2.1.0-beta', + Stability::Beta, + [], + ), + new ReleaseStub( + $this->repository, + '2.1.0-alpha', + 'v2.1.0-alpha', + Stability::Alpha, + [], + ), + new ReleaseStub( + $this->repository, + '2.0.1-rc1', + 'v2.0.1-rc1', + Stability::RC, + [], + ), + ]; + + // Create the collection with all releases + $this->collection = new ReleasesCollection($this->releases); + } + + /** + * Helper method to extract version names from a collection + */ + private function getVersionsFromCollection(ReleasesCollection $collection): array + { + return \array_map( + static fn(ReleaseInterface $release): string => $release->getName(), + \iterator_to_array($collection), + ); + } +} diff --git a/tests/Unit/Module/Repository/Stub/AssetStub.php b/tests/Unit/Module/Repository/Stub/AssetStub.php new file mode 100644 index 0000000..0c14637 --- /dev/null +++ b/tests/Unit/Module/Repository/Stub/AssetStub.php @@ -0,0 +1,59 @@ +release; + } + + public function getName(): string + { + return $this->name; + } + + public function getUri(): string + { + return $this->uri; + } + + public function getOperatingSystem(): ?OperatingSystem + { + return $this->operatingSystem; + } + + public function getArchitecture(): ?Architecture + { + return $this->architecture; + } + + public function download(): \Traversable + { + // Simulate a download stream with mock content + yield 'Mock download content for ' . $this->name; + } +} diff --git a/tests/Unit/Module/Repository/Stub/ReleaseStub.php b/tests/Unit/Module/Repository/Stub/ReleaseStub.php new file mode 100644 index 0000000..9464de9 --- /dev/null +++ b/tests/Unit/Module/Repository/Stub/ReleaseStub.php @@ -0,0 +1,67 @@ + $assets + */ + public function __construct( + private RepositoryInterface $repository, + private string $name, + private string $version, + private Stability $stability, + private array $assets = [], + ) {} + + public function getRepository(): RepositoryInterface + { + return $this->repository; + } + + public function getName(): string + { + return $this->name; + } + + public function getVersion(): string + { + return $this->version; + } + + public function getStability(): Stability + { + return $this->stability; + } + + public function getAssets(): AssetsCollection + { + // For repository stub integration + if ($this->repository instanceof RepositoryStub) { + $this->assets = $this->repository->getAssetsForRelease($this); + } + + return new AssetsCollection($this->assets); + } + + public function satisfies(string $constraint): bool + { + // Using Composer's semver for consistent version comparison + return Semver::satisfies($this->name, $constraint); + } +} diff --git a/tests/Unit/Module/Repository/Stub/RepositoryStub.php b/tests/Unit/Module/Repository/Stub/RepositoryStub.php new file mode 100644 index 0000000..dcccb2c --- /dev/null +++ b/tests/Unit/Module/Repository/Stub/RepositoryStub.php @@ -0,0 +1,69 @@ + + */ + private array $releases; + + /** + * @var array> Mapping of release name to assets + */ + private array $assetsMap = []; + + /** + * @param non-empty-string $name + * @param array $releases + */ + public function __construct( + private string $name, + array $releases, + ) { + $this->releases = $releases; + } + + public function getName(): string + { + return $this->name; + } + + public function getReleases(): ReleasesCollection + { + return new ReleasesCollection($this->releases); + } + + /** + * Set assets for a specific release in this repository. + * Helper method for testing. + * + * @param array $assets + */ + public function setAssets(array $assets, ReleaseInterface $release): void + { + $this->assetsMap[$release->getName()] = $assets; + } + + /** + * Get assets for a specific release. + * Helper method for testing. + * + * @return array + */ + public function getAssetsForRelease(ReleaseInterface $release): array + { + return $this->assetsMap[$release->getName()] ?? []; + } +} From 2107a52e84b46080764d2a58517a2cf104b5ff3e Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sun, 6 Apr 2025 16:16:40 +0400 Subject: [PATCH 19/30] tests(repository): cover AssetsCollection class --- .../Repository/AssetsCollectionTest.php | 219 ++++++++++++++++++ 1 file changed, 219 insertions(+) create mode 100644 tests/Unit/Module/Repository/AssetsCollectionTest.php diff --git a/tests/Unit/Module/Repository/AssetsCollectionTest.php b/tests/Unit/Module/Repository/AssetsCollectionTest.php new file mode 100644 index 0000000..5c881c3 --- /dev/null +++ b/tests/Unit/Module/Repository/AssetsCollectionTest.php @@ -0,0 +1,219 @@ + [ + '/^package-1\.2\.3-linux-x64\.tar\.gz$/', + ['package-1.2.3-linux-x64.tar.gz'], + ]; + + yield 'all linux assets' => [ + '/linux/', + [ + 'package-1.2.3-linux-x64.tar.gz', + 'package-1.2.3-linux-arm64.tar.gz', + ], + ]; + + yield 'all tar.gz assets' => [ + '/\.tar\.gz$/', + [ + 'package-1.2.3-linux-x64.tar.gz', + 'package-1.2.3-linux-arm64.tar.gz', + 'package-1.2.3-darwin-x64.tar.gz', + ], + ]; + + yield 'all zip assets' => [ + '/\.zip$/', + [ + 'package-1.2.3-windows-x64.zip', + 'package-1.2.3-source.zip', + ], + ]; + + yield 'no matches' => [ + '/nonexistent/', + [], + ]; + } + + public function testWhereOperatingSystemFiltersAssetsByOs(): void + { + // Act + $result = $this->collection->whereOperatingSystem(OperatingSystem::Linux); + + // Assert + self::assertCount(2, $result); + + foreach ($result as $asset) { + self::assertSame(OperatingSystem::Linux, $asset->getOperatingSystem()); + } + } + + public function testWhereArchitectureFiltersAssetsByArchitecture(): void + { + // Act + $result = $this->collection->whereArchitecture(Architecture::ARM_64); + + // Assert + self::assertCount(3, $result); + + foreach ($result as $asset) { + self::assertSame(Architecture::ARM_64, $asset->getArchitecture()); + } + } + + #[DataProvider('provideNamePatterns')] + public function testWhereNameMatchesFiltersAssetsByNamePattern( + string $pattern, + array $expectedMatches, + ): void { + // Act + $result = $this->collection->whereNameMatches($pattern); + + // Assert + self::assertCount(\count($expectedMatches), $result); + + $actualNames = \array_map( + static fn($asset) => $asset->getName(), + \iterator_to_array($result), + ); + + foreach ($expectedMatches as $expectedName) { + self::assertContains($expectedName, $actualNames); + } + } + + public function testWhereNameMatchesWithInvalidPattern(): void + { + // Act + $new = $this->collection->whereNameMatches('/invalid[pattern/'); + + // Assert + self::assertCount(0, $new); + } + + public function testChainedFiltersWorkCorrectly(): void + { + // Act + $result = $this->collection + ->whereOperatingSystem(OperatingSystem::Linux) + ->whereArchitecture(Architecture::ARM_64); + + // Assert + self::assertCount(1, $result); + $asset = $result->first(); + self::assertSame('package-1.2.3-linux-arm64.tar.gz', $asset->getName()); + } + + public function testFirstReturnsFirstAssetOrNull(): void + { + // Act with non-empty collection + $first = $this->collection->first(); + + // Assert + self::assertNotNull($first); + self::assertSame('package-1.2.3-linux-x64.tar.gz', $first->getName()); + + // Act with empty collection + $empty = new AssetsCollection([]); + $result = $empty->first(); + + // Assert + self::assertNull($result); + } + + public function testEmptyReturnsTrueForEmptyCollection(): void + { + // Act with non-empty collection + $resultNonEmpty = $this->collection->empty(); + + // Assert + self::assertFalse($resultNonEmpty); + + // Act with empty collection + $empty = new AssetsCollection([]); + $resultEmpty = $empty->empty(); + + // Assert + self::assertTrue($resultEmpty); + } + + protected function setUp(): void + { + // Arrange + $this->repository = new RepositoryStub('vendor/package', []); + $this->release = new ReleaseStub( + $this->repository, + '1.2.3', + 'v1.2.3', + Stability::Stable, + [], + ); + + // Create a variety of assets with different characteristics + $this->assets = [ + new AssetStub( + $this->release, + 'package-1.2.3-linux-x64.tar.gz', + 'https://example.com/downloads/package-1.2.3-linux-x64.tar.gz', + OperatingSystem::Linux, + Architecture::X86_64, + ), + new AssetStub( + $this->release, + 'package-1.2.3-linux-arm64.tar.gz', + 'https://example.com/downloads/package-1.2.3-linux-arm64.tar.gz', + OperatingSystem::Linux, + Architecture::ARM_64, + ), + new AssetStub( + $this->release, + 'package-1.2.3-windows-x64.zip', + 'https://example.com/downloads/package-1.2.3-windows-x64.zip', + OperatingSystem::Windows, + Architecture::ARM_64, + ), + new AssetStub( + $this->release, + 'package-1.2.3-darwin-x64.tar.gz', + 'https://example.com/downloads/package-1.2.3-darwin-x64.tar.gz', + OperatingSystem::Darwin, + Architecture::ARM_64, + ), + new AssetStub( + $this->release, + 'package-1.2.3-source.zip', + 'https://example.com/downloads/package-1.2.3-source.zip', + null, + null, + ), + ]; + + $this->collection = new AssetsCollection($this->assets); + } +} From 756c0f822081d289a5409ab42a73c7d23717ca20 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sun, 6 Apr 2025 17:09:44 +0400 Subject: [PATCH 20/30] feat(repository): add `RepositoryFactory` interface to improve extensibility --- src/Bootstrap.php | 7 + src/Module/Downloader/Downloader.php | 6 +- .../Collection/CompositeRepository.php | 8 +- .../Repository/Internal/GitHub/Factory.php | 16 +- .../Internal/GitHub/GitHubRepository.php | 4 +- src/Module/Repository/Internal/Release.php | 6 +- src/Module/Repository/ReleaseInterface.php | 4 +- ...RepositoryInterface.php => Repository.php} | 2 +- src/Module/Repository/RepositoryFactory.php | 30 ++++ src/Module/Repository/RepositoryProvider.php | 39 +++-- .../Repository/ReleasesCollectionTest.php | 2 +- .../Repository/RepositoryProviderTest.php | 163 ++++++++++++++++++ .../Collection/ReleasesCollectionStub.php | 61 +++++++ .../Module/Repository/Stub/ReleaseStub.php | 6 +- .../Repository/Stub/RepositoryFactoryStub.php | 42 +++++ .../Module/Repository/Stub/RepositoryStub.php | 57 ++---- 16 files changed, 368 insertions(+), 85 deletions(-) rename src/Module/Repository/{RepositoryInterface.php => Repository.php} (97%) create mode 100644 src/Module/Repository/RepositoryFactory.php create mode 100644 tests/Unit/Module/Repository/RepositoryProviderTest.php create mode 100644 tests/Unit/Module/Repository/Stub/Collection/ReleasesCollectionStub.php create mode 100644 tests/Unit/Module/Repository/Stub/RepositoryFactoryStub.php diff --git a/src/Bootstrap.php b/src/Bootstrap.php index ba8964f..3ca60a9 100644 --- a/src/Bootstrap.php +++ b/src/Bootstrap.php @@ -9,6 +9,8 @@ use Internal\DLoad\Module\Common\Internal\ObjectContainer; use Internal\DLoad\Module\Common\OperatingSystem; use Internal\DLoad\Module\Common\Stability; +use Internal\DLoad\Module\Repository\Internal\GitHub\Factory as GithubRepositoryFactory; +use Internal\DLoad\Module\Repository\RepositoryProvider; use Internal\DLoad\Service\Container; /** @@ -93,6 +95,11 @@ public function withConfig( $this->container->bind(Architecture::class); $this->container->bind(OperatingSystem::class); $this->container->bind(Stability::class); + $this->container->bind( + RepositoryProvider::class, + static fn(Container $container): RepositoryProvider => (new RepositoryProvider()) + ->addRepositoryFactory($container->get(GithubRepositoryFactory::class)), + ); return $this; } diff --git a/src/Module/Downloader/Downloader.php b/src/Module/Downloader/Downloader.php index ca11a96..c26af5f 100644 --- a/src/Module/Downloader/Downloader.php +++ b/src/Module/Downloader/Downloader.php @@ -16,7 +16,7 @@ use Internal\DLoad\Module\Downloader\Task\DownloadTask; use Internal\DLoad\Module\Repository\AssetInterface; use Internal\DLoad\Module\Repository\ReleaseInterface; -use Internal\DLoad\Module\Repository\RepositoryInterface; +use Internal\DLoad\Module\Repository\Repository; use Internal\DLoad\Module\Repository\RepositoryProvider; use Internal\DLoad\Service\Destroyable; use Internal\DLoad\Service\Logger; @@ -115,11 +115,11 @@ public function download( * * Fetches and filters releases from the repository based on stability and version constraints. * - * @param RepositoryInterface $repository Repository to process + * @param Repository $repository Repository to process * @param DownloadContext $context Download context information * @return \Closure(): ReleaseInterface Closure that returns the selected release */ - private function processRepository(RepositoryInterface $repository, DownloadContext $context): \Closure + private function processRepository(Repository $repository, DownloadContext $context): \Closure { return function () use ($repository, $context): ReleaseInterface { $this->logger->info( diff --git a/src/Module/Repository/Collection/CompositeRepository.php b/src/Module/Repository/Collection/CompositeRepository.php index 2629491..1e509fc 100644 --- a/src/Module/Repository/Collection/CompositeRepository.php +++ b/src/Module/Repository/Collection/CompositeRepository.php @@ -4,7 +4,7 @@ namespace Internal\DLoad\Module\Repository\Collection; -use Internal\DLoad\Module\Repository\RepositoryInterface; +use Internal\DLoad\Module\Repository\Repository; /** * Collection of repositories that also implements the repository interface. @@ -23,15 +23,15 @@ * @internal * @psalm-internal Internal\DLoad\Module */ -final class CompositeRepository implements RepositoryInterface +final class CompositeRepository implements Repository { /** - * @var array + * @var array */ private array $repositories; /** - * @param array $repositories List of repositories to include + * @param array $repositories List of repositories to include */ public function __construct(array $repositories) { diff --git a/src/Module/Repository/Internal/GitHub/Factory.php b/src/Module/Repository/Internal/GitHub/Factory.php index 3a9ea3e..55ad829 100644 --- a/src/Module/Repository/Internal/GitHub/Factory.php +++ b/src/Module/Repository/Internal/GitHub/Factory.php @@ -4,7 +4,9 @@ namespace Internal\DLoad\Module\Repository\Internal\GitHub; +use Internal\DLoad\Module\Common\Config\Embed\Repository as RepositoryConfig; use Internal\DLoad\Module\Common\Config\GitHub as GitHubConfig; +use Internal\DLoad\Module\Repository\RepositoryFactory; use Symfony\Component\HttpClient\HttpClient; use Symfony\Contracts\HttpClient\HttpClientInterface; @@ -12,18 +14,20 @@ * @internal * @psalm-internal Internal\DLoad\Module\Repository */ -final class Factory +final class Factory implements RepositoryFactory { public function __construct( private readonly GitHubConfig $config, ) {} - /** - * @param non-empty-string $uri Package name in format "owner/repository" or full URL - */ - public function create(string $uri): GitHubRepository + public function supports(RepositoryConfig $config): bool { - $uri = \parse_url($uri, PHP_URL_PATH) ?? $uri; + return \strtolower($config->type) === 'github'; + } + + public function create(RepositoryConfig $config): GitHubRepository + { + $uri = \parse_url($config->uri, PHP_URL_PATH) ?? $config->uri; [$org, $repo] = \array_slice(\explode('/', $uri), -2); return new GitHubRepository($org, $repo, $this->createClient()); diff --git a/src/Module/Repository/Internal/GitHub/GitHubRepository.php b/src/Module/Repository/Internal/GitHub/GitHubRepository.php index 096bc72..684177c 100644 --- a/src/Module/Repository/Internal/GitHub/GitHubRepository.php +++ b/src/Module/Repository/Internal/GitHub/GitHubRepository.php @@ -5,7 +5,7 @@ namespace Internal\DLoad\Module\Repository\Internal\GitHub; use Internal\DLoad\Module\Repository\Collection\ReleasesCollection; -use Internal\DLoad\Module\Repository\RepositoryInterface; +use Internal\DLoad\Module\Repository\Repository; use Internal\DLoad\Service\Destroyable; use Symfony\Component\HttpClient\HttpClient; use Symfony\Contracts\HttpClient\Exception\ExceptionInterface; @@ -18,7 +18,7 @@ * @internal * @psalm-internal Internal\DLoad\Module\Repository\GitHub */ -final class GitHubRepository implements RepositoryInterface, Destroyable +final class GitHubRepository implements Repository, Destroyable { private const URL_RELEASES = 'https://api.github.com/repos/%s/releases'; diff --git a/src/Module/Repository/Internal/Release.php b/src/Module/Repository/Internal/Release.php index a7f3c7c..7b9213b 100644 --- a/src/Module/Repository/Internal/Release.php +++ b/src/Module/Repository/Internal/Release.php @@ -9,7 +9,7 @@ use Internal\DLoad\Module\Common\Stability; use Internal\DLoad\Module\Repository\Collection\AssetsCollection; use Internal\DLoad\Module\Repository\ReleaseInterface; -use Internal\DLoad\Module\Repository\RepositoryInterface; +use Internal\DLoad\Module\Repository\Repository; /** * @internal @@ -32,7 +32,7 @@ abstract class Release implements ReleaseInterface * @param non-empty-string $version */ public function __construct( - protected RepositoryInterface $repository, + protected Repository $repository, string $name, protected string $version, ?Stability $stability = null, @@ -43,7 +43,7 @@ public function __construct( $this->stability = $stability ?? $this->parseStability($version); } - public function getRepository(): RepositoryInterface + public function getRepository(): Repository { return $this->repository; } diff --git a/src/Module/Repository/ReleaseInterface.php b/src/Module/Repository/ReleaseInterface.php index 7cfa6f3..4528dbe 100644 --- a/src/Module/Repository/ReleaseInterface.php +++ b/src/Module/Repository/ReleaseInterface.php @@ -29,9 +29,9 @@ interface ReleaseInterface /** * Returns the repository this release belongs to. * - * @return RepositoryInterface The parent repository + * @return Repository The parent repository */ - public function getRepository(): RepositoryInterface; + public function getRepository(): Repository; /** * Returns Composer's compatible "pretty" release version. diff --git a/src/Module/Repository/RepositoryInterface.php b/src/Module/Repository/Repository.php similarity index 97% rename from src/Module/Repository/RepositoryInterface.php rename to src/Module/Repository/Repository.php index de72362..2297e90 100644 --- a/src/Module/Repository/RepositoryInterface.php +++ b/src/Module/Repository/Repository.php @@ -18,7 +18,7 @@ * $filteredReleases = $releases->satisfies('^2.0.0')->sortByVersion(); * ``` */ -interface RepositoryInterface +interface Repository { /** * Returns the unique identifier of the repository. diff --git a/src/Module/Repository/RepositoryFactory.php b/src/Module/Repository/RepositoryFactory.php new file mode 100644 index 0000000..00ec5d7 --- /dev/null +++ b/src/Module/Repository/RepositoryFactory.php @@ -0,0 +1,30 @@ +factories[] = $factory; + return $this; + } /** * Creates a repository instance based on the provided configuration. * - * @param Repository $config Repository configuration - * @return RepositoryInterface Created repository instance - * @throws \RuntimeException When an unknown repository type is specified + * @param RepositoryConfig $config Repository configuration + * @return Repository Created repository instance + * @throws \RuntimeException When no factory supports the repository type */ - public function getByConfig(Repository $config): RepositoryInterface + public function getByConfig(RepositoryConfig $config): Repository { - return match (\strtolower($config->type)) { - 'github' => $this->githubFactory->create($config->uri), - default => throw new \RuntimeException("Unknown repository type `$config->type`."), - }; + foreach ($this->factories as $factory) { + if ($factory->supports($config)) { + return $factory->create($config); + } + } + + throw new \RuntimeException("No factory found for repository type `$config->type`."); } } diff --git a/tests/Unit/Module/Repository/ReleasesCollectionTest.php b/tests/Unit/Module/Repository/ReleasesCollectionTest.php index 53f6e34..b1e109a 100644 --- a/tests/Unit/Module/Repository/ReleasesCollectionTest.php +++ b/tests/Unit/Module/Repository/ReleasesCollectionTest.php @@ -135,7 +135,7 @@ public function testWithAssetsFiltersReleasesWithAssets(): void 'https://example.com/downloads/package-1.5.0-linux-x64.tar.gz', OperatingSystem::Linux, Architecture::X86_64, - ) + ), ]; $this->repository->setAssets($assets, $release); diff --git a/tests/Unit/Module/Repository/RepositoryProviderTest.php b/tests/Unit/Module/Repository/RepositoryProviderTest.php new file mode 100644 index 0000000..8f82a6d --- /dev/null +++ b/tests/Unit/Module/Repository/RepositoryProviderTest.php @@ -0,0 +1,163 @@ +type = 'github'; + $githubConfig->uri = 'vendor/package'; + + $gitlabConfig = new RepositoryConfig(); + $gitlabConfig->type = 'gitlab'; + $gitlabConfig->uri = 'group/project'; + $gitlabConfig->assetPattern = '/^release-.*$/'; + + $customConfig = new RepositoryConfig(); + $customConfig->type = 'custom'; + $customConfig->uri = 'https://example.com/repo'; + + yield 'github config with support' => [$githubConfig, true]; + yield 'gitlab config with support' => [$gitlabConfig, true]; + yield 'custom config without support' => [$customConfig, false]; + } + + public function testAddRepositoryFactoryReturnsSelf(): void + { + // Arrange + $factory = $this->createMock(RepositoryFactory::class); + + // Act + $result = $this->repositoryProvider->addRepositoryFactory($factory); + + // Assert + self::assertSame($this->repositoryProvider, $result); + } + + public function testGetByConfigReturnsRepositoryFromSupportingFactory(): void + { + // Arrange + $config = new RepositoryConfig(); + $config->type = 'github'; + $config->uri = 'vendor/package'; + + $repository = $this->createMock(Repository::class); + + $unsupportedFactory = $this->createMock(RepositoryFactory::class); + $unsupportedFactory->method('supports')->with($config)->willReturn(false); + $unsupportedFactory->expects(self::never())->method('create'); + + $supportedFactory = $this->createMock(RepositoryFactory::class); + $supportedFactory->method('supports')->with($config)->willReturn(true); + $supportedFactory->method('create')->with($config)->willReturn($repository); + + // Add factories to provider (order matters - first unsupported, then supported) + $this->repositoryProvider->addRepositoryFactory($unsupportedFactory); + $this->repositoryProvider->addRepositoryFactory($supportedFactory); + + // Act + $result = $this->repositoryProvider->getByConfig($config); + + // Assert + self::assertSame($repository, $result); + } + + public function testGetByConfigUsesFirstSupportingFactory(): void + { + // Arrange + $config = new RepositoryConfig(); + $config->type = 'github'; + $config->uri = 'vendor/package'; + + $repository1 = $this->createMock(Repository::class); + $repository2 = $this->createMock(Repository::class); + + $firstFactory = $this->createMock(RepositoryFactory::class); + $firstFactory->method('supports')->with($config)->willReturn(true); + $firstFactory->method('create')->with($config)->willReturn($repository1); + + $secondFactory = $this->createMock(RepositoryFactory::class); + $secondFactory->method('supports')->with($config)->willReturn(true); + $secondFactory->expects(self::never())->method('create'); + + // Add both factories (both support the config, but first one should be used) + $this->repositoryProvider->addRepositoryFactory($firstFactory); + $this->repositoryProvider->addRepositoryFactory($secondFactory); + + // Act + $result = $this->repositoryProvider->getByConfig($config); + + // Assert + self::assertSame($repository1, $result); + } + + public function testGetByConfigThrowsExceptionWhenNoFactorySupportsConfig(): void + { + // Arrange + $config = new RepositoryConfig(); + $config->type = 'unsupported'; + $config->uri = 'vendor/package'; + + $factory = $this->createMock(RepositoryFactory::class); + $factory->method('supports')->with($config)->willReturn(false); + $this->repositoryProvider->addRepositoryFactory($factory); + + // Assert (before Act for exceptions) + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage("No factory found for repository type `unsupported`."); + + // Act + $this->repositoryProvider->getByConfig($config); + } + + #[DataProvider('provideRepositoryConfigs')] + public function testGetByConfigWithVariousConfigs(RepositoryConfig $config, bool $factorySupports): void + { + // Arrange + $repository = $this->createMock(Repository::class); + + $factory = $this->createMock(RepositoryFactory::class); + $factory->method('supports')->with($config)->willReturn($factorySupports); + + if ($factorySupports) { + $factory->method('create')->with($config)->willReturn($repository); + } + + $this->repositoryProvider->addRepositoryFactory($factory); + + // Assert expectation for exception if no factory supports + if (!$factorySupports) { + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage("No factory found for repository type `{$config->type}`."); + } + + // Act + $result = $this->repositoryProvider->getByConfig($config); + + // Assert result if factory supports + if ($factorySupports) { + self::assertSame($repository, $result); + } + } + + protected function setUp(): void + { + // Arrange (common setup) + $this->repositoryProvider = new RepositoryProvider(); + } +} diff --git a/tests/Unit/Module/Repository/Stub/Collection/ReleasesCollectionStub.php b/tests/Unit/Module/Repository/Stub/Collection/ReleasesCollectionStub.php new file mode 100644 index 0000000..425d7ed --- /dev/null +++ b/tests/Unit/Module/Repository/Stub/Collection/ReleasesCollectionStub.php @@ -0,0 +1,61 @@ +releases = $releases; + } + + public function count(): int + { + return \count($this->releases); + } + + public function getIterator(): \Traversable + { + return new \ArrayIterator($this->releases); + } + + public function satisfies(string $constraint): ReleasesCollection + { + // Simple implementation for testing - returns same collection + return $this; + } + + public function sortByVersion(bool $descending = true): ReleasesCollection + { + // Simple implementation for testing - returns same collection + return $this; + } + + public function first(): ?ReleaseInterface + { + return $this->releases[0] ?? null; + } + + public function last(): ?ReleaseInterface + { + if (empty($this->releases)) { + return null; + } + + return $this->releases[\count($this->releases) - 1]; + } +} diff --git a/tests/Unit/Module/Repository/Stub/ReleaseStub.php b/tests/Unit/Module/Repository/Stub/ReleaseStub.php index 9464de9..48dd639 100644 --- a/tests/Unit/Module/Repository/Stub/ReleaseStub.php +++ b/tests/Unit/Module/Repository/Stub/ReleaseStub.php @@ -9,7 +9,7 @@ use Internal\DLoad\Module\Repository\AssetInterface; use Internal\DLoad\Module\Repository\Collection\AssetsCollection; use Internal\DLoad\Module\Repository\ReleaseInterface; -use Internal\DLoad\Module\Repository\RepositoryInterface; +use Internal\DLoad\Module\Repository\Repository; /** * Test stub implementation of ReleaseInterface for unit tests. @@ -22,14 +22,14 @@ final class ReleaseStub implements ReleaseInterface * @param array $assets */ public function __construct( - private RepositoryInterface $repository, + private Repository $repository, private string $name, private string $version, private Stability $stability, private array $assets = [], ) {} - public function getRepository(): RepositoryInterface + public function getRepository(): Repository { return $this->repository; } diff --git a/tests/Unit/Module/Repository/Stub/RepositoryFactoryStub.php b/tests/Unit/Module/Repository/Stub/RepositoryFactoryStub.php new file mode 100644 index 0000000..3c4b876 --- /dev/null +++ b/tests/Unit/Module/Repository/Stub/RepositoryFactoryStub.php @@ -0,0 +1,42 @@ + $supportedTypes Types this factory will support + * @param Repository|null $repository Repository to return when create() is called + */ + public function __construct(array $supportedTypes = ['github'], ?Repository $repository = null) + { + $this->supportedTypes = $supportedTypes; + $this->repository = $repository; + } + + public function supports(RepositoryConfig $config): bool + { + return \in_array($config->type, $this->supportedTypes, true); + } + + public function create(RepositoryConfig $config): Repository + { + if ($this->repository === null) { + return new RepositoryStub($config->uri); + } + + return $this->repository; + } +} diff --git a/tests/Unit/Module/Repository/Stub/RepositoryStub.php b/tests/Unit/Module/Repository/Stub/RepositoryStub.php index dcccb2c..76f58c6 100644 --- a/tests/Unit/Module/Repository/Stub/RepositoryStub.php +++ b/tests/Unit/Module/Repository/Stub/RepositoryStub.php @@ -4,35 +4,26 @@ namespace Internal\DLoad\Tests\Unit\Module\Repository\Stub; -use Internal\DLoad\Module\Repository\AssetInterface; use Internal\DLoad\Module\Repository\Collection\ReleasesCollection; -use Internal\DLoad\Module\Repository\ReleaseInterface; -use Internal\DLoad\Module\Repository\RepositoryInterface; +use Internal\DLoad\Module\Repository\Repository; +use Internal\DLoad\Tests\Unit\Module\Repository\Stub\Collection\ReleasesCollectionStub; /** - * Test stub implementation of RepositoryInterface for unit tests. + * Stub implementation of Repository for testing. */ -final class RepositoryStub implements RepositoryInterface +final class RepositoryStub implements Repository { - /** - * @var array - */ - private array $releases; - - /** - * @var array> Mapping of release name to assets - */ - private array $assetsMap = []; + private string $name; + private ?ReleasesCollection $releases; /** - * @param non-empty-string $name - * @param array $releases + * @param string $name Repository name + * @param ReleasesCollection|null $releases Collection of releases to return */ - public function __construct( - private string $name, - array $releases, - ) { - $this->releases = $releases; + public function __construct(string $name, ?ReleasesCollection $releases = null) + { + $this->name = $name; + $this->releases = $releases ?? new ReleasesCollectionStub(); } public function getName(): string @@ -42,28 +33,6 @@ public function getName(): string public function getReleases(): ReleasesCollection { - return new ReleasesCollection($this->releases); - } - - /** - * Set assets for a specific release in this repository. - * Helper method for testing. - * - * @param array $assets - */ - public function setAssets(array $assets, ReleaseInterface $release): void - { - $this->assetsMap[$release->getName()] = $assets; - } - - /** - * Get assets for a specific release. - * Helper method for testing. - * - * @return array - */ - public function getAssetsForRelease(ReleaseInterface $release): array - { - return $this->assetsMap[$release->getName()] ?? []; + return $this->releases; } } From 014815b7a480b1348f6876150d2d3c1adf41a0d5 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sun, 6 Apr 2025 17:28:44 +0400 Subject: [PATCH 21/30] tests(repository): fix tests --- .../Repository/AssetsCollectionTest.php | 2 +- .../Repository/ReleasesCollectionTest.php | 5 +++-- .../Collection/ReleasesCollectionStub.php | 16 ++------------ .../Module/Repository/Stub/ReleaseStub.php | 21 +++++++++++-------- .../Module/Repository/Stub/RepositoryStub.php | 12 +++++++++-- 5 files changed, 28 insertions(+), 28 deletions(-) diff --git a/tests/Unit/Module/Repository/AssetsCollectionTest.php b/tests/Unit/Module/Repository/AssetsCollectionTest.php index 5c881c3..bb0c2fb 100644 --- a/tests/Unit/Module/Repository/AssetsCollectionTest.php +++ b/tests/Unit/Module/Repository/AssetsCollectionTest.php @@ -166,7 +166,7 @@ public function testEmptyReturnsTrueForEmptyCollection(): void protected function setUp(): void { // Arrange - $this->repository = new RepositoryStub('vendor/package', []); + $this->repository = new RepositoryStub('vendor/package'); $this->release = new ReleaseStub( $this->repository, '1.2.3', diff --git a/tests/Unit/Module/Repository/ReleasesCollectionTest.php b/tests/Unit/Module/Repository/ReleasesCollectionTest.php index b1e109a..ad782f9 100644 --- a/tests/Unit/Module/Repository/ReleasesCollectionTest.php +++ b/tests/Unit/Module/Repository/ReleasesCollectionTest.php @@ -19,6 +19,7 @@ final class ReleasesCollectionTest extends TestCase { private RepositoryStub $repository; + /** @var list */ private array $releases; private ReleasesCollection $collection; @@ -138,7 +139,7 @@ public function testWithAssetsFiltersReleasesWithAssets(): void ), ]; - $this->repository->setAssets($assets, $release); + $release->setAssets($assets); // Act $result = $this->collection->withAssets(); @@ -152,7 +153,7 @@ public function testWithAssetsFiltersReleasesWithAssets(): void protected function setUp(): void { // Arrange - $this->repository = new RepositoryStub('vendor/package', []); + $this->repository = new RepositoryStub('vendor/package'); // Create a series of releases with different versions and stabilities $this->releases = [ diff --git a/tests/Unit/Module/Repository/Stub/Collection/ReleasesCollectionStub.php b/tests/Unit/Module/Repository/Stub/Collection/ReleasesCollectionStub.php index 425d7ed..5788e7c 100644 --- a/tests/Unit/Module/Repository/Stub/Collection/ReleasesCollectionStub.php +++ b/tests/Unit/Module/Repository/Stub/Collection/ReleasesCollectionStub.php @@ -5,24 +5,17 @@ namespace Internal\DLoad\Tests\Unit\Module\Repository\Stub\Collection; use Internal\DLoad\Module\Repository\Collection\ReleasesCollection; +use Internal\DLoad\Module\Repository\Internal\Collection; use Internal\DLoad\Module\Repository\ReleaseInterface; /** * Stub implementation of ReleasesCollection for testing. */ -final class ReleasesCollectionStub implements ReleasesCollection +final class ReleasesCollectionStub extends Collection { /** @var ReleaseInterface[] */ private array $releases; - /** - * @param ReleaseInterface[] $releases List of releases - */ - public function __construct(array $releases = []) - { - $this->releases = $releases; - } - public function count(): int { return \count($this->releases); @@ -45,11 +38,6 @@ public function sortByVersion(bool $descending = true): ReleasesCollection return $this; } - public function first(): ?ReleaseInterface - { - return $this->releases[0] ?? null; - } - public function last(): ?ReleaseInterface { if (empty($this->releases)) { diff --git a/tests/Unit/Module/Repository/Stub/ReleaseStub.php b/tests/Unit/Module/Repository/Stub/ReleaseStub.php index 48dd639..fe751a7 100644 --- a/tests/Unit/Module/Repository/Stub/ReleaseStub.php +++ b/tests/Unit/Module/Repository/Stub/ReleaseStub.php @@ -22,10 +22,10 @@ final class ReleaseStub implements ReleaseInterface * @param array $assets */ public function __construct( - private Repository $repository, - private string $name, - private string $version, - private Stability $stability, + private readonly RepositoryStub $repository, + private readonly string $name, + private readonly string $version, + private readonly Stability $stability, private array $assets = [], ) {} @@ -51,14 +51,17 @@ public function getStability(): Stability public function getAssets(): AssetsCollection { - // For repository stub integration - if ($this->repository instanceof RepositoryStub) { - $this->assets = $this->repository->getAssetsForRelease($this); - } - return new AssetsCollection($this->assets); } + /** + * @param array $assets + */ + public function setAssets(array $assets): void + { + $this->assets = $assets; + } + public function satisfies(string $constraint): bool { // Using Composer's semver for consistent version comparison diff --git a/tests/Unit/Module/Repository/Stub/RepositoryStub.php b/tests/Unit/Module/Repository/Stub/RepositoryStub.php index 76f58c6..b36594c 100644 --- a/tests/Unit/Module/Repository/Stub/RepositoryStub.php +++ b/tests/Unit/Module/Repository/Stub/RepositoryStub.php @@ -5,6 +5,8 @@ namespace Internal\DLoad\Tests\Unit\Module\Repository\Stub; use Internal\DLoad\Module\Repository\Collection\ReleasesCollection; +use Internal\DLoad\Module\Repository\Internal\Collection; +use Internal\DLoad\Module\Repository\ReleaseInterface; use Internal\DLoad\Module\Repository\Repository; use Internal\DLoad\Tests\Unit\Module\Repository\Stub\Collection\ReleasesCollectionStub; @@ -14,7 +16,8 @@ final class RepositoryStub implements Repository { private string $name; - private ?ReleasesCollection $releases; + /** @var Collection */ + private ?Collection $releases; /** * @param string $name Repository name @@ -23,7 +26,7 @@ final class RepositoryStub implements Repository public function __construct(string $name, ?ReleasesCollection $releases = null) { $this->name = $name; - $this->releases = $releases ?? new ReleasesCollectionStub(); + $this->releases = $releases ?? new ReleasesCollectionStub([]); } public function getName(): string @@ -35,4 +38,9 @@ public function getReleases(): ReleasesCollection { return $this->releases; } + + public function setAsset(ReleaseStub $release, array $assets): void + { + $release->setAssets($assets); + } } From 105527a02ac446d4f28195a898572e963f69fcf4 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 6 Apr 2025 13:29:13 +0000 Subject: [PATCH 22/30] style(php-cs-fixer): fix coding standards --- tests/Unit/Module/Repository/ReleasesCollectionTest.php | 2 ++ tests/Unit/Module/Repository/Stub/RepositoryStub.php | 1 + 2 files changed, 3 insertions(+) diff --git a/tests/Unit/Module/Repository/ReleasesCollectionTest.php b/tests/Unit/Module/Repository/ReleasesCollectionTest.php index ad782f9..dbab924 100644 --- a/tests/Unit/Module/Repository/ReleasesCollectionTest.php +++ b/tests/Unit/Module/Repository/ReleasesCollectionTest.php @@ -19,8 +19,10 @@ final class ReleasesCollectionTest extends TestCase { private RepositoryStub $repository; + /** @var list */ private array $releases; + private ReleasesCollection $collection; public function testSatisfiesFiltersReleasesByVersionConstraint(): void diff --git a/tests/Unit/Module/Repository/Stub/RepositoryStub.php b/tests/Unit/Module/Repository/Stub/RepositoryStub.php index b36594c..837e05d 100644 --- a/tests/Unit/Module/Repository/Stub/RepositoryStub.php +++ b/tests/Unit/Module/Repository/Stub/RepositoryStub.php @@ -16,6 +16,7 @@ final class RepositoryStub implements Repository { private string $name; + /** @var Collection */ private ?Collection $releases; From 6f31afce648b8cde7179a362f5cb40f6aedb2294 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sun, 6 Apr 2025 17:36:24 +0400 Subject: [PATCH 23/30] tests: remove phpunit slow-test-detector plugin --- composer.json | 1 - composer.lock | 75 +----------------------------------------------- phpunit.xml.dist | 3 -- 3 files changed, 1 insertion(+), 78 deletions(-) diff --git a/composer.json b/composer.json index 33e22a2..47fad10 100644 --- a/composer.json +++ b/composer.json @@ -31,7 +31,6 @@ "require-dev": { "buggregator/trap": "^1.10", "dereuromark/composer-prefer-lowest": "^0.1.10", - "ergebnis/phpunit-slow-test-detector": "^2.14", "phpunit/phpunit": "^10.5", "spiral/code-style": "^2.2.2", "ta-tikoma/phpunit-architecture-test": "^0.8.4", diff --git a/composer.lock b/composer.lock index 2281141..64ad0a8 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "42e791fb2bb9b34d63eda84606615e2e", + "content-hash": "9993c6c2bb8ef3d475d105e3d098b2d1", "packages": [ { "name": "psr/container", @@ -2713,79 +2713,6 @@ }, "time": "2024-12-07T21:18:45+00:00" }, - { - "name": "ergebnis/phpunit-slow-test-detector", - "version": "2.19.0", - "source": { - "type": "git", - "url": "https://github.com/ergebnis/phpunit-slow-test-detector.git", - "reference": "2f66ad7fcc468a5db03592a0c73a930e0c0eccce" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/ergebnis/phpunit-slow-test-detector/zipball/2f66ad7fcc468a5db03592a0c73a930e0c0eccce", - "reference": "2f66ad7fcc468a5db03592a0c73a930e0c0eccce", - "shasum": "" - }, - "require": { - "php": "~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", - "phpunit/phpunit": "^6.5.0 || ^7.5.0 || ^8.5.19 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0" - }, - "require-dev": { - "ergebnis/composer-normalize": "^2.45.0", - "ergebnis/license": "^2.6.0", - "ergebnis/php-cs-fixer-config": "^6.43.1", - "fakerphp/faker": "~1.20.0", - "phpstan/extension-installer": "^1.4.3", - "phpstan/phpstan": "^1.12.11", - "phpstan/phpstan-deprecation-rules": "^1.2.1", - "phpstan/phpstan-phpunit": "^1.4.1", - "phpstan/phpstan-strict-rules": "^1.6.1", - "psr/container": "~1.0.0", - "rector/rector": "^1.2.10" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "2.16-dev" - }, - "composer-normalize": { - "indent-size": 2, - "indent-style": "space" - } - }, - "autoload": { - "psr-4": { - "Ergebnis\\PHPUnit\\SlowTestDetector\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Andreas Möller", - "email": "am@localheinz.com", - "homepage": "https://localheinz.com" - } - ], - "description": "Provides facilities for detecting slow tests in phpunit/phpunit.", - "homepage": "https://github.com/ergebnis/phpunit-slow-test-detector", - "keywords": [ - "detector", - "extension", - "phpunit", - "slow", - "test" - ], - "support": { - "issues": "https://github.com/ergebnis/phpunit-slow-test-detector/issues", - "security": "https://github.com/ergebnis/phpunit-slow-test-detector/blob/main/.github/SECURITY.md", - "source": "https://github.com/ergebnis/phpunit-slow-test-detector" - }, - "time": "2025-02-23T15:25:48+00:00" - }, { "name": "evenement/evenement", "version": "v3.0.2", diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 3d3008e..9a5a970 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -10,9 +10,6 @@ stderr="true" beStrictAboutOutputDuringTests="true" > - - - tests/Unit From 2103156922c8d9428288650dcefeff139eeecec3 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sun, 6 Apr 2025 17:43:21 +0400 Subject: [PATCH 24/30] tests(phpunit): fix cc config --- composer.json | 2 +- phpunit.xml.dist | 7 ++++--- src/Module/Repository/Collection/AssetsCollection.php | 6 +++++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index 47fad10..5715965 100644 --- a/composer.json +++ b/composer.json @@ -94,7 +94,7 @@ ], "test:cc": [ "@putenv XDEBUG_MODE=coverage", - "phpunit --coverage-clover=.build/phpunit/logs/clover.xml --color=always" + "phpunit --coverage-clover=runtime/phpunit/logs/clover.xml --color=always" ] } } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 9a5a970..cb41381 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -9,6 +9,7 @@ executionOrder="random" stderr="true" beStrictAboutOutputDuringTests="true" + displayDetailsOnTestsThatTriggerWarnings="true" > @@ -23,9 +24,9 @@ - - - + + + diff --git a/src/Module/Repository/Collection/AssetsCollection.php b/src/Module/Repository/Collection/AssetsCollection.php index c950db1..a43c89d 100644 --- a/src/Module/Repository/Collection/AssetsCollection.php +++ b/src/Module/Repository/Collection/AssetsCollection.php @@ -103,7 +103,11 @@ static function (AssetInterface $asset) use ($extensions): bool { public function whereNameMatches(string $pattern): self { return $this->filter( - static fn(AssetInterface $asset): bool => \preg_match($pattern, $asset->getName()) === 1, + static fn(AssetInterface $asset): bool => @\preg_match( + $pattern, + $asset->getName(), + flags: \PREG_NO_ERROR, + ) === 1, ); } } From 32c4ac3cc3baff03336de7e9cd82a22c5d55ea85 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sun, 6 Apr 2025 18:04:57 +0400 Subject: [PATCH 25/30] cs(yaml): fix context files --- context.yaml | 12 ++++++------ resources/prompts.yaml | 2 ++ resources/prompts/cover-class-by-docs.yaml | 2 ++ 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/context.yaml b/context.yaml index 9d73d63..765edc3 100644 --- a/context.yaml +++ b/context.yaml @@ -3,7 +3,7 @@ $schema: 'https://raw.githubusercontent.com/context-hub/generator/refs/heads/main/json-schema.json' import: - - path: resources/prompts.yaml + - path: resources/prompts.yaml documents: # Project structure overview @@ -15,11 +15,11 @@ documents: content: | The PSR-4 is used in the project. - type: tree - sourcePaths: ['src'] + sourcePaths: [ 'src' ] showCharCount: true showSize: true - type: file - sourcePaths: ['README.md'] + sourcePaths: [ 'README.md' ] # Modules API - description: 'Modules API allowed to be used in the project' @@ -27,9 +27,9 @@ documents: overwrite: true sources: - type: file - sourcePaths: ['src/Module', 'src/Service'] + sourcePaths: [ 'src/Module', 'src/Service' ] filePattern: '*.php' - excludePatterns: ['/Internal/'] + excludePatterns: [ '/Internal/' ] modifiers: - name: php-content-filter options: @@ -48,6 +48,6 @@ documents: There are all the guidelines about how to do some things in the project. Feel free to load any related guideline to the current context to make the work more efficient. - type: tree - sourcePaths: ['docs/guidelines'] + sourcePaths: 'docs/guidelines' showCharCount: true showSize: true diff --git a/resources/prompts.yaml b/resources/prompts.yaml index f5beecd..d75c488 100644 --- a/resources/prompts.yaml +++ b/resources/prompts.yaml @@ -1,3 +1,5 @@ +--- + $schema: 'https://raw.githubusercontent.com/context-hub/generator/refs/heads/main/json-schema.json' import: diff --git a/resources/prompts/cover-class-by-docs.yaml b/resources/prompts/cover-class-by-docs.yaml index 11e6eab..ae47518 100644 --- a/resources/prompts/cover-class-by-docs.yaml +++ b/resources/prompts/cover-class-by-docs.yaml @@ -1,3 +1,5 @@ +--- + $schema: 'https://raw.githubusercontent.com/context-hub/generator/refs/heads/main/json-schema.json' prompts: From e979355cc1c617758216cb3b251c24c8d7f0a356 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sun, 6 Apr 2025 18:09:52 +0400 Subject: [PATCH 26/30] cs(markdown): disable markdown linter for `docs` folder --- .github/workflows/coding-standards.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/coding-standards.yml b/.github/workflows/coding-standards.yml index 11fcb2c..e71eb4c 100644 --- a/.github/workflows/coding-standards.yml +++ b/.github/workflows/coding-standards.yml @@ -61,7 +61,6 @@ jobs: with: globs: | *.md - docs/**/*.md !CHANGELOG.md coding-standards: From c6d3823b80b4269a9c0e81fabedde76fb747350f Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sun, 6 Apr 2025 20:20:08 +0400 Subject: [PATCH 27/30] feat(repository): add Paginator and CachedGenerator. To make lazy collections in the future --- .../Repository/Internal/CachedGenerator.php | 142 ++++++++ src/Module/Repository/Internal/Paginator.php | 127 ++++++++ .../Repository/Internal/PaginatorTest.php | 89 +++++ .../Internal/CachedGeneratorTest.php | 307 ++++++++++++++++++ 4 files changed, 665 insertions(+) create mode 100644 src/Module/Repository/Internal/CachedGenerator.php create mode 100644 src/Module/Repository/Internal/Paginator.php create mode 100644 tests/Module/Repository/Internal/PaginatorTest.php create mode 100644 tests/Unit/Module/Repository/Internal/CachedGeneratorTest.php diff --git a/src/Module/Repository/Internal/CachedGenerator.php b/src/Module/Repository/Internal/CachedGenerator.php new file mode 100644 index 0000000..8c9e0f0 --- /dev/null +++ b/src/Module/Repository/Internal/CachedGenerator.php @@ -0,0 +1,142 @@ + + * + * @internal + * @psalm-internal Internal\DLoad\Module\Repository + */ +final class CachedGenerator implements \IteratorAggregate +{ + /** + * @var array Cached items from the generator + */ + private array $cache = []; + + private ?\Generator $generator; + private bool $inited = false; + + /** + * @param \Traversable $generator The generator to cache + */ + public function __construct( + \Traversable $generator, + ) { + $this->generator = (static function (\Traversable $generator) { + foreach ($generator as $key => $value) { + yield $key => $value; + } + })($generator); + } + + /** + * Returns an iterator that yields the cached items and continues + * fetching from the generator when the cache is exhausted. + * + * @return \Traversable + */ + public function getIterator(): \Traversable + { + $index = 0; + + // First yield all cached items + while ($index < \count($this->cache)) { + yield $this->cache[$index]; + $index++; + } + + start: + $next = $this->rollItem(); + if ($this->generator === null) { + return; + } + + yield $next; + goto start; + } + + /** + * Returns the first item in the cache or from the generator. + * + * @return T|null The first item or null if the generator is empty + */ + public function first(): mixed + { + // If we have cached items, return the first one + return $this->cache === [] + ? $this->rollItem() + : $this->cache[0]; + } + + /** + * Returns whether the generator has any items. + * + * @return bool True if there are items, false otherwise + */ + public function isEmpty(): bool + { + // If we already have items, it's not empty + if (!empty($this->cache)) { + return false; + } + + // Try to get the first item to determine if it's empty + return $this->first() === null; + } + + /** + * Returns the count of items in the generator. + * + * @return int<0, max> The number of items + */ + public function count(): int + { + if ($this->generator !== null) { + while ($this->generator !== null && $this->generator->valid()) { + $this->rollItem(); + } + } + + return \count($this->cache); + } + + /** + * Get the next item from the generator. + * + * @return T The next item + */ + private function rollItem(): mixed + { + if ($this->inited) { + if ($this->generator === null) { + return null; + } + + try { + $this->generator->next(); + if (!$this->generator->valid()) { + $this->generator = null; + return null; + } + } catch (\Throwable $e) { + $this->generator = null; + throw $e; + } + } + + $this->inited = true; + /** @var T $next */ + $next = $this->generator->current(); + $this->cache[] = $next; + + return $next; + } +} diff --git a/src/Module/Repository/Internal/Paginator.php b/src/Module/Repository/Internal/Paginator.php new file mode 100644 index 0000000..59d2295 --- /dev/null +++ b/src/Module/Repository/Internal/Paginator.php @@ -0,0 +1,127 @@ + + * + * @internal + * @psalm-internal Internal\DLoad\Module\Repository + */ +final class Paginator implements \IteratorAggregate, \Countable +{ + /** @var list */ + private array $collection; + + /** @var self|null */ + private ?self $nextPage = null; + + private ?int $totalItems = null; + + /** + * @param \Generator> $loader + * @param int<1, max> $pageNumber + * @param null|\Closure(): int<0, max> $counter + */ + private function __construct( + private readonly \Generator $loader, + private readonly int $pageNumber, + private ?\Closure $counter, + ) { + $this->collection = $loader->current(); + } + + /** + * @template TInitItem + * + * @param \Generator> $loader + * @param null|callable(): int<0, max> $counter Returns total number of items. + * + * @return self + */ + public static function createFromGenerator(\Generator $loader, ?callable $counter): self + { + return new self($loader, 1, $counter === null ? null : $counter(...)); + } + + /** + * Load next page. + * + * @return self|null + */ + public function getNextPage(): ?self + { + if ($this->nextPage !== null) { + return $this->nextPage; + } + + $this->loader->next(); + if (!$this->loader->valid()) { + return null; + } + $this->nextPage = new self($this->loader, $this->pageNumber + 1, $this->counter); + /** @psalm-suppress UnsupportedPropertyReferenceUsage */ + $this->nextPage->counter = &$this->counter; + + return $this->nextPage; + } + + /** + * @return array + */ + public function getPageItems(): array + { + return $this->collection; + } + + /** + * Iterate all items from current page and all next pages. + * + * @return \Traversable + */ + public function getIterator(): \Traversable + { + $paginator = $this; + while ($paginator !== null) { + foreach ($paginator->getPageItems() as $item) { + yield $item; + } + + $paginator = $paginator->getNextPage(); + } + } + + /** + * @return int<1, max> + */ + public function getPageNumber(): int + { + return $this->pageNumber; + } + + /** + * Value is cached in all produced pages after first call in any page. + * + * Note: the method may call yet another RPC to get total number of items. + * It means that the result may be different from the number of items at the moment of the pagination start. + * + * @throws \LogicException If counter is not set. + */ + public function count(): int + { + if ($this->totalItems !== null) { + return $this->totalItems; + } + + if ($this->counter === null) { + throw new \LogicException('Paginator does not support counting.'); + } + + return $this->totalItems = ($this->counter)(); + } +} diff --git a/tests/Module/Repository/Internal/PaginatorTest.php b/tests/Module/Repository/Internal/PaginatorTest.php new file mode 100644 index 0000000..974bda0 --- /dev/null +++ b/tests/Module/Repository/Internal/PaginatorTest.php @@ -0,0 +1,89 @@ +assertEquals(['Item 1', 'Item 2'], $paginator->getPageItems()); + $this->assertEquals(1, $paginator->getPageNumber()); + + // Test getting next page + $page2 = $paginator->getNextPage(); + $this->assertNotNull($page2); + $this->assertEquals(['Item 3', 'Item 4'], $page2->getPageItems()); + $this->assertEquals(2, $page2->getPageNumber()); + + // Test getting third page + $page3 = $page2->getNextPage(); + $this->assertNotNull($page3); + $this->assertEquals(['Item 5'], $page3->getPageItems()); + $this->assertEquals(3, $page3->getPageNumber()); + + // Test that there's no fourth page + $page4 = $page3->getNextPage(); + $this->assertNull($page4); + } + + public function testIteration(): void + { + // Create a generator that yields arrays of items for each page + $loader = static function (): \Generator { + yield ['Item 1', 'Item 2']; // Page 1 + yield ['Item 3', 'Item 4']; // Page 2 + }; + + $paginator = Paginator::createFromGenerator($loader(), null); + + // Test iterating through all items + $items = \iterator_to_array($paginator); + $this->assertEquals(['Item 1', 'Item 2', 'Item 3', 'Item 4'], $items); + } + + public function testCountWithCounter(): void + { + // Create a generator + $loader = static function (): \Generator { + yield ['Item 1', 'Item 2']; // Page 1 + yield ['Item 3', 'Item 4']; // Page 2 + }; + + // Create a counter function + $counter = static fn() => 4; // Total number of items + + $paginator = Paginator::createFromGenerator($loader(), $counter); + + // Test count() + $this->assertEquals(4, $paginator->count()); + } + + public function testCountWithoutCounter(): void + { + // Create a generator + $loader = static function (): \Generator { + yield ['Item 1', 'Item 2']; // Page 1 + }; + + $paginator = Paginator::createFromGenerator($loader(), null); + + // Test that count() throws an exception when no counter is provided + $this->expectException(\LogicException::class); + $paginator->count(); + } +} diff --git a/tests/Unit/Module/Repository/Internal/CachedGeneratorTest.php b/tests/Unit/Module/Repository/Internal/CachedGeneratorTest.php new file mode 100644 index 0000000..cb992ad --- /dev/null +++ b/tests/Unit/Module/Repository/Internal/CachedGeneratorTest.php @@ -0,0 +1,307 @@ + [ + (static function () { + yield 'a'; + yield 'b'; + yield 'c'; + })(), + ['a', 'b', 'c'], + ]; + + yield 'array iterator' => [ + new \ArrayIterator(['x', 'y', 'z']), + ['x', 'y', 'z'], + ]; + } + + /** + * Tests that the generator correctly caches items as they are yielded. + */ + public function testIterationCachesYieldedItems(): void + { + // Arrange + $generator = $this->createGenerator(5); + $cachedGenerator = new CachedGenerator($generator); + + // Act - Partially iterate through generator + $i = 0; + $iteratedItems = []; + foreach ($cachedGenerator->getIterator() as $item) { + $iteratedItems[] = $item; + // Only consume first 3 items + if (++$i >= 3) { + break; + } + } + + // Get all items using a new iteration + $allItems = \iterator_to_array($cachedGenerator); + + // Assert + self::assertCount(3, $iteratedItems); + self::assertSame([0, 1, 2], $iteratedItems); + // self::assertCount(5, $allItems); + self::assertSame([0, 1, 2, 3, 4], $allItems); + } + + /** + * Tests that first() returns the first element from the generator. + */ + public function testFirstReturnsFirstElement(): void + { + // Arrange + $generator = $this->createGenerator(3); + $cachedGenerator = new CachedGenerator($generator); + + // Act + $firstItem = $cachedGenerator->first(); + + // Assert + self::assertSame(0, $firstItem); + } + + /** + * Tests that first() returns null when the generator is empty. + */ + public function testFirstReturnsNullForEmptyGenerator(): void + { + // Arrange + $generator = $this->createGenerator(0); + $cachedGenerator = new CachedGenerator($generator); + + // Act + $firstItem = $cachedGenerator->first(); + + // Assert + self::assertNull($firstItem); + } + + /** + * Tests that first() can retrieve the first item without affecting future iterations. + */ + public function testFirstDoesNotConsumeItemFromIteration(): void + { + // Arrange + $generator = $this->createGenerator(3); + $cachedGenerator = new CachedGenerator($generator); + + // Act + $firstItem = $cachedGenerator->first(); + $allItems = \iterator_to_array($cachedGenerator); + + // Assert + self::assertSame(0, $firstItem); + self::assertCount(3, $allItems); + self::assertSame([0, 1, 2], $allItems); + } + + /** + * Tests that isEmpty() correctly identifies empty generators. + */ + public function testIsEmptyReturnsTrueForEmptyGenerator(): void + { + // Arrange + $generator = $this->createGenerator(0); + $cachedGenerator = new CachedGenerator($generator); + + // Act + $isEmpty = $cachedGenerator->isEmpty(); + + // Assert + self::assertTrue($isEmpty); + } + + /** + * Tests that isEmpty() correctly identifies non-empty generators. + */ + public function testIsEmptyReturnsFalseForNonEmptyGenerator(): void + { + // Arrange + $generator = $this->createGenerator(1); + $cachedGenerator = new CachedGenerator($generator); + + // Act + $isEmpty = $cachedGenerator->isEmpty(); + + // Assert + self::assertFalse($isEmpty); + } + + /** + * Tests that count() correctly returns the number of items in the generator. + */ + public function testCountReturnsCorrectItemCount(): void + { + // Arrange + $generator = $this->createGenerator(5); + $cachedGenerator = new CachedGenerator($generator); + + // Act + $count = $cachedGenerator->count(); + + // Assert + self::assertSame(5, $count); + } + + /** + * Tests that count() returns zero for an empty generator. + */ + public function testCountReturnsZeroForEmptyGenerator(): void + { + // Arrange + $generator = $this->createGenerator(0); + $cachedGenerator = new CachedGenerator($generator); + + // Act + $count = $cachedGenerator->count(); + + // Assert + self::assertSame(0, $count); + } + + /** + * Tests that partial iteration followed by count() returns the correct total count. + */ + public function testPartialIterationFollowedByCountReturnsCorrectTotal(): void + { + // Arrange + $generator = $this->createGenerator(5); + $cachedGenerator = new CachedGenerator($generator); + + // Act - Partially iterate + $i = 0; + foreach ($cachedGenerator as $item) { + if (++$i >= 2) { + break; + } + } + + // Get count + $count = $cachedGenerator->count(); + + // Assert + self::assertSame(5, $count); + } + + /** + * Tests that the cache persists after a complete iteration. + */ + public function testCachePersistsAfterCompleteIteration(): void + { + // Arrange + $generator = $this->createGenerator(3); + $cachedGenerator = new CachedGenerator($generator); + + // Act - First iteration + $firstIteration = \iterator_to_array($cachedGenerator); + + // Second iteration should use cache + $secondIteration = \iterator_to_array($cachedGenerator); + + // Assert + self::assertSame($firstIteration, $secondIteration); + self::assertSame([0, 1, 2], $firstIteration); + } + + /** + * Tests that the cached generator correctly handles various traversable types. + * + * @param \Traversable $traversable The traversable to test + * @param array $expected The expected result + */ + #[DataProvider('provideTraversables')] + public function testHandlesVariousTraversableTypes(\Traversable $traversable, array $expected): void + { + // Arrange + $cachedGenerator = new CachedGenerator($traversable); + + // Act + $result = \iterator_to_array($cachedGenerator); + + // Assert + self::assertSame($expected, $result); + } + + /** + * Tests that cached generator works correctly with nested generators. + */ + public function testHandlesNestedGenerators(): void + { + // Arrange + $nestedGenerator = function () { + yield from $this->createGenerator(2); + yield from $this->createGenerator(2, 10); + }; + + $cachedGenerator = new CachedGenerator($nestedGenerator()); + + // Act + $result = \iterator_to_array($cachedGenerator); + + // Assert + self::assertSame([0, 1, 10, 11], $result); + } + + /** + * Tests behavior with large datasets to ensure memory effectiveness. + */ + public function testHandlesLargeDatasets(): void + { + // Arrange + $largeGenerator = $this->createGenerator(1000); + $cachedGenerator = new CachedGenerator($largeGenerator); + + // Act + $firstAccess = $cachedGenerator->first(); + + // Partially iterate + $partialIteration = []; + $i = 0; + foreach ($cachedGenerator as $item) { + $partialIteration[] = $item; + if (++$i >= 10) { + break; + } + } + + // Get count without completing iteration + $totalCount = $cachedGenerator->count(); + + // Assert + self::assertSame(0, $firstAccess); + self::assertCount(10, $partialIteration); + self::assertSame(1000, $totalCount); + } + + /** + * Creates a simple generator that yields consecutive integers. + * + * @param int $count Number of items to yield + * @param int $start Starting value + */ + private function createGenerator(int $count, int $start = 0): \Generator + { + for ($i = $start; $i < $start + $count; $i++) { + yield $i; + } + } +} From 3f43168c218c81b00e175b0bd7f230c36cded9b9 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sun, 6 Apr 2025 21:04:29 +0400 Subject: [PATCH 28/30] feat(repository): make base base Collection and GitHubRepository lazy --- .../Collection/CompositeRepository.php | 2 +- .../Repository/Internal/CachedGenerator.php | 37 +-- src/Module/Repository/Internal/Collection.php | 143 +++++++++--- .../Internal/GitHub/GitHubRelease.php | 2 +- .../Internal/GitHub/GitHubRepository.php | 50 +++- src/Module/Repository/Internal/Paginator.php | 4 +- .../Repository/Internal/CollectionTest.php | 148 ++++++++++++ .../GitHubRepositoryLazyLoadingTest.php | 219 ++++++++++++++++++ .../Repository/Internal/PaginatorTest.php | 2 +- 9 files changed, 538 insertions(+), 69 deletions(-) create mode 100644 tests/Unit/Module/Repository/Internal/CollectionTest.php create mode 100644 tests/Unit/Module/Repository/Internal/GitHub/GitHubRepositoryLazyLoadingTest.php rename tests/{ => Unit}/Module/Repository/Internal/PaginatorTest.php (97%) diff --git a/src/Module/Repository/Collection/CompositeRepository.php b/src/Module/Repository/Collection/CompositeRepository.php index 1e509fc..674b0ca 100644 --- a/src/Module/Repository/Collection/CompositeRepository.php +++ b/src/Module/Repository/Collection/CompositeRepository.php @@ -53,7 +53,7 @@ public function getName(): string */ public function getReleases(): ReleasesCollection { - return ReleasesCollection::from(function () { + return ReleasesCollection::create(function () { foreach ($this->repositories as $repository) { yield from $repository->getReleases(); } diff --git a/src/Module/Repository/Internal/CachedGenerator.php b/src/Module/Repository/Internal/CachedGenerator.php index 8c9e0f0..b304174 100644 --- a/src/Module/Repository/Internal/CachedGenerator.php +++ b/src/Module/Repository/Internal/CachedGenerator.php @@ -9,7 +9,7 @@ * This prevents values from being "consumed" when iterating. * * @template T - * @template-implements \IteratorAggregate + * @template-implements \IteratorAggregate * * @internal * @psalm-internal Internal\DLoad\Module\Repository @@ -84,7 +84,7 @@ public function first(): mixed public function isEmpty(): bool { // If we already have items, it's not empty - if (!empty($this->cache)) { + if ($this->cache !== []) { return false; } @@ -115,27 +115,28 @@ public function count(): int */ private function rollItem(): mixed { - if ($this->inited) { - if ($this->generator === null) { - return null; - } + if ($this->generator === null) { + return null; + } - try { + try { + if ($this->inited) { $this->generator->next(); - if (!$this->generator->valid()) { - $this->generator = null; - return null; - } - } catch (\Throwable $e) { + } + + $this->inited = true; + /** @var T $next */ + $next = $this->generator->current(); + if (!$this->generator->valid()) { $this->generator = null; - throw $e; + return null; } - } - $this->inited = true; - /** @var T $next */ - $next = $this->generator->current(); - $this->cache[] = $next; + $this->cache[] = $next; + } catch (\Throwable $e) { + $this->generator = null; + throw $e; + } return $next; } diff --git a/src/Module/Repository/Internal/Collection.php b/src/Module/Repository/Internal/Collection.php index ef94abb..395e8e3 100644 --- a/src/Module/Repository/Internal/Collection.php +++ b/src/Module/Repository/Internal/Collection.php @@ -33,17 +33,18 @@ abstract class Collection implements \IteratorAggregate, \Countable { /** - * @var array + * Array of filter callbacks to apply when iterating + * + * @var array */ - protected array $items; + protected array $filters = []; /** - * @param array $items Collection items + * @param array|CachedGenerator $items Collection items */ - final public function __construct(array $items) - { - $this->items = $items; - } + final public function __construct( + protected readonly array|CachedGenerator $items, + ) {} /** * Creates a new collection from various sources. @@ -51,17 +52,19 @@ final public function __construct(array $items) * Supports creating from an existing collection, a traversable, * an array, or a generator function. * - * @param self|iterable|\Closure $items Source of items - * @return static New collection instance + * @template TNew + * + * @param iterable $items Source of items + * @return static New collection instance * @throws \InvalidArgumentException If the input cannot be converted to a collection */ public static function create(mixed $items): static { return match (true) { $items instanceof static => $items, - $items instanceof \Traversable => new static(\iterator_to_array($items)), \is_array($items) => new static($items), - $items instanceof \Closure => static::from($items), + $items instanceof \Traversable => new static(new CachedGenerator($items)), + $items instanceof \Closure => static::create($items()), default => throw new \InvalidArgumentException( \sprintf('Unsupported iterable type %s.', \get_debug_type($items)), ), @@ -69,25 +72,18 @@ public static function create(mixed $items): static } /** - * Creates a collection from a generator function. - * - * @param \Closure $generator Function that yields collection items - * @return static New collection instance - */ - public static function from(\Closure $generator): static - { - return static::create($generator()); - } - - /** - * Filters the collection using the provided callback. + * Adds a filter to the collection. + * This does not immediately apply the filter, but stores it for later use during iteration. * * @param callable(T): bool $filter Function that returns true for items to keep * @return $this New filtered collection */ public function filter(callable $filter): static { - return new static(\array_filter($this->items, $filter)); + // For iterables or collections with existing filters, add the filter to the pipeline + $clone = clone $this; + $clone->filters[] = $filter; + return $clone; } /** @@ -98,7 +94,13 @@ public function filter(callable $filter): static */ public function map(callable $map): static { - return new static(\array_map($map, $this->items)); + // For iterables or collections with filters, we need to materialize and map + $items = []; + foreach ($this as $item) { + $items[] = $map($item); + } + + return new static($items); } /** @@ -112,9 +114,8 @@ public function map(callable $map): static */ public function except(callable $filter): static { - $callback = static fn(...$args): bool => ! $filter(...$args); - - return new static(\array_filter($this->items, $callback)); + $callback = static fn(...$args): bool => !$filter(...$args); + return $this->filter($callback); } /** @@ -127,9 +128,21 @@ public function except(callable $filter): static */ public function first(?callable $filter = null): ?object { - $self = $filter === null ? $this : $this->filter($filter); + $combinedFilter = $this->getCombinedFilter($filter); + if ($combinedFilter === null) { + return $this->items instanceof CachedGenerator + ? $this->items->first() + : ($this->items === [] ? null : \reset($this->items)); + } - return $self->items === [] ? null : \reset($self->items); + // For iterables, iterate until we find a match + foreach ($this->getIterator() as $item) { + if ($filter === null || $filter($item)) { + return $item; + } + } + + return null; } /** @@ -151,7 +164,20 @@ public function firstOr(callable $otherwise, ?callable $filter = null): object */ public function getIterator(): \Traversable { - return new \ArrayIterator($this->items); + $combinedFilter = $this->getCombinedFilter(); + + // If no filters, return the items directly + if ($combinedFilter === null) { + yield from $this->items; + return; + } + + // Apply filters during iteration + foreach ($this->items as $item) { + if ($combinedFilter($item)) { + yield $item; + } + } } /** @@ -161,7 +187,19 @@ public function getIterator(): \Traversable */ public function count(): int { - return \count($this->items); + if ($this->filters === []) { + return $this->items instanceof CachedGenerator + ? $this->items->count() + : \count($this->items); + } + + // For iterables or with filters, we need to count matching items + $count = 0; + foreach ($this as $item) { + $count++; + } + + return $count; } /** @@ -172,10 +210,7 @@ public function count(): int */ public function whenEmpty(callable $then): static { - if ($this->empty()) { - $then(); - } - + $this->empty() and $then(); return $this; } @@ -186,7 +221,14 @@ public function whenEmpty(callable $then): static */ public function empty(): bool { - return $this->items === []; + if ($this->filters === []) { + return $this->items instanceof CachedGenerator + ? $this->items->isEmpty() + : $this->items === []; + } + + // For iterables or with filters, try to get the first item + return $this->first() === null; } /** @@ -196,6 +238,31 @@ public function empty(): bool */ public function toArray(): array { - return \array_values($this->items); + return \iterator_to_array($this->getIterator()); + } + + /** + * Combines all filters with an optional additional filter. + * Returns null if there are no filters to apply. + * + * @param null|callable(T): bool $additionalFilter + * @return null|callable(T): bool + */ + private function getCombinedFilter(?callable $additionalFilter = null): ?callable + { + if ($this->filters === []) { + return $additionalFilter; + } + + // Combine collection filters with additional filter + return function ($item) use ($additionalFilter) { + foreach ($this->filters as $filter) { + if (!$filter($item)) { + return false; + } + } + + return $additionalFilter === null ? true : $additionalFilter($item); + }; } } diff --git a/src/Module/Repository/Internal/GitHub/GitHubRelease.php b/src/Module/Repository/Internal/GitHub/GitHubRelease.php index d3c41b5..639ca88 100644 --- a/src/Module/Repository/Internal/GitHub/GitHubRelease.php +++ b/src/Module/Repository/Internal/GitHub/GitHubRelease.php @@ -51,7 +51,7 @@ public static function fromApiResponse(GitHubRepository $repository, HttpClientI $version = $data['tag_name'] ?? (string) $data['name']; $result = new self($client, $repository, $name, $version); - $result->assets = AssetsCollection::from(static function () use ($client, $result, $data): \Generator { + $result->assets = AssetsCollection::create(static function () use ($client, $result, $data): \Generator { /** @var GitHubAssetApiResponse $item */ foreach ($data['assets'] ?? [] as $item) { yield GitHubAsset::fromApiResponse($client, $result, $item); diff --git a/src/Module/Repository/Internal/GitHub/GitHubRepository.php b/src/Module/Repository/Internal/GitHub/GitHubRepository.php index 684177c..7819317 100644 --- a/src/Module/Repository/Internal/GitHub/GitHubRepository.php +++ b/src/Module/Repository/Internal/GitHub/GitHubRepository.php @@ -5,6 +5,7 @@ namespace Internal\DLoad\Module\Repository\Internal\GitHub; use Internal\DLoad\Module\Repository\Collection\ReleasesCollection; +use Internal\DLoad\Module\Repository\Internal\Paginator; use Internal\DLoad\Module\Repository\Repository; use Internal\DLoad\Service\Destroyable; use Symfony\Component\HttpClient\HttpClient; @@ -50,23 +51,56 @@ public function __construct(string $org, string $repo, ?HttpClientInterface $cli } /** + * Returns a lazily loaded collection of repository releases. + * Pages are loaded only when needed during iteration or filtering. + * * @throws ExceptionInterface */ public function getReleases(): ReleasesCollection { - return $this->releases ??= ReleasesCollection::from(function () { + if ($this->releases !== null) { + return $this->releases; + } + + // Create a generator function for lazy loading release pages + $pageLoader = function (): \Generator { $page = 0; - // Iterate over all pages do { - $response = $this->releasesRequest(++$page); + try { + // to avoid first eager loading because of generator + yield []; + + $response = $this->releasesRequest(++$page); + + /** @psalm-var array $data */ + $data = $response->toArray(); - /** @psalm-var GitHubReleaseApiResponse $data */ - foreach ($response->toArray() as $data) { - yield GitHubRelease::fromApiResponse($this, $this->client, $data); + // If empty response, no more pages + if ($data === []) { + return; + } + + yield \array_map( + fn(array $releaseData): GitHubRelease => GitHubRelease::fromApiResponse($this, $this->client, $releaseData), + $data, + ); + + // Check if there are more pages + $hasMorePages = $this->hasNextPage($response); + } catch (ExceptionInterface) { + return; } - } while ($this->hasNextPage($response)); - }); + } while ($hasMorePages); + }; + + // Create paginator + $paginator = Paginator::createFromGenerator($pageLoader(), null); + + // Create a collection with the paginator + $this->releases = ReleasesCollection::create($paginator); + + return $this->releases; } public function getName(): string diff --git a/src/Module/Repository/Internal/Paginator.php b/src/Module/Repository/Internal/Paginator.php index 59d2295..032d519 100644 --- a/src/Module/Repository/Internal/Paginator.php +++ b/src/Module/Repository/Internal/Paginator.php @@ -24,7 +24,7 @@ final class Paginator implements \IteratorAggregate, \Countable private ?int $totalItems = null; /** - * @param \Generator> $loader + * @param \Generator, mixed, mixed> $loader * @param int<1, max> $pageNumber * @param null|\Closure(): int<0, max> $counter */ @@ -33,7 +33,7 @@ private function __construct( private readonly int $pageNumber, private ?\Closure $counter, ) { - $this->collection = $loader->current(); + $this->collection = $loader->valid() ? $loader->current() : []; } /** diff --git a/tests/Unit/Module/Repository/Internal/CollectionTest.php b/tests/Unit/Module/Repository/Internal/CollectionTest.php new file mode 100644 index 0000000..fb1d9bd --- /dev/null +++ b/tests/Unit/Module/Repository/Internal/CollectionTest.php @@ -0,0 +1,148 @@ +assertEquals(['item1', 'item2', 'item3'], $arrayCollection->toArray()); + + // Test with generator + $generator = static function () { + yield 'gen1'; + yield 'gen2'; + }; + + $generatorCollection = $testCollection::create($generator()); + $this->assertEquals(['gen1', 'gen2'], $generatorCollection->toArray()); + } + + public function testCollectionWithPaginator(): void + { + // Create a test collection class + $testCollection = new class([]) extends Collection {}; + + // Create a paginator + $pageLoader = static function (): \Generator { + yield ['page1-item1', 'page1-item2']; + yield ['page2-item1', 'page2-item2']; + }; + + $paginator = Paginator::createFromGenerator($pageLoader(), null); + + // Create collection with paginator + $collection = $testCollection::create($paginator); + + // Test toArray() loads all pages + $this->assertEquals( + ['page1-item1', 'page1-item2', 'page2-item1', 'page2-item2'], + $collection->toArray(), + ); + } + + public function testFilterChaining(): void + { + // Create a test collection class + $testCollection = new class([]) extends Collection {}; + + // Create a collection with array + $collection = $testCollection::create([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + + // Apply multiple filters + $filtered = $collection + ->filter(static fn($item) => $item > 3) + ->filter(static fn($item) => $item < 8) + ->filter(static fn($item) => $item % 2 === 0); + + // Check result + $this->assertEquals([4, 6], $filtered->toArray()); + } + + public function testFilterWithPaginator(): void + { + // Create a test collection class + $testCollection = new class([]) extends Collection {}; + + // Create a paginator + $pageLoader = static function (): \Generator { + yield [1, 2, 3, 4, 5]; + yield [6, 7, 8, 9, 10]; + }; + + $paginator = Paginator::createFromGenerator($pageLoader(), null); + + // Create collection with paginator + $collection = $testCollection::create($paginator); + + // Apply multiple filters + $filtered = $collection + ->filter(static fn($item) => $item > 3) + ->filter(static fn($item) => $item < 9) + ->filter(static fn($item) => $item % 2 === 0); + + // Check result + $this->assertEquals([4, 6, 8], $filtered->toArray()); + } + + public function testFirst(): void + { + // Create a test collection class + $testCollection = new class([]) extends Collection {}; + + // Create a paginator that will verify lazy loading + $pagesLoaded = [0 => false, 1 => false, 2 => false]; + $f = static fn(int $num): object => (object) ['num' => $num]; + $pageLoader = static function () use (&$pagesLoaded, $f): \Generator { + $pagesLoaded[0] = true; + yield [$f(0)]; + + $pagesLoaded[1] = true; + yield [$f(1), $f(2), $f(3), $f(4), $f(5)]; + + $pagesLoaded[2] = true; + yield [$f(6), $f(7), $f(8), $f(9), $f(10)]; + }; + + $paginator = Paginator::createFromGenerator($pageLoader(), null); + // It always starts the generator when the closure is called + $this->assertTrue($pagesLoaded[0]); + + // Create collection with paginator + $collection = $testCollection::create($paginator); + + // Apply filter that will only match items on the 2nd page + $filtered = $collection->filter(static fn($item) => $item->num < 7); + + // At this point, no pages should be loaded + $this->assertFalse($pagesLoaded[1]); + $this->assertFalse($pagesLoaded[2]); + + // Get first matching item + $first = $filtered->first(); + self::assertSame(0, $first->num); + // We got '0' from the 0-page, so we need to load the next pages + $this->assertFalse($pagesLoaded[1]); + $this->assertFalse($pagesLoaded[2]); + + + // Now the 1st page should be loaded to get "1" value + $first = $filtered->first(static fn($item) => $item->num > 0); + self::assertSame(1, $first->num); + $this->assertTrue($pagesLoaded[1]); + $this->assertFalse($pagesLoaded[2]); + } +} diff --git a/tests/Unit/Module/Repository/Internal/GitHub/GitHubRepositoryLazyLoadingTest.php b/tests/Unit/Module/Repository/Internal/GitHub/GitHubRepositoryLazyLoadingTest.php new file mode 100644 index 0000000..25cad08 --- /dev/null +++ b/tests/Unit/Module/Repository/Internal/GitHub/GitHubRepositoryLazyLoadingTest.php @@ -0,0 +1,219 @@ + "Release $releaseNumber", + 'tag_name' => "v1.$releaseNumber.0", + 'assets' => [], + ]; + } + } + + // Add Link header for pagination + $headers = []; + if ($page < 3) { + $nextPage = $page + 1; + $headers['Link'] = ["; rel=\"next\""]; + } + + return new MockResponse(\json_encode($releases), [ + 'response_headers' => $headers, + ]); + }); + + // Create repository + $repository = new GitHubRepository('test', 'repo', $mockClient); + + // At this point, no requests should have been made + $this->assertEmpty($requestLog); + + // Get releases collection + $releases = $repository->getReleases(); + + // Still no requests should have been made + $this->assertEmpty($requestLog); + + // Get first release - this should load the first page + $firstRelease = $releases->first(); + $this->assertCount(1, $requestLog); + $this->assertEquals("Requested page 1", $requestLog[0]); + $this->assertEquals("v1.1.0", $firstRelease?->getVersion()); + + // Filter to releases with version greater than v1.2.0 + // This should NOT load additional pages yet + $laterReleases = $releases->filter(static fn($release) => \version_compare($release->getVersion(), "v1.2.0", ">")); + + // No additional pages should have been loaded after filter call + $this->assertCount(1, $requestLog); + + // Add another filter - this should still not load additional pages + $laterReleases = $laterReleases->filter(static fn($release) => \str_contains($release->getVersion(), "v1.")); + + // Still no additional pages loaded + $this->assertCount(1, $requestLog); + + // Get the first matching item - this should load pages until a match is found + $firstLaterRelease = $laterReleases->first(); + + // Now page 2 should be loaded because v1.1.0 and v1.2.0 don't match our filter + $this->assertCount(2, $requestLog); + $this->assertEquals("Requested page 2", $requestLog[1]); + $this->assertEquals("v1.3.0", $firstLaterRelease?->getVersion()); + + // Converting to array to force loading all pages + $allLaterReleases = $laterReleases->toArray(); + + // Now all pages should have been requested + $this->assertCount(3, $requestLog); + $this->assertEquals("Requested page 3", $requestLog[2]); + + // Check that we have the correct releases + $this->assertCount(2, $allLaterReleases); + $this->assertEquals("v1.3.0", $allLaterReleases[0]->getVersion()); + $this->assertEquals("v1.4.0", $allLaterReleases[1]->getVersion()); + } + + public function testMultipleFiltersWithoutIteration(): void + { + $requestLog = []; + + // Create a mock HTTP client that tracks page requests + $mockClient = new MockHttpClient(static function (string $method, string $url) use (&$requestLog): MockResponse { + // Extract page number from URL query + \parse_str(\parse_url($url, PHP_URL_QUERY) ?? '', $query); + $page = (int) ($query['page'] ?? 1); + + // Log this request + $requestLog[] = "Requested page $page"; + + return new MockResponse(\json_encode([ + [ + 'name' => "Release $page", + 'tag_name' => "v1.$page.0", + 'assets' => [], + ], + ])); + }); + + // Create repository + $repository = new GitHubRepository('test', 'repo', $mockClient); + $releases = $repository->getReleases(); + + // Apply multiple filters without iterating + $filtered = $releases + ->filter(static fn($release) => \str_contains($release->getVersion(), "v1")) + ->filter(static fn($release) => \version_compare($release->getVersion(), "v1.0.0", ">")) + ->filter(static fn($release) => $release->getName() !== "Ignored"); + + // No requests should have been made yet + $this->assertEmpty($requestLog); + } + + public function testEmptyRepository(): void + { + // Create a mock HTTP client that returns empty responses + $mockClient = new MockHttpClient([ + new MockResponse('[]'), // Empty first page + ]); + + // Create repository + $repository = new GitHubRepository('test', 'empty-repo', $mockClient); + + // Get releases collection + $releases = $repository->getReleases(); + + // Check that the collection is empty + $this->assertTrue($releases->empty()); + $this->assertNull($releases->first()); + $this->assertCount(0, \iterator_to_array($releases)); + } + + public function testChainingMultipleFilters(): void + { + // Create a mock HTTP client with predefined responses + $mockClient = new MockHttpClient([ + // Page 1 + new MockResponse(\json_encode([ + [ + 'name' => 'Release 1', + 'tag_name' => 'v1.0.0', + 'assets' => [ + ['name' => 'asset1.zip', 'browser_download_url' => 'https://example.com/asset1.zip'], + ], + ], + [ + 'name' => 'Release 2', + 'tag_name' => 'v2.0.0', + 'assets' => [], + ], + ]), [ + 'response_headers' => [ + 'Link' => ['; rel="next"'], + ], + ]), + // Page 2 + new MockResponse(\json_encode([ + [ + 'name' => 'Release 3', + 'tag_name' => 'v3.0.0', + 'assets' => [ + ['name' => 'asset3.zip', 'browser_download_url' => 'https://example.com/asset3.zip'], + ], + ], + [ + 'name' => 'Release 4', + 'tag_name' => 'v4.0.0', + 'assets' => [ + ['name' => 'asset4.zip', 'browser_download_url' => 'https://example.com/asset4.zip'], + ], + ], + ])), + ]); + + // Create repository and get releases + $repository = new GitHubRepository('test', 'repo', $mockClient); + $releases = $repository->getReleases(); + + // Create a filtered collection with multiple filters + $filteredReleases = $releases + ->filter(static fn($release) => \version_compare($release->getVersion(), "v2.0.0", ">=")) // v2.0.0 and above + ->filter(static fn($release) => !$release->getAssets()->empty()); // Only with assets + + // Convert to array to execute the filters + $result = $filteredReleases->toArray(); + + // Should have 2 releases: v3.0.0 and v4.0.0 (both have assets and >= v2.0.0) + $this->assertCount(2, $result); + $this->assertEquals("v3.0.0", $result[0]->getVersion()); + $this->assertEquals("v4.0.0", $result[1]->getVersion()); + } +} diff --git a/tests/Module/Repository/Internal/PaginatorTest.php b/tests/Unit/Module/Repository/Internal/PaginatorTest.php similarity index 97% rename from tests/Module/Repository/Internal/PaginatorTest.php rename to tests/Unit/Module/Repository/Internal/PaginatorTest.php index 974bda0..fa8df1b 100644 --- a/tests/Module/Repository/Internal/PaginatorTest.php +++ b/tests/Unit/Module/Repository/Internal/PaginatorTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Internal\DLoad\Tests\Module\Repository\Internal; +namespace Internal\DLoad\Tests\Unit\Module\Repository\Internal; use Internal\DLoad\Module\Repository\Internal\Paginator; use PHPUnit\Framework\TestCase; From 2a0ac6097d477ad046fd3d9ef6abea9c232b8b88 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sun, 6 Apr 2025 22:02:16 +0400 Subject: [PATCH 29/30] feat(repository): integrate lazy releases page loading --- .../Common/Internal/ObjectContainer.php | 1 + src/Module/Downloader/Downloader.php | 4 +- .../Collection/ReleasesCollection.php | 2 +- src/Module/Repository/Internal/Collection.php | 43 ++++-- .../Repository/Internal/CollectionTest.php | 145 ++++++++++++++++++ 5 files changed, 181 insertions(+), 14 deletions(-) diff --git a/src/Module/Common/Internal/ObjectContainer.php b/src/Module/Common/Internal/ObjectContainer.php index 23b849c..481ac02 100644 --- a/src/Module/Common/Internal/ObjectContainer.php +++ b/src/Module/Common/Internal/ObjectContainer.php @@ -37,6 +37,7 @@ public function __construct() $this->injector = (new Injector($this))->withCacheReflections(false); $this->cache[Injector::class] = $this->injector; $this->cache[self::class] = $this; + $this->cache[Container::class] = $this; $this->cache[ContainerInterface::class] = $this; } diff --git a/src/Module/Downloader/Downloader.php b/src/Module/Downloader/Downloader.php index c26af5f..4c95640 100644 --- a/src/Module/Downloader/Downloader.php +++ b/src/Module/Downloader/Downloader.php @@ -136,11 +136,13 @@ private function processRepository(Repository $repository, DownloadContext $cont ->satisfies($context->actionConfig->version); /** @var ReleaseInterface[] $releases */ - $releases = $releasesCollection->sortByVersion()->toArray(); + $releases = $releasesCollection->limit(10)->sortByVersion()->toArray(); $this->logger->debug('%d releases found.', \count($releases)); process_release: + // Try without limit + $releases === [] and $releases = $releasesCollection->limit(0)->toArray(); $releases === [] and throw new \RuntimeException('No relevant release found.'); $context->release = \array_shift($releases); diff --git a/src/Module/Repository/Collection/ReleasesCollection.php b/src/Module/Repository/Collection/ReleasesCollection.php index 9dcc330..aa8b57e 100644 --- a/src/Module/Repository/Collection/ReleasesCollection.php +++ b/src/Module/Repository/Collection/ReleasesCollection.php @@ -74,7 +74,7 @@ public function withAssets(): self */ public function sortByVersion(): self { - $result = $this->items; + $result = \iterator_to_array($this->getIterator()); $sort = function (ReleaseInterface $a, ReleaseInterface $b): int { return \version_compare($this->comparisonVersionString($b), $this->comparisonVersionString($a)); diff --git a/src/Module/Repository/Internal/Collection.php b/src/Module/Repository/Internal/Collection.php index 395e8e3..8afb1b3 100644 --- a/src/Module/Repository/Internal/Collection.php +++ b/src/Module/Repository/Internal/Collection.php @@ -39,6 +39,12 @@ abstract class Collection implements \IteratorAggregate, \Countable */ protected array $filters = []; + /** + * Maximum number of items to return from the collection + * Zero means no limit + */ + protected int $limitCount = 0; + /** * @param array|CachedGenerator $items Collection items */ @@ -132,7 +138,7 @@ public function first(?callable $filter = null): ?object if ($combinedFilter === null) { return $this->items instanceof CachedGenerator ? $this->items->first() - : ($this->items === [] ? null : \reset($this->items)); + : ($this->items === [] ? null : $this->items[\array_key_first($this->items)]); } // For iterables, iterate until we find a match @@ -157,6 +163,21 @@ public function firstOr(callable $otherwise, ?callable $filter = null): object return $this->first($filter) ?? $otherwise(); } + /** + * Limits the collection to a specified number of items. + * Using 0 as a count will reset any previous limit and return all items. + * + * @param int<0, max> $count Maximum number of items to include + * @return $this New collection with limited items + * @throws \InvalidArgumentException If count is negative + */ + public function limit(int $count): static + { + $clone = clone $this; + $clone->limitCount = $count; + return $clone; + } + /** * Returns an iterator for traversing the collection. * @@ -165,17 +186,15 @@ public function firstOr(callable $otherwise, ?callable $filter = null): object public function getIterator(): \Traversable { $combinedFilter = $this->getCombinedFilter(); + $itemCount = 0; - // If no filters, return the items directly - if ($combinedFilter === null) { - yield from $this->items; - return; - } - - // Apply filters during iteration + // Apply filters and limit during iteration foreach ($this->items as $item) { - if ($combinedFilter($item)) { + if ($combinedFilter === null || $combinedFilter($item)) { yield $item; + if ($this->limitCount > 0 && ++$itemCount >= $this->limitCount) { + return; + } } } } @@ -187,13 +206,13 @@ public function getIterator(): \Traversable */ public function count(): int { - if ($this->filters === []) { + if ($this->filters === [] && $this->limitCount === 0) { return $this->items instanceof CachedGenerator ? $this->items->count() : \count($this->items); } - // For iterables or with filters, we need to count matching items + // For iterables or with filters or limits, we need to count matching items $count = 0; foreach ($this as $item) { $count++; @@ -227,7 +246,7 @@ public function empty(): bool : $this->items === []; } - // For iterables or with filters, try to get the first item + // For iterables or with filters or limits, try to get the first item return $this->first() === null; } diff --git a/tests/Unit/Module/Repository/Internal/CollectionTest.php b/tests/Unit/Module/Repository/Internal/CollectionTest.php index fb1d9bd..9aed9b9 100644 --- a/tests/Unit/Module/Repository/Internal/CollectionTest.php +++ b/tests/Unit/Module/Repository/Internal/CollectionTest.php @@ -7,11 +7,35 @@ use Internal\DLoad\Module\Repository\Internal\Collection; use Internal\DLoad\Module\Repository\Internal\Paginator; use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; #[CoversClass(Collection::class)] final class CollectionTest extends TestCase { + public static function provideLimitCases(): \Generator + { + yield 'empty collection' => [ + [], 5, [], + ]; + + yield 'limit less than collection size' => [ + [1, 2, 3, 4, 5], 3, [1, 2, 3], + ]; + + yield 'limit equal to collection size' => [ + [1, 2, 3], 3, [1, 2, 3], + ]; + + yield 'limit greater than collection size' => [ + [1, 2, 3], 5, [1, 2, 3], + ]; + + yield 'zero limit returns all items' => [ + [1, 2, 3, 4, 5], 0, [1, 2, 3, 4, 5], + ]; + } + public function testCollectionWithIterable(): void { // Create a test collection class @@ -145,4 +169,125 @@ public function testFirst(): void $this->assertTrue($pagesLoaded[1]); $this->assertFalse($pagesLoaded[2]); } + + #[DataProvider('provideLimitCases')] + public function testLimit(array $sourceItems, int $limit, array $expectedItems): void + { + // Arrange + $testCollection = new class([]) extends Collection {}; + $collection = $testCollection::create($sourceItems); + + // Act + $limited = $collection->limit($limit); + + // Assert + self::assertEquals($expectedItems, $limited->toArray()); + + // Check count matches expected + self::assertCount(\count($expectedItems), $limited); + } + + public function testLimitWithFilter(): void + { + // Arrange + $testCollection = new class([]) extends Collection {}; + $collection = $testCollection::create([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + + // Act + $filtered = $collection + ->filter(static fn($item) => $item > 3) + ->limit(2); + + // Assert + self::assertEquals([4, 5], $filtered->toArray()); + self::assertCount(2, $filtered); + } + + public function testLimitWithPaginator(): void + { + // Arrange + $testCollection = new class([]) extends Collection {}; + + $pageLoader = static function (): \Generator { + yield [1, 2, 3]; + yield [4, 5, 6]; + yield [7, 8, 9]; + }; + + $paginator = Paginator::createFromGenerator($pageLoader(), null); + $collection = $testCollection::create($paginator); + + // Act + $limited = $collection->limit(4); + + // Assert + self::assertEquals([1, 2, 3, 4], $limited->toArray()); + self::assertCount(4, $limited); + } + + public function testLimitWithZeroCountResetsLimit(): void + { + // Arrange + $testCollection = new class([]) extends Collection {}; + $collection = $testCollection::create([1, 2, 3, 4, 5]); + $limitedCollection = $collection->limit(2); + + // Verify the limit was applied + self::assertEquals([1, 2], $limitedCollection->toArray()); + + // Act - apply zero limit to reset the limit + $resetCollection = $limitedCollection->limit(0); + + // Assert - should have all items + self::assertEquals([1, 2, 3, 4, 5], $resetCollection->toArray()); + } + + public function testLimitResetWithFilteredCollection(): void + { + // Arrange + $testCollection = new class([]) extends Collection {}; + $collection = $testCollection::create([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + + // Apply filter and limit + $filtered = $collection + ->filter(static fn($item) => $item > 3) + ->limit(2); + + // Verify initial state + self::assertEquals([4, 5], $filtered->toArray()); + + // Act - reset limit + $resetLimited = $filtered->limit(0); + + // Assert - filter should still be applied, but not the limit + self::assertEquals([4, 5, 6, 7, 8, 9, 10], $resetLimited->toArray()); + } + + public function testCountWithLimit(): void + { + // Arrange + $testCollection = new class([]) extends Collection {}; + $collection = $testCollection::create([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + + // Act + $limited = $collection->limit(3); + + // Assert + self::assertCount(3, $limited); + } + + public function testEmptyWithLimit(): void + { + // Arrange + $testCollection = new class([]) extends Collection {}; + $collection = $testCollection::create([1, 2, 3]); + + // Act & Assert + self::assertFalse($collection->limit(1)->empty()); + self::assertFalse($collection->limit(0)->empty()); + + // With an empty source collection + $emptyCollection = $testCollection::create([]); + self::assertTrue($emptyCollection->limit(5)->empty()); + } } From 9b92cef4dc2546577057118712b8eb1060d66ea8 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Mon, 7 Apr 2025 00:55:46 +0400 Subject: [PATCH 30/30] tests(psalm): update baseline --- psalm-baseline.xml | 72 +++++++++++++++++++++++++++++++++++++---- tests/Unit/InfoTest.php | 15 --------- 2 files changed, 65 insertions(+), 22 deletions(-) delete mode 100644 tests/Unit/InfoTest.php diff --git a/psalm-baseline.xml b/psalm-baseline.xml index b99872d..9bdfa79 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -126,6 +126,9 @@ + + + @@ -167,6 +170,15 @@ + + + repositories as $repository) { + yield from $repository->getReleases(); + } + }]]> + + + + + ]]> + + + + + + + + static::create($items())]]> + + + + + + + - + + + + + + + + + + + items instanceof CachedGenerator + ? $this->items->first() + : ($this->items === [] ? null : $this->items[\array_key_first($this->items)])]]> + - items, $callback))]]> - items, $filter))]]> - items))]]> - + + @@ -239,6 +280,12 @@ + @@ -270,12 +317,15 @@ + + + - client, $data)]]> + client, $releaseData)]]> - client, $data)]]> + client, $releaseData)]]> @@ -287,6 +337,14 @@ name]]> releases]]> + + + + + + + valid() ? $loader->current() : []]]> + diff --git a/tests/Unit/InfoTest.php b/tests/Unit/InfoTest.php deleted file mode 100644 index bece496..0000000 --- a/tests/Unit/InfoTest.php +++ /dev/null @@ -1,15 +0,0 @@ -