From 79469dbde6abe4140b860c66642f45be4c21c1a7 Mon Sep 17 00:00:00 2001 From: Holger Veltrup <92872893+sitepark-veltrup@users.noreply.github.com> Date: Thu, 27 Jun 2024 13:10:08 +0200 Subject: [PATCH] feat: initial implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --------- Co-authored-by: Mario Schäper <95750382+sitepark-schaeper@users.noreply.github.com> --- composer.json | 12 +- composer.lock | 2373 +++++++++++------ config/routes.yaml | 4 +- config/services.yaml | 82 +- phpstan.neon.dist | 17 + src/AtooloRuntimeCheckBundle.php | 4 +- src/Console/Command/CheckCommand.php | 133 +- src/Console/Command/Io/TypifiedInput.php | 57 + src/Controller/CheckController.php | 37 +- src/Service/CheckStatus.php | 152 ++ src/Service/Checker/Checker.php | 14 + src/Service/Checker/CheckerCollection.php | 33 + src/Service/Checker/MonologChecker.php | 278 ++ src/Service/Checker/PhpStatus.php | 195 ++ src/Service/Checker/ProcessStatus.php | 38 + src/Service/Checker/phpStatus.json | 30 + src/Service/Cli/FastCgiStatus.php | 115 + src/Service/Cli/FastCgiStatusFactory.php | 101 + src/Service/Cli/RuntimeCheck.php | 76 + src/Service/FpmFcgi/CliStatus.php | 70 + src/Service/FpmFcgi/RuntimeCheck.php | 82 + src/Service/Platform.php | 61 + src/Service/RuntimeStatus.php | 97 + src/Service/RuntimeType.php | 26 + src/Service/Worker/OneTimeTrigger.php | 36 + src/Service/Worker/WorkerCheckEvent.php | 12 + src/Service/Worker/WorkerCheckScheduler.php | 59 + src/Service/Worker/WorkerStatusFile.php | 107 + test/Console/Command/CheckCommandTest.php | 99 +- test/Console/Command/Io/TypifiedInputTest.php | 160 ++ test/Controller/CheckControllerTest.php | 116 +- test/Service/CheckStatusTest.php | 183 ++ .../Service/Checker/CheckerCollectionTest.php | 56 + test/Service/Checker/MonologCheckerTest.php | 354 +++ test/Service/Checker/PhpStatusTest.php | 165 ++ test/Service/Checker/ProcessStatusTest.php | 55 + test/Service/Cli/FastCGIStatusTest.php | 139 + test/Service/Cli/FastCgiStatusFactoryTest.php | 93 + test/Service/Cli/RuntimeCheckTest.php | 94 + test/Service/FpmFcgi/CliStatusTest.php | 48 + test/Service/FpmFcgi/RuntimeCheckTest.php | 82 + test/Service/RuntimeStatusTest.php | 142 + test/Service/RuntimeTypeTest.php | 23 + test/Service/Worker/OneTimeTriggerTest.php | 58 + .../Worker/WorkerCheckSchedulerTest.php | 61 + test/Service/Worker/WorkerStatusFileTest.php | 269 ++ .../Checker/MonologCheckerTest/logging.log | 1 + .../Checker/MonologCheckerTest/logging.log.1 | 1 + .../MonologCheckerTest/logging.log.1.gz | 1 + .../Checker/MonologCheckerTest/logging.log.2 | 1 + .../MonologCheckerTest/logging.log.2.gz | 1 + .../PhpStatusTest/empty-phpStatus.json | 2 + .../conf.d/include.conf | 1 + .../php-fpm.conf | 4 + .../fpm-config-unreadable/php-fpm.conf | 1 + .../PhpStatusTest/fpm/conf.d/include.conf | 2 + .../Checker/PhpStatusTest/fpm/php-fpm.conf | 4 + ...tatus-unreadable-php-fpm-conf-include.json | 5 + .../phpStatus-unreadable-php-fpm-conf.json | 5 + .../Checker/PhpStatusTest/phpStatus.json | 14 + .../Cli/FastCgiStatusFactoryTest/unix-socket | 1 + .../FpmFcgi/CliStatusTest/console-exitcode-1 | 2 + .../FpmFcgi/CliStatusTest/console-missing-cli | 6 + .../FpmFcgi/CliStatusTest/console-success | 9 + .../empty-statusFile.json | 3 + .../WorkerStatusFileTest/statusFile.json | 8 + .../statusFileWithInvalidLastRun.json | 8 + 67 files changed, 5757 insertions(+), 821 deletions(-) create mode 100644 src/Console/Command/Io/TypifiedInput.php create mode 100644 src/Service/CheckStatus.php create mode 100644 src/Service/Checker/Checker.php create mode 100644 src/Service/Checker/CheckerCollection.php create mode 100644 src/Service/Checker/MonologChecker.php create mode 100644 src/Service/Checker/PhpStatus.php create mode 100644 src/Service/Checker/ProcessStatus.php create mode 100644 src/Service/Checker/phpStatus.json create mode 100644 src/Service/Cli/FastCgiStatus.php create mode 100644 src/Service/Cli/FastCgiStatusFactory.php create mode 100644 src/Service/Cli/RuntimeCheck.php create mode 100644 src/Service/FpmFcgi/CliStatus.php create mode 100644 src/Service/FpmFcgi/RuntimeCheck.php create mode 100644 src/Service/Platform.php create mode 100644 src/Service/RuntimeStatus.php create mode 100644 src/Service/RuntimeType.php create mode 100644 src/Service/Worker/OneTimeTrigger.php create mode 100644 src/Service/Worker/WorkerCheckEvent.php create mode 100644 src/Service/Worker/WorkerCheckScheduler.php create mode 100644 src/Service/Worker/WorkerStatusFile.php create mode 100644 test/Console/Command/Io/TypifiedInputTest.php create mode 100644 test/Service/CheckStatusTest.php create mode 100644 test/Service/Checker/CheckerCollectionTest.php create mode 100644 test/Service/Checker/MonologCheckerTest.php create mode 100644 test/Service/Checker/PhpStatusTest.php create mode 100644 test/Service/Checker/ProcessStatusTest.php create mode 100644 test/Service/Cli/FastCGIStatusTest.php create mode 100644 test/Service/Cli/FastCgiStatusFactoryTest.php create mode 100644 test/Service/Cli/RuntimeCheckTest.php create mode 100644 test/Service/FpmFcgi/CliStatusTest.php create mode 100644 test/Service/FpmFcgi/RuntimeCheckTest.php create mode 100644 test/Service/RuntimeStatusTest.php create mode 100644 test/Service/RuntimeTypeTest.php create mode 100644 test/Service/Worker/OneTimeTriggerTest.php create mode 100644 test/Service/Worker/WorkerCheckSchedulerTest.php create mode 100644 test/Service/Worker/WorkerStatusFileTest.php create mode 100644 test/resources/Service/Checker/MonologCheckerTest/logging.log create mode 100644 test/resources/Service/Checker/MonologCheckerTest/logging.log.1 create mode 100644 test/resources/Service/Checker/MonologCheckerTest/logging.log.1.gz create mode 100644 test/resources/Service/Checker/MonologCheckerTest/logging.log.2 create mode 100644 test/resources/Service/Checker/MonologCheckerTest/logging.log.2.gz create mode 100644 test/resources/Service/Checker/PhpStatusTest/empty-phpStatus.json create mode 100644 test/resources/Service/Checker/PhpStatusTest/fpm-config-include-unreadable/conf.d/include.conf create mode 100644 test/resources/Service/Checker/PhpStatusTest/fpm-config-include-unreadable/php-fpm.conf create mode 100644 test/resources/Service/Checker/PhpStatusTest/fpm-config-unreadable/php-fpm.conf create mode 100644 test/resources/Service/Checker/PhpStatusTest/fpm/conf.d/include.conf create mode 100644 test/resources/Service/Checker/PhpStatusTest/fpm/php-fpm.conf create mode 100644 test/resources/Service/Checker/PhpStatusTest/phpStatus-unreadable-php-fpm-conf-include.json create mode 100644 test/resources/Service/Checker/PhpStatusTest/phpStatus-unreadable-php-fpm-conf.json create mode 100644 test/resources/Service/Checker/PhpStatusTest/phpStatus.json create mode 100644 test/resources/Service/Cli/FastCgiStatusFactoryTest/unix-socket create mode 100755 test/resources/Service/FpmFcgi/CliStatusTest/console-exitcode-1 create mode 100755 test/resources/Service/FpmFcgi/CliStatusTest/console-missing-cli create mode 100755 test/resources/Service/FpmFcgi/CliStatusTest/console-success create mode 100644 test/resources/Service/Worker/WorkerStatusFileTest/empty-statusFile.json create mode 100644 test/resources/Service/Worker/WorkerStatusFileTest/statusFile.json create mode 100644 test/resources/Service/Worker/WorkerStatusFileTest/statusFileWithInvalidLastRun.json diff --git a/composer.json b/composer.json index 0bcaa37..67cbb22 100644 --- a/composer.json +++ b/composer.json @@ -11,11 +11,20 @@ ], "require": { "php": ">=8.1 <8.4.0", + "ext-posix": "*", + "ext-zend-opcache": "*", + "hollodotme/fast-cgi-client": "^3.1", + "monolog/monolog": "^3.6", "symfony/config": "^6.3 || ^7.0", "symfony/dependency-injection": "^6.3 || ^7.0", + "symfony/expression-language": "^6.3 || ^7.1", "symfony/framework-bundle": "^6.3 || ^7.0", "symfony/http-kernel": "^6.3 || ^7.0", + "symfony/lock": "^6.3 || ^7.0", "symfony/messenger": "^6.3 || ^7.0", + "symfony/process": "^6.3 || ^7.0", + "symfony/scheduler": "^6.3 || ^7.0", + "symfony/security-http": "^6.3 || ^7.1", "symfony/yaml": "^6.3 || ^7.0" }, "require-dev": { @@ -24,7 +33,8 @@ "phpcompatibility/php-compatibility": "^9.3", "phpunit/phpunit": "^10.4", "roave/security-advisories": "dev-latest", - "squizlabs/php_codesniffer": "^3.7" + "squizlabs/php_codesniffer": "^3.7", + "symfony/filesystem": "^7.1" }, "minimum-stability": "dev", "prefer-stable": true, diff --git a/composer.lock b/composer.lock index 1915cdb..e535530 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,159 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a81d05c280a9855ce0a775248c9a9db7", + "content-hash": "e9d58f0c9d46e86539fceb0014ce1457", "packages": [ + { + "name": "hollodotme/fast-cgi-client", + "version": "v3.1.7", + "source": { + "type": "git", + "url": "https://github.com/hollodotme/fast-cgi-client.git", + "reference": "062182d4eda73c161cc2839783acc83096ec0f37" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hollodotme/fast-cgi-client/zipball/062182d4eda73c161cc2839783acc83096ec0f37", + "reference": "062182d4eda73c161cc2839783acc83096ec0f37", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=7.1" + }, + "require-dev": { + "ext-xdebug": ">=2.6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "hollodotme\\FastCGI\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Holger Woltersdorf", + "email": "hw@hollo.me" + } + ], + "description": "A PHP fast CGI client to send requests (a)synchronously to PHP-FPM.", + "keywords": [ + "Socket", + "async", + "fastcgi", + "php-fpm" + ], + "support": { + "issues": "https://github.com/hollodotme/fast-cgi-client/issues", + "source": "https://github.com/hollodotme/fast-cgi-client/tree/v3.1.7" + }, + "time": "2021-12-07T10:10:20+00:00" + }, + { + "name": "monolog/monolog", + "version": "3.6.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "4b18b21a5527a3d5ffdac2fd35d3ab25a9597654" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/4b18b21a5527a3d5ffdac2fd35d3ab25a9597654", + "reference": "4b18b21a5527a3d5ffdac2fd35d3ab25a9597654", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "3.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2 || ^2.0", + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "phpstan/phpstan": "^1.9", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-strict-rules": "^1.4", + "phpunit/phpunit": "^10.5.17", + "predis/predis": "^1.1 || ^2", + "ruflin/elastica": "^7", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/3.6.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2024-04-12T21:02:21+00:00" + }, { "name": "psr/cache", "version": "3.0.0", @@ -956,6 +1107,70 @@ ], "time": "2024-04-18T09:32:20+00:00" }, + { + "name": "symfony/expression-language", + "version": "v7.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/expression-language.git", + "reference": "463cb95f80c14136175f4e03f7f6199b01c6b8b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/expression-language/zipball/463cb95f80c14136175f4e03f7f6199b01c6b8b4", + "reference": "463cb95f80c14136175f4e03f7f6199b01c6b8b4", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/cache": "^6.4|^7.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/service-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\ExpressionLanguage\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an engine that can compile and evaluate expressions", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/expression-language/tree/v7.1.1" + }, + "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-05-31T14:57:53+00:00" + }, { "name": "symfony/filesystem", "version": "v7.1.1", @@ -1424,6 +1639,84 @@ ], "time": "2024-06-04T06:52:15+00:00" }, + { + "name": "symfony/lock", + "version": "v7.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/lock.git", + "reference": "1f8c941f1270dee046e09a826bcdd3b2ebada45e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/lock/zipball/1f8c941f1270dee046e09a826bcdd3b2ebada45e", + "reference": "1f8c941f1270dee046e09a826bcdd3b2ebada45e", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3" + }, + "conflict": { + "doctrine/dbal": "<3.6", + "symfony/cache": "<6.4" + }, + "require-dev": { + "doctrine/dbal": "^3.6|^4", + "predis/predis": "^1.1|^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Lock\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jérémy Derussé", + "email": "jeremy@derusse.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Creates and manages locks, a mechanism to provide exclusive access to a shared resource", + "homepage": "https://symfony.com", + "keywords": [ + "cas", + "flock", + "locking", + "mutex", + "redlock", + "semaphore" + ], + "support": { + "source": "https://github.com/symfony/lock/tree/v7.1.1" + }, + "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-05-31T14:57:53+00:00" + }, { "name": "symfony/messenger", "version": "v7.1.1", @@ -1511,42 +1804,37 @@ "time": "2024-05-31T14:57:53+00:00" }, { - "name": "symfony/polyfill-ctype", - "version": "v1.29.0", + "name": "symfony/password-hasher", + "version": "v7.1.1", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4" + "url": "https://github.com/symfony/password-hasher.git", + "reference": "4ad96eb7cf9e2f8f133ada95f2b8021769061662" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ef4d7e442ca910c4764bce785146269b30cb5fc4", - "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4", + "url": "https://api.github.com/repos/symfony/password-hasher/zipball/4ad96eb7cf9e2f8f133ada95f2b8021769061662", + "reference": "4ad96eb7cf9e2f8f133ada95f2b8021769061662", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=8.2" }, - "provide": { - "ext-ctype": "*" + "conflict": { + "symfony/security-core": "<6.4" }, - "suggest": { - "ext-ctype": "For best performance" + "require-dev": { + "symfony/console": "^6.4|^7.0", + "symfony/security-core": "^6.4|^7.0" }, "type": "library", - "extra": { - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, "autoload": { - "files": [ - "bootstrap.php" - ], "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - } + "Symfony\\Component\\PasswordHasher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1554,24 +1842,22 @@ ], "authors": [ { - "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" + "name": "Robin Chalas", + "email": "robin.chalas@gmail.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill for ctype functions", + "description": "Provides password hashing utilities", "homepage": "https://symfony.com", "keywords": [ - "compatibility", - "ctype", - "polyfill", - "portable" + "hashing", + "password" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.29.0" + "source": "https://github.com/symfony/password-hasher/tree/v7.1.1" }, "funding": [ { @@ -1587,30 +1873,30 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-05-31T14:57:53+00:00" }, { - "name": "symfony/polyfill-mbstring", - "version": "v1.29.0", + "name": "symfony/polyfill-ctype", + "version": "v1.30.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec" + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "0424dff1c58f028c451efff2045f5d92410bd540" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9773676c8a1bb1f8d4340a62efe641cf76eda7ec", - "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/0424dff1c58f028c451efff2045f5d92410bd540", + "reference": "0424dff1c58f028c451efff2045f5d92410bd540", "shasum": "" }, "require": { "php": ">=7.1" }, "provide": { - "ext-mbstring": "*" + "ext-ctype": "*" }, "suggest": { - "ext-mbstring": "For best performance" + "ext-ctype": "For best performance" }, "type": "library", "extra": { @@ -1624,7 +1910,7 @@ "bootstrap.php" ], "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" + "Symfony\\Polyfill\\Ctype\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -1633,25 +1919,24 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill for the Mbstring extension", + "description": "Symfony polyfill for ctype functions", "homepage": "https://symfony.com", "keywords": [ "compatibility", - "mbstring", + "ctype", "polyfill", - "portable", - "shim" + "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.30.0" }, "funding": [ { @@ -1667,25 +1952,28 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { - "name": "symfony/polyfill-php80", - "version": "v1.29.0", + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.30.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b" + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", - "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/64647a7c30b2283f5d49b874d84a18fc22054b7a", + "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a", "shasum": "" }, "require": { "php": ">=7.1" }, + "suggest": { + "ext-intl": "For best performance" + }, "type": "library", "extra": { "thanks": { @@ -1698,21 +1986,14 @@ "bootstrap.php" ], "psr-4": { - "Symfony\\Polyfill\\Php80\\": "" - }, - "classmap": [ - "Resources/stubs" - ] + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], "authors": [ - { - "name": "Ion Bazan", - "email": "ion.bazan@gmail.com" - }, { "name": "Nicolas Grekas", "email": "p@tchwork.com" @@ -1722,16 +2003,18 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "description": "Symfony polyfill for intl's grapheme_* functions", "homepage": "https://symfony.com", "keywords": [ "compatibility", + "grapheme", + "intl", "polyfill", "portable", "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.30.0" }, "funding": [ { @@ -1747,25 +2030,27 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { - "name": "symfony/polyfill-php83", - "version": "v1.29.0", + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.30.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php83.git", - "reference": "86fcae159633351e5fd145d1c47de6c528f8caff" + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/86fcae159633351e5fd145d1c47de6c528f8caff", - "reference": "86fcae159633351e5fd145d1c47de6c528f8caff", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/a95281b0be0d9ab48050ebd988b967875cdb9fdb", + "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb", "shasum": "" }, "require": { - "php": ">=7.1", - "symfony/polyfill-php80": "^1.14" + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" }, "type": "library", "extra": { @@ -1779,7 +2064,7 @@ "bootstrap.php" ], "psr-4": { - "Symfony\\Polyfill\\Php83\\": "" + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" }, "classmap": [ "Resources/stubs" @@ -1799,16 +2084,18 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "description": "Symfony polyfill for intl's Normalizer class and related functions", "homepage": "https://symfony.com", "keywords": [ "compatibility", + "intl", + "normalizer", "polyfill", "portable", "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.30.0" }, "funding": [ { @@ -1824,47 +2111,45 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { - "name": "symfony/routing", - "version": "v7.1.1", + "name": "symfony/polyfill-mbstring", + "version": "v1.30.0", "source": { "type": "git", - "url": "https://github.com/symfony/routing.git", - "reference": "60c31bab5c45af7f13091b87deb708830f3c96c0" + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/60c31bab5c45af7f13091b87deb708830f3c96c0", - "reference": "60c31bab5c45af7f13091b87deb708830f3c96c0", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fd22ab50000ef01661e2a31d850ebaa297f8e03c", + "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3" + "php": ">=7.1" }, - "conflict": { - "symfony/config": "<6.4", - "symfony/dependency-injection": "<6.4", - "symfony/yaml": "<6.4" + "provide": { + "ext-mbstring": "*" }, - "require-dev": { - "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/yaml": "^6.4|^7.0" + "suggest": { + "ext-mbstring": "For best performance" }, "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, "autoload": { + "files": [ + "bootstrap.php" + ], "psr-4": { - "Symfony\\Component\\Routing\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] + "Symfony\\Polyfill\\Mbstring\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1872,24 +2157,25 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Maps an HTTP request to a set of configuration variables", + "description": "Symfony polyfill for the Mbstring extension", "homepage": "https://symfony.com", "keywords": [ - "router", - "routing", - "uri", - "url" + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" ], "support": { - "source": "https://github.com/symfony/routing/tree/v7.1.1" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.30.0" }, "funding": [ { @@ -1905,46 +2191,41 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:57:53+00:00" + "time": "2024-06-19T12:30:46+00:00" }, { - "name": "symfony/service-contracts", - "version": "v3.5.0", + "name": "symfony/polyfill-php83", + "version": "v1.30.0", "source": { "type": "git", - "url": "https://github.com/symfony/service-contracts.git", - "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f" + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "dbdcdf1a4dcc2743591f1079d0c35ab1e2dcbbc9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", - "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/dbdcdf1a4dcc2743591f1079d0c35ab1e2dcbbc9", + "reference": "dbdcdf1a4dcc2743591f1079d0c35ab1e2dcbbc9", "shasum": "" }, "require": { - "php": ">=8.1", - "psr/container": "^1.1|^2.0", - "symfony/deprecation-contracts": "^2.5|^3" - }, - "conflict": { - "ext-psr": "<1.1|>=2" + "php": ">=7.1" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "3.5-dev" - }, "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" } }, "autoload": { + "files": [ + "bootstrap.php" + ], "psr-4": { - "Symfony\\Contracts\\Service\\": "" + "Symfony\\Polyfill\\Php83\\": "" }, - "exclude-from-classmap": [ - "/Test/" + "classmap": [ + "Resources/stubs" ] }, "notification-url": "https://packagist.org/downloads/", @@ -1961,18 +2242,16 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Generic abstractions related to writing services", + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" + "compatibility", + "polyfill", + "portable", + "shim" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.5.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.30.0" }, "funding": [ { @@ -1988,47 +2267,29 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:32:20+00:00" + "time": "2024-06-19T12:35:24+00:00" }, { - "name": "symfony/var-dumper", + "name": "symfony/process", "version": "v7.1.1", "source": { "type": "git", - "url": "https://github.com/symfony/var-dumper.git", - "reference": "deb2c2b506ff6fdbb340e00b34e9901e1605f293" + "url": "https://github.com/symfony/process.git", + "reference": "febf90124323a093c7ee06fdb30e765ca3c20028" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/deb2c2b506ff6fdbb340e00b34e9901e1605f293", - "reference": "deb2c2b506ff6fdbb340e00b34e9901e1605f293", + "url": "https://api.github.com/repos/symfony/process/zipball/febf90124323a093c7ee06fdb30e765ca3c20028", + "reference": "febf90124323a093c7ee06fdb30e765ca3c20028", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/polyfill-mbstring": "~1.0" - }, - "conflict": { - "symfony/console": "<6.4" - }, - "require-dev": { - "ext-iconv": "*", - "symfony/console": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/uid": "^6.4|^7.0", - "twig/twig": "^3.0.4" + "php": ">=8.2" }, - "bin": [ - "Resources/bin/var-dump-server" - ], "type": "library", "autoload": { - "files": [ - "Resources/functions/dump.php" - ], "psr-4": { - "Symfony\\Component\\VarDumper\\": "" + "Symfony\\Component\\Process\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -2040,22 +2301,18 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", - "keywords": [ - "debug", - "dump" - ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.1.1" + "source": "https://github.com/symfony/process/tree/v7.1.1" }, "funding": [ { @@ -2074,31 +2331,30 @@ "time": "2024-05-31T14:57:53+00:00" }, { - "name": "symfony/var-exporter", + "name": "symfony/property-access", "version": "v7.1.1", "source": { "type": "git", - "url": "https://github.com/symfony/var-exporter.git", - "reference": "db82c2b73b88734557cfc30e3270d83fa651b712" + "url": "https://github.com/symfony/property-access.git", + "reference": "74e39e6a6276b8e384f34c6ddbc10a6c9a60193a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/db82c2b73b88734557cfc30e3270d83fa651b712", - "reference": "db82c2b73b88734557cfc30e3270d83fa651b712", + "url": "https://api.github.com/repos/symfony/property-access/zipball/74e39e6a6276b8e384f34c6ddbc10a6c9a60193a", + "reference": "74e39e6a6276b8e384f34c6ddbc10a6c9a60193a", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.2", + "symfony/property-info": "^6.4|^7.0" }, "require-dev": { - "symfony/property-access": "^6.4|^7.0", - "symfony/serializer": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0" + "symfony/cache": "^6.4|^7.0" }, "type": "library", "autoload": { "psr-4": { - "Symfony\\Component\\VarExporter\\": "" + "Symfony\\Component\\PropertyAccess\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -2110,28 +2366,29 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Allows exporting any serializable PHP data structure to plain PHP code", + "description": "Provides functions to read and write from/to an object or array using a simple string notation", "homepage": "https://symfony.com", "keywords": [ - "clone", - "construct", - "export", - "hydrate", - "instantiate", - "lazy-loading", - "proxy", - "serialize" + "access", + "array", + "extraction", + "index", + "injection", + "object", + "property", + "property-path", + "reflection" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v7.1.1" + "source": "https://github.com/symfony/property-access/tree/v7.1.1" }, "funding": [ { @@ -2150,36 +2407,41 @@ "time": "2024-05-31T14:57:53+00:00" }, { - "name": "symfony/yaml", + "name": "symfony/property-info", "version": "v7.1.1", "source": { "type": "git", - "url": "https://github.com/symfony/yaml.git", - "reference": "fa34c77015aa6720469db7003567b9f772492bf2" + "url": "https://github.com/symfony/property-info.git", + "reference": "0f80f818c6728f15de30a4f89866d68e4912ae84" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/fa34c77015aa6720469db7003567b9f772492bf2", - "reference": "fa34c77015aa6720469db7003567b9f772492bf2", + "url": "https://api.github.com/repos/symfony/property-info/zipball/0f80f818c6728f15de30a4f89866d68e4912ae84", + "reference": "0f80f818c6728f15de30a4f89866d68e4912ae84", "shasum": "" }, "require": { "php": ">=8.2", - "symfony/polyfill-ctype": "^1.8" + "symfony/string": "^6.4|^7.0", + "symfony/type-info": "^7.1" }, "conflict": { - "symfony/console": "<6.4" + "phpdocumentor/reflection-docblock": "<5.2", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/dependency-injection": "<6.4", + "symfony/serializer": "<6.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0" + "phpdocumentor/reflection-docblock": "^5.2", + "phpstan/phpdoc-parser": "^1.0", + "symfony/cache": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0" }, - "bin": [ - "Resources/bin/yaml-lint" - ], "type": "library", "autoload": { "psr-4": { - "Symfony\\Component\\Yaml\\": "" + "Symfony\\Component\\PropertyInfo\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -2191,18 +2453,26 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Kévin Dunglas", + "email": "dunglas@gmail.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Loads and dumps YAML files", + "description": "Extracts information about PHP class' properties using metadata of popular sources", "homepage": "https://symfony.com", + "keywords": [ + "doctrine", + "phpdoc", + "property", + "symfony", + "type", + "validator" + ], "support": { - "source": "https://github.com/symfony/yaml/tree/v7.1.1" + "source": "https://github.com/symfony/property-info/tree/v7.1.1" }, "funding": [ { @@ -2219,35 +2489,852 @@ } ], "time": "2024-05-31T14:57:53+00:00" - } - ], - "packages-dev": [ + }, { - "name": "colinodell/json5", - "version": "v2.3.0", + "name": "symfony/routing", + "version": "v7.1.1", "source": { "type": "git", - "url": "https://github.com/colinodell/json5.git", - "reference": "15b063f8cb5e6deb15f0cd39123264ec0d19c710" + "url": "https://github.com/symfony/routing.git", + "reference": "60c31bab5c45af7f13091b87deb708830f3c96c0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/colinodell/json5/zipball/15b063f8cb5e6deb15f0cd39123264ec0d19c710", - "reference": "15b063f8cb5e6deb15f0cd39123264ec0d19c710", + "url": "https://api.github.com/repos/symfony/routing/zipball/60c31bab5c45af7f13091b87deb708830f3c96c0", + "reference": "60c31bab5c45af7f13091b87deb708830f3c96c0", "shasum": "" }, "require": { - "ext-json": "*", - "ext-mbstring": "*", - "php": "^7.1.3|^8.0" + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" }, "conflict": { - "scrutinizer/ocular": "1.7.*" + "symfony/config": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/yaml": "<6.4" }, "require-dev": { - "mikehaertl/php-shellcommand": "^1.2.5", - "phpstan/phpstan": "^1.4", - "scrutinizer/ocular": "^1.6", + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/yaml": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Routing\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Maps an HTTP request to a set of configuration variables", + "homepage": "https://symfony.com", + "keywords": [ + "router", + "routing", + "uri", + "url" + ], + "support": { + "source": "https://github.com/symfony/routing/tree/v7.1.1" + }, + "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-05-31T14:57:53+00:00" + }, + { + "name": "symfony/scheduler", + "version": "v7.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/scheduler.git", + "reference": "024c63fb700453cb53c2d9f71e6868288b0b6fe9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/scheduler/zipball/024c63fb700453cb53c2d9f71e6868288b0b6fe9", + "reference": "024c63fb700453cb53c2d9f71e6868288b0b6fe9", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/clock": "^6.4|^7.0" + }, + "require-dev": { + "dragonmantank/cron-expression": "^3.1", + "symfony/cache": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/lock": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Scheduler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sergey Rabochiy", + "email": "upyx.00@gmail.com" + }, + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides scheduling through Symfony Messenger", + "homepage": "https://symfony.com", + "keywords": [ + "cron", + "schedule", + "scheduler" + ], + "support": { + "source": "https://github.com/symfony/scheduler/tree/v7.1.1" + }, + "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-06-02T15:49:12+00:00" + }, + { + "name": "symfony/security-core", + "version": "v7.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/security-core.git", + "reference": "536399671a46b0e615d69583f067e30ad25ad038" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/security-core/zipball/536399671a46b0e615d69583f067e30ad25ad038", + "reference": "536399671a46b0e615d69583f067e30ad25ad038", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/event-dispatcher-contracts": "^2.5|^3", + "symfony/password-hasher": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/http-foundation": "<6.4", + "symfony/ldap": "<6.4", + "symfony/translation": "<6.4.3|>=7.0,<7.0.3", + "symfony/validator": "<6.4" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "psr/container": "^1.1|^2.0", + "psr/log": "^1|^2|^3", + "symfony/cache": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/ldap": "^6.4|^7.0", + "symfony/string": "^6.4|^7.0", + "symfony/translation": "^6.4.3|^7.0.3", + "symfony/validator": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Security\\Core\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Security Component - Core Library", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/security-core/tree/v7.1.1" + }, + "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-05-31T14:57:53+00:00" + }, + { + "name": "symfony/security-http", + "version": "v7.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/security-http.git", + "reference": "8fa539ad9fe3c45452b8cca8381c2414cef78559" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/security-http/zipball/8fa539ad9fe3c45452b8cca8381c2414cef78559", + "reference": "8fa539ad9fe3c45452b8cca8381c2414cef78559", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/polyfill-mbstring": "~1.0", + "symfony/property-access": "^6.4|^7.0", + "symfony/security-core": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/clock": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/http-client-contracts": "<3.0", + "symfony/security-bundle": "<6.4", + "symfony/security-csrf": "<6.4" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/cache": "^6.4|^7.0", + "symfony/clock": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/http-client-contracts": "^3.0", + "symfony/rate-limiter": "^6.4|^7.0", + "symfony/routing": "^6.4|^7.0", + "symfony/security-csrf": "^6.4|^7.0", + "symfony/translation": "^6.4|^7.0", + "web-token/jwt-library": "^3.3.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Security\\Http\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Security Component - HTTP Integration", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/security-http/tree/v7.1.1" + }, + "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-05-31T14:57:53+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.5.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", + "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "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": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.5.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-04-18T09:32:20+00:00" + }, + { + "name": "symfony/string", + "version": "v7.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "60bc311c74e0af215101235aa6f471bcbc032df2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/60bc311c74e0af215101235aa6f471bcbc032df2", + "reference": "60bc311c74e0af215101235aa6f471bcbc032df2", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.1", + "symfony/error-handler": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v7.1.1" + }, + "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-06-04T06:40:14+00:00" + }, + { + "name": "symfony/type-info", + "version": "v7.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/type-info.git", + "reference": "60b28eb733f1453287f1263ed305b96091e0d1dc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/type-info/zipball/60b28eb733f1453287f1263ed305b96091e0d1dc", + "reference": "60b28eb733f1453287f1263ed305b96091e0d1dc", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/container": "^1.1|^2.0" + }, + "conflict": { + "phpstan/phpdoc-parser": "<1.0", + "symfony/dependency-injection": "<6.4", + "symfony/property-info": "<6.4" + }, + "require-dev": { + "phpstan/phpdoc-parser": "^1.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/property-info": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\TypeInfo\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mathias Arlaud", + "email": "mathias.arlaud@gmail.com" + }, + { + "name": "Baptiste LEDUC", + "email": "baptiste.leduc@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Extracts PHP types information.", + "homepage": "https://symfony.com", + "keywords": [ + "PHPStan", + "phpdoc", + "symfony", + "type" + ], + "support": { + "source": "https://github.com/symfony/type-info/tree/v7.1.1" + }, + "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-05-31T14:59:31+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v7.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "deb2c2b506ff6fdbb340e00b34e9901e1605f293" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/deb2c2b506ff6fdbb340e00b34e9901e1605f293", + "reference": "deb2c2b506ff6fdbb340e00b34e9901e1605f293", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "ext-iconv": "*", + "symfony/console": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/uid": "^6.4|^7.0", + "twig/twig": "^3.0.4" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v7.1.1" + }, + "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-05-31T14:57:53+00:00" + }, + { + "name": "symfony/var-exporter", + "version": "v7.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-exporter.git", + "reference": "db82c2b73b88734557cfc30e3270d83fa651b712" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/db82c2b73b88734557cfc30e3270d83fa651b712", + "reference": "db82c2b73b88734557cfc30e3270d83fa651b712", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "symfony/property-access": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\VarExporter\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows exporting any serializable PHP data structure to plain PHP code", + "homepage": "https://symfony.com", + "keywords": [ + "clone", + "construct", + "export", + "hydrate", + "instantiate", + "lazy-loading", + "proxy", + "serialize" + ], + "support": { + "source": "https://github.com/symfony/var-exporter/tree/v7.1.1" + }, + "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-05-31T14:57:53+00:00" + }, + { + "name": "symfony/yaml", + "version": "v7.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "fa34c77015aa6720469db7003567b9f772492bf2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/fa34c77015aa6720469db7003567b9f772492bf2", + "reference": "fa34c77015aa6720469db7003567b9f772492bf2", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0" + }, + "bin": [ + "Resources/bin/yaml-lint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v7.1.1" + }, + "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-05-31T14:57:53+00:00" + } + ], + "packages-dev": [ + { + "name": "colinodell/json5", + "version": "v2.3.0", + "source": { + "type": "git", + "url": "https://github.com/colinodell/json5.git", + "reference": "15b063f8cb5e6deb15f0cd39123264ec0d19c710" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/colinodell/json5/zipball/15b063f8cb5e6deb15f0cd39123264ec0d19c710", + "reference": "15b063f8cb5e6deb15f0cd39123264ec0d19c710", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "php": "^7.1.3|^8.0" + }, + "conflict": { + "scrutinizer/ocular": "1.7.*" + }, + "require-dev": { + "mikehaertl/php-shellcommand": "^1.2.5", + "phpstan/phpstan": "^1.4", + "scrutinizer/ocular": "^1.6", "squizlabs/php_codesniffer": "^2.3 || ^3.0", "symfony/finder": "^4.4|^5.4|^6.0", "symfony/phpunit-bridge": "^5.4|^6.0" @@ -2969,16 +4056,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.11.1", + "version": "1.12.0", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" + "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", - "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", + "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", "shasum": "" }, "require": { @@ -2986,11 +4073,12 @@ }, "conflict": { "doctrine/collections": "<1.6.8", - "doctrine/common": "<2.13.3 || >=3,<3.2.2" + "doctrine/common": "<2.13.3 || >=3 <3.2.2" }, "require-dev": { "doctrine/collections": "^1.6.8", "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" }, "type": "library", @@ -3016,7 +4104,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" + "source": "https://github.com/myclabs/DeepCopy/tree/1.12.0" }, "funding": [ { @@ -3024,7 +4112,7 @@ "type": "tidelift" } ], - "time": "2023-03-08T13:26:56+00:00" + "time": "2024-06-12T14:39:25+00:00" }, { "name": "nikic/php-parser", @@ -3663,16 +4751,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.5.20", + "version": "10.5.24", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "547d314dc24ec1e177720d45c6263fb226cc2ae3" + "reference": "5f124e3e3e561006047b532fd0431bf5bb6b9015" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/547d314dc24ec1e177720d45c6263fb226cc2ae3", - "reference": "547d314dc24ec1e177720d45c6263fb226cc2ae3", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/5f124e3e3e561006047b532fd0431bf5bb6b9015", + "reference": "5f124e3e3e561006047b532fd0431bf5bb6b9015", "shasum": "" }, "require": { @@ -3744,7 +4832,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.20" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.24" }, "funding": [ { @@ -3760,7 +4848,7 @@ "type": "tidelift" } ], - "time": "2024-04-24T06:32:35+00:00" + "time": "2024-06-20T13:09:54+00:00" }, { "name": "roave/security-advisories", @@ -3768,12 +4856,12 @@ "source": { "type": "git", "url": "https://github.com/Roave/SecurityAdvisories.git", - "reference": "01c19f1a89daf51e8355cc0b7e3113d5274929b5" + "reference": "59b0da5b0c3aea934f731036370e1dcb7c56da19" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/01c19f1a89daf51e8355cc0b7e3113d5274929b5", - "reference": "01c19f1a89daf51e8355cc0b7e3113d5274929b5", + "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/59b0da5b0c3aea934f731036370e1dcb7c56da19", + "reference": "59b0da5b0c3aea934f731036370e1dcb7c56da19", "shasum": "" }, "conflict": { @@ -3781,7 +4869,7 @@ "admidio/admidio": "<4.2.13", "adodb/adodb-php": "<=5.20.20|>=5.21,<=5.21.3", "aheinze/cockpit": "<2.2", - "aimeos/ai-client-html": ">=2020.04.1,<2020.10.27|>=2021.04.1,<2021.10.21|>=2022.04.1,<2022.10.12|>=2023.04.1,<2023.10.14|>=2024.04.1,<2024.04.4", + "aimeos/ai-client-html": ">=2020.04.1,<2020.10.27|>=2021.04.1,<2021.10.22|>=2022.04.1,<2022.10.13|>=2023.04.1,<2023.10.15|>=2024.04.1,<2024.04.7", "aimeos/aimeos-core": ">=2022.04.1,<2022.10.17|>=2023.04.1,<2023.10.17|>=2024.04.1,<2024.04.7", "aimeos/aimeos-typo3": "<19.10.12|>=20,<20.10.5", "airesvsg/acf-to-rest-api": "<=3.1", @@ -3833,6 +4921,7 @@ "bmarshall511/wordpress_zero_spam": "<5.2.13", "bolt/bolt": "<3.7.2", "bolt/core": "<=4.2", + "born05/craft-twofactorauthentication": "<3.3.4", "bottelet/flarepoint": "<2.2.1", "bref/bref": "<2.1.17", "brightlocal/phpwhois": "<=4.2.5", @@ -3864,7 +4953,7 @@ "codeigniter4/framework": "<4.4.7", "codeigniter4/shield": "<1.0.0.0-beta8", "codiad/codiad": "<=2.8.4", - "composer/composer": "<1.10.27|>=2,<2.2.23|>=2.3,<2.7", + "composer/composer": "<1.10.27|>=2,<2.2.24|>=2.3,<2.7.7", "concrete5/concrete5": "<9.2.8", "concrete5/core": "<8.5.8|>=9,<9.1", "contao-components/mediaelement": ">=2.14.2,<2.21.1", @@ -3984,7 +5073,7 @@ "funadmin/funadmin": "<=3.2|>=3.3.2,<=3.3.3", "gaoming13/wechat-php-sdk": "<=1.10.2", "genix/cms": "<=1.1.11", - "getformwork/formwork": "<1.13", + "getformwork/formwork": "<1.13.1|==2.0.0.0-beta1", "getgrav/grav": "<1.7.46", "getkirby/cms": "<4.1.1", "getkirby/kirby": "<=2.5.12", @@ -3998,7 +5087,7 @@ "gos/web-socket-bundle": "<1.10.4|>=2,<2.6.1|>=3,<3.3", "gree/jose": "<2.2.1", "gregwar/rst": "<1.0.3", - "grumpydictator/firefly-iii": "<6.1.7", + "grumpydictator/firefly-iii": "<6.1.17", "gugoan/economizzer": "<=0.9.0.0-beta1", "guzzlehttp/guzzle": "<6.5.8|>=7,<7.4.5", "guzzlehttp/psr7": "<1.9.1|>=2,<2.4.5", @@ -4055,6 +5144,7 @@ "jsdecena/laracom": "<2.0.9", "jsmitty12/phpwhois": "<5.1", "juzaweb/cms": "<=3.4", + "jweiland/events2": "<8.3.8|>=9,<9.0.6", "kazist/phpwhois": "<=4.2.6", "kelvinmo/simplexrd": "<3.1.1", "kevinpapst/kimai2": "<1.16.7", @@ -4092,7 +5182,7 @@ "lms/routes": "<2.1.1", "localizationteam/l10nmgr": "<7.4|>=8,<8.7|>=9,<9.2", "luyadev/yii-helpers": "<1.2.1", - "magento/community-edition": "<2.4.3.0-patch3|>=2.4.4,<2.4.5", + "magento/community-edition": "<2.4.5|==2.4.5|>=2.4.5.0-patch1,<2.4.5.0-patch8|==2.4.6|>=2.4.6.0-patch1,<2.4.6.0-patch6|==2.4.7", "magento/core": "<=1.9.4.5", "magento/magento1ce": "<1.9.4.3-dev", "magento/magento1ee": ">=1,<1.14.4.3-dev", @@ -4125,7 +5215,7 @@ "mojo42/jirafeau": "<4.4", "mongodb/mongodb": ">=1,<1.9.2", "monolog/monolog": ">=1.8,<1.12", - "moodle/moodle": "<4.3.4", + "moodle/moodle": "<4.3.5|>=4.4.0.0-beta,<4.4.1", "mos/cimage": "<0.7.19", "movim/moxl": ">=0.8,<=0.10", "movingbytes/social-network": "<=1.2.1", @@ -4167,7 +5257,7 @@ "onelogin/php-saml": "<2.10.4", "oneup/uploader-bundle": ">=1,<1.9.3|>=2,<2.1.5", "open-web-analytics/open-web-analytics": "<1.7.4", - "opencart/opencart": "<=3.0.3.7|>=4,<4.0.2.3-dev", + "opencart/opencart": "<=3.0.3.9|>=4", "openid/php-openid": "<2.3", "openmage/magento-lts": "<20.5", "opensolutions/vimbadmin": "<=3.0.15", @@ -4318,7 +5408,7 @@ "slim/slim": "<2.6", "slub/slub-events": "<3.0.3", "smarty/smarty": "<4.5.3|>=5,<5.1.1", - "snipe/snipe-it": "<=6.2.2", + "snipe/snipe-it": "<6.4.2", "socalnick/scn-social-auth": "<1.15.2", "socialiteproviders/steam": "<1.1", "spatie/browsershot": "<3.57.4", @@ -4331,8 +5421,10 @@ "statamic/cms": "<4.46|>=5.3,<5.6.2", "stormpath/sdk": "<9.9.99", "studio-42/elfinder": "<2.1.62", + "studiomitte/friendlycaptcha": "<0.1.4", "subhh/libconnect": "<7.0.8|>=8,<8.1", "sukohi/surpass": "<1", + "sulu/form-bundle": ">=2,<2.5.3", "sulu/sulu": "<1.6.44|>=2,<2.4.17|>=2.5,<2.5.13", "sumocoders/framework-user-bundle": "<1.4", "superbig/craft-audit": "<3.0.2", @@ -4396,7 +5488,7 @@ "thorsten/phpmyfaq": "<3.2.2", "tikiwiki/tiki-manager": "<=17.1", "timber/timber": ">=0.16.6,<1.23.1|>=1.24,<1.24.1|>=2,<2.1", - "tinymce/tinymce": "<7", + "tinymce/tinymce": "<7.2", "tinymighty/wiki-seo": "<1.2.2", "titon/framework": "<9.9.99", "tobiasbg/tablepress": "<=2.0.0.0-RC1", @@ -4459,7 +5551,7 @@ "winter/wn-dusk-plugin": "<2.1", "winter/wn-system-module": "<1.2.4", "wintercms/winter": "<=1.2.3", - "woocommerce/woocommerce": "<6.6", + "woocommerce/woocommerce": "<6.6|>=8.8,<8.8.5|>=8.9,<8.9.3", "wp-cli/wp-cli": ">=0.12,<2.5", "wp-graphql/wp-graphql": "<=1.14.5", "wp-premium/gravityforms": "<2.4.21", @@ -4502,7 +5594,7 @@ "zendframework/zend-ldap": ">=2,<2.0.99|>=2.1,<2.1.99|>=2.2,<2.2.8|>=2.3,<2.3.3", "zendframework/zend-mail": "<2.4.11|>=2.5,<2.7.2", "zendframework/zend-navigation": ">=2,<2.2.7|>=2.3,<2.3.1", - "zendframework/zend-session": ">=2,<2.0.99|>=2.1,<2.1.99|>=2.2,<2.2.9|>=2.3,<2.3.4", + "zendframework/zend-session": ">=2,<2.2.9|>=2.3,<2.3.4", "zendframework/zend-validator": ">=2.3,<2.3.6", "zendframework/zend-view": ">=2,<2.2.7|>=2.3,<2.3.1", "zendframework/zend-xmlrpc": ">=2.1,<2.1.6|>=2.2,<2.2.6", @@ -4561,7 +5653,7 @@ "type": "tidelift" } ], - "time": "2024-06-05T14:05:01+00:00" + "time": "2024-06-25T18:06:06+00:00" }, { "name": "sanmai/later", @@ -4629,16 +5721,16 @@ }, { "name": "sanmai/pipeline", - "version": "v6.10", + "version": "v6.11", "source": { "type": "git", "url": "https://github.com/sanmai/pipeline.git", - "reference": "cbd2ea30ba8bef596b8dad1adb9c92fb2987e430" + "reference": "a5fa2a6c6ca93efa37e7c24aab72f47448a6b110" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sanmai/pipeline/zipball/cbd2ea30ba8bef596b8dad1adb9c92fb2987e430", - "reference": "cbd2ea30ba8bef596b8dad1adb9c92fb2987e430", + "url": "https://api.github.com/repos/sanmai/pipeline/zipball/a5fa2a6c6ca93efa37e7c24aab72f47448a6b110", + "reference": "a5fa2a6c6ca93efa37e7c24aab72f47448a6b110", "shasum": "" }, "require": { @@ -4682,7 +5774,7 @@ "description": "General-purpose collections pipeline", "support": { "issues": "https://github.com/sanmai/pipeline/issues", - "source": "https://github.com/sanmai/pipeline/tree/v6.10" + "source": "https://github.com/sanmai/pipeline/tree/v6.11" }, "funding": [ { @@ -4690,7 +5782,7 @@ "type": "github" } ], - "time": "2024-03-16T01:33:30+00:00" + "time": "2024-06-15T03:11:19+00:00" }, { "name": "sebastian/cli-parser", @@ -5230,236 +6322,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "6.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Snapshotting of global state", - "homepage": "https://www.github.com/sebastianbergmann/global-state", - "keywords": [ - "global state" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/global-state/issues", - "security": "https://github.com/sebastianbergmann/global-state/security/policy", - "source": "https://github.com/sebastianbergmann/global-state/tree/6.0.2" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-03-02T07:19:19+00:00" - }, - { - "name": "sebastian/lines-of-code", - "version": "2.0.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/856e7f6a75a84e339195d48c556f23be2ebf75d0", - "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0", - "shasum": "" - }, - "require": { - "nikic/php-parser": "^4.18 || ^5.0", - "php": ">=8.1" - }, - "require-dev": { - "phpunit/phpunit": "^10.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "2.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library for counting the lines of code in PHP source code", - "homepage": "https://github.com/sebastianbergmann/lines-of-code", - "support": { - "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", - "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/2.0.2" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2023-12-21T08:38:20+00:00" - }, - { - "name": "sebastian/object-enumerator", - "version": "5.0.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/202d0e344a580d7f7d04b3fafce6933e59dae906", - "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906", - "shasum": "" - }, - "require": { - "php": ">=8.1", - "sebastian/object-reflector": "^3.0", - "sebastian/recursion-context": "^5.0" - }, - "require-dev": { - "phpunit/phpunit": "^10.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "5.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Traverses array structures and object graphs to enumerate all referenced objects", - "homepage": "https://github.com/sebastianbergmann/object-enumerator/", - "support": { - "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", - "source": "https://github.com/sebastianbergmann/object-enumerator/tree/5.0.0" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2023-02-03T07:08:32+00:00" - }, - { - "name": "sebastian/object-reflector", - "version": "3.0.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "24ed13d98130f0e7122df55d06c5c4942a577957" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/24ed13d98130f0e7122df55d06c5c4942a577957", - "reference": "24ed13d98130f0e7122df55d06c5c4942a577957", - "shasum": "" - }, - "require": { - "php": ">=8.1" - }, - "require-dev": { - "phpunit/phpunit": "^10.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Allows reflection of object attributes, including inherited and non-public ones", - "homepage": "https://github.com/sebastianbergmann/object-reflector/", - "support": { - "issues": "https://github.com/sebastianbergmann/object-reflector/issues", - "source": "https://github.com/sebastianbergmann/object-reflector/tree/3.0.0" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2023-02-03T07:06:18+00:00" - }, - { - "name": "sebastian/recursion-context", - "version": "5.0.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "05909fb5bc7df4c52992396d0116aed689f93712" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/05909fb5bc7df4c52992396d0116aed689f93712", - "reference": "05909fb5bc7df4c52992396d0116aed689f93712", - "shasum": "" - }, - "require": { - "php": ">=8.1" - }, - "require-dev": { - "phpunit/phpunit": "^10.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "5.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -5475,21 +6338,17 @@ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de" - }, - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Adam Harvey", - "email": "aharvey@php.net" } ], - "description": "Provides functionality to recursively process PHP variables", - "homepage": "https://github.com/sebastianbergmann/recursion-context", + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], "support": { - "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.0" + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/6.0.2" }, "funding": [ { @@ -5497,23 +6356,24 @@ "type": "github" } ], - "time": "2023-02-03T07:05:40+00:00" + "time": "2024-03-02T07:19:19+00:00" }, { - "name": "sebastian/type", - "version": "4.0.0", + "name": "sebastian/lines-of-code", + "version": "2.0.2", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/type.git", - "reference": "462699a16464c3944eefc02ebdd77882bd3925bf" + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/462699a16464c3944eefc02ebdd77882bd3925bf", - "reference": "462699a16464c3944eefc02ebdd77882bd3925bf", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/856e7f6a75a84e339195d48c556f23be2ebf75d0", + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0", "shasum": "" }, "require": { + "nikic/php-parser": "^4.18 || ^5.0", "php": ">=8.1" }, "require-dev": { @@ -5522,7 +6382,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "4.0-dev" + "dev-main": "2.0-dev" } }, "autoload": { @@ -5541,11 +6401,12 @@ "role": "lead" } ], - "description": "Collection of value objects that represent the types of the PHP type system", - "homepage": "https://github.com/sebastianbergmann/type", + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", "support": { - "issues": "https://github.com/sebastianbergmann/type/issues", - "source": "https://github.com/sebastianbergmann/type/tree/4.0.0" + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/2.0.2" }, "funding": [ { @@ -5553,29 +6414,34 @@ "type": "github" } ], - "time": "2023-02-03T07:10:45+00:00" + "time": "2023-12-21T08:38:20+00:00" }, { - "name": "sebastian/version", - "version": "4.0.1", + "name": "sebastian/object-enumerator", + "version": "5.0.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/version.git", - "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17" + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c51fa83a5d8f43f1402e3f32a005e6262244ef17", - "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/202d0e344a580d7f7d04b3fafce6933e59dae906", + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "4.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -5590,15 +6456,14 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "email": "sebastian@phpunit.de" } ], - "description": "Library that helps with managing the version number of Git-hosted PHP projects", - "homepage": "https://github.com/sebastianbergmann/version", + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", "support": { - "issues": "https://github.com/sebastianbergmann/version/issues", - "source": "https://github.com/sebastianbergmann/version/tree/4.0.1" + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/5.0.0" }, "funding": [ { @@ -5606,440 +6471,362 @@ "type": "github" } ], - "time": "2023-02-07T11:34:05+00:00" + "time": "2023-02-03T07:08:32+00:00" }, { - "name": "squizlabs/php_codesniffer", - "version": "3.10.1", + "name": "sebastian/object-reflector", + "version": "3.0.0", "source": { "type": "git", - "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "8f90f7a53ce271935282967f53d0894f8f1ff877" + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/8f90f7a53ce271935282967f53d0894f8f1ff877", - "reference": "8f90f7a53ce271935282967f53d0894f8f1ff877", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/24ed13d98130f0e7122df55d06c5c4942a577957", + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957", "shasum": "" }, "require": { - "ext-simplexml": "*", - "ext-tokenizer": "*", - "ext-xmlwriter": "*", - "php": ">=5.4.0" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" + "phpunit/phpunit": "^10.0" }, - "bin": [ - "bin/phpcbf", - "bin/phpcs" - ], "type": "library", "extra": { "branch-alias": { - "dev-master": "3.x-dev" + "dev-main": "3.0-dev" } }, + "autoload": { + "classmap": [ + "src/" + ] + }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], "authors": [ { - "name": "Greg Sherwood", - "role": "Former lead" - }, - { - "name": "Juliette Reinders Folmer", - "role": "Current lead" - }, - { - "name": "Contributors", - "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" } ], - "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", - "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer", - "keywords": [ - "phpcs", - "standards", - "static analysis" - ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", "support": { - "issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues", - "security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy", - "source": "https://github.com/PHPCSStandards/PHP_CodeSniffer", - "wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki" + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/3.0.0" }, "funding": [ { - "url": "https://github.com/PHPCSStandards", - "type": "github" - }, - { - "url": "https://github.com/jrfnl", + "url": "https://github.com/sebastianbergmann", "type": "github" - }, - { - "url": "https://opencollective.com/php_codesniffer", - "type": "open_collective" } ], - "time": "2024-05-22T21:24:41+00:00" + "time": "2023-02-03T07:06:18+00:00" }, { - "name": "symfony/console", - "version": "v7.1.1", + "name": "sebastian/recursion-context", + "version": "5.0.0", "source": { "type": "git", - "url": "https://github.com/symfony/console.git", - "reference": "9b008f2d7b21c74ef4d0c3de6077a642bc55ece3" + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "05909fb5bc7df4c52992396d0116aed689f93712" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/9b008f2d7b21c74ef4d0c3de6077a642bc55ece3", - "reference": "9b008f2d7b21c74ef4d0c3de6077a642bc55ece3", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/05909fb5bc7df4c52992396d0116aed689f93712", + "reference": "05909fb5bc7df4c52992396d0116aed689f93712", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/polyfill-mbstring": "~1.0", - "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^6.4|^7.0" - }, - "conflict": { - "symfony/dependency-injection": "<6.4", - "symfony/dotenv": "<6.4", - "symfony/event-dispatcher": "<6.4", - "symfony/lock": "<6.4", - "symfony/process": "<6.4" - }, - "provide": { - "psr/log-implementation": "1.0|2.0|3.0" + "php": ">=8.1" }, "require-dev": { - "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/lock": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/stopwatch": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0" + "phpunit/phpunit": "^10.0" }, "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, "autoload": { - "psr-4": { - "Symfony\\Component\\Console\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" + "classmap": [ + "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" }, { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" } ], - "description": "Eases the creation of beautiful and testable command line interfaces", - "homepage": "https://symfony.com", - "keywords": [ - "cli", - "command-line", - "console", - "terminal" - ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { - "source": "https://github.com/symfony/console/tree/v7.1.1" + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.0" }, "funding": [ { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", + "url": "https://github.com/sebastianbergmann", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" } ], - "time": "2024-05-31T14:57:53+00:00" + "time": "2023-02-03T07:05:40+00:00" }, { - "name": "symfony/polyfill-intl-grapheme", - "version": "v1.29.0", + "name": "sebastian/type", + "version": "4.0.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f" + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/32a9da87d7b3245e09ac426c83d334ae9f06f80f", - "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/462699a16464c3944eefc02ebdd77882bd3925bf", + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=8.1" }, - "suggest": { - "ext-intl": "For best performance" + "require-dev": { + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "branch-alias": { + "dev-main": "4.0-dev" } }, "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Intl\\Grapheme\\": "" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "Symfony polyfill for intl's grapheme_* functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "grapheme", - "intl", - "polyfill", - "portable", - "shim" - ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.29.0" + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/4.0.0" }, "funding": [ { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", + "url": "https://github.com/sebastianbergmann", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2023-02-03T07:10:45+00:00" }, { - "name": "symfony/polyfill-intl-normalizer", - "version": "v1.29.0", + "name": "sebastian/version", + "version": "4.0.1", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "bc45c394692b948b4d383a08d7753968bed9a83d" + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/bc45c394692b948b4d383a08d7753968bed9a83d", - "reference": "bc45c394692b948b4d383a08d7753968bed9a83d", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c51fa83a5d8f43f1402e3f32a005e6262244ef17", + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17", "shasum": "" }, "require": { - "php": ">=7.1" - }, - "suggest": { - "ext-intl": "For best performance" + "php": ">=8.1" }, "type": "library", "extra": { - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "branch-alias": { + "dev-main": "4.0-dev" } }, "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Intl\\Normalizer\\": "" - }, "classmap": [ - "Resources/stubs" + "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "Symfony polyfill for intl's Normalizer class and related functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "intl", - "normalizer", - "polyfill", - "portable", - "shim" - ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.29.0" + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/4.0.1" }, "funding": [ { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", + "url": "https://github.com/sebastianbergmann", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2023-02-07T11:34:05+00:00" }, { - "name": "symfony/process", - "version": "v7.1.1", + "name": "squizlabs/php_codesniffer", + "version": "3.10.1", "source": { "type": "git", - "url": "https://github.com/symfony/process.git", - "reference": "febf90124323a093c7ee06fdb30e765ca3c20028" + "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", + "reference": "8f90f7a53ce271935282967f53d0894f8f1ff877" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/febf90124323a093c7ee06fdb30e765ca3c20028", - "reference": "febf90124323a093c7ee06fdb30e765ca3c20028", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/8f90f7a53ce271935282967f53d0894f8f1ff877", + "reference": "8f90f7a53ce271935282967f53d0894f8f1ff877", "shasum": "" }, "require": { - "php": ">=8.2" + "ext-simplexml": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" }, + "bin": [ + "bin/phpcbf", + "bin/phpcs" + ], "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Process\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Greg Sherwood", + "role": "Former lead" }, { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Juliette Reinders Folmer", + "role": "Current lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors" } ], - "description": "Executes commands in sub-processes", - "homepage": "https://symfony.com", + "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "keywords": [ + "phpcs", + "standards", + "static analysis" + ], "support": { - "source": "https://github.com/symfony/process/tree/v7.1.1" + "issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues", + "security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy", + "source": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki" }, "funding": [ { - "url": "https://symfony.com/sponsor", - "type": "custom" + "url": "https://github.com/PHPCSStandards", + "type": "github" }, { - "url": "https://github.com/fabpot", + "url": "https://github.com/jrfnl", "type": "github" }, { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" } ], - "time": "2024-05-31T14:57:53+00:00" + "time": "2024-05-22T21:24:41+00:00" }, { - "name": "symfony/string", + "name": "symfony/console", "version": "v7.1.1", "source": { "type": "git", - "url": "https://github.com/symfony/string.git", - "reference": "60bc311c74e0af215101235aa6f471bcbc032df2" + "url": "https://github.com/symfony/console.git", + "reference": "9b008f2d7b21c74ef4d0c3de6077a642bc55ece3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/60bc311c74e0af215101235aa6f471bcbc032df2", - "reference": "60bc311c74e0af215101235aa6f471bcbc032df2", + "url": "https://api.github.com/repos/symfony/console/zipball/9b008f2d7b21c74ef4d0c3de6077a642bc55ece3", + "reference": "9b008f2d7b21c74ef4d0c3de6077a642bc55ece3", "shasum": "" }, "require": { "php": ">=8.2", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-intl-grapheme": "~1.0", - "symfony/polyfill-intl-normalizer": "~1.0", - "symfony/polyfill-mbstring": "~1.0" + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^6.4|^7.0" }, "conflict": { - "symfony/translation-contracts": "<2.5" + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" }, "require-dev": { - "symfony/emoji": "^7.1", - "symfony/error-handler": "^6.4|^7.0", - "symfony/http-client": "^6.4|^7.0", - "symfony/intl": "^6.4|^7.0", - "symfony/translation-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^6.4|^7.0" + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" }, "type": "library", "autoload": { - "files": [ - "Resources/functions.php" - ], "psr-4": { - "Symfony\\Component\\String\\": "" + "Symfony\\Component\\Console\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -6051,26 +6838,24 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "description": "Eases the creation of beautiful and testable command line interfaces", "homepage": "https://symfony.com", "keywords": [ - "grapheme", - "i18n", - "string", - "unicode", - "utf-8", - "utf8" + "cli", + "command-line", + "console", + "terminal" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.1.1" + "source": "https://github.com/symfony/console/tree/v7.1.1" }, "funding": [ { @@ -6086,7 +6871,7 @@ "type": "tidelift" } ], - "time": "2024-06-04T06:40:14+00:00" + "time": "2024-05-31T14:57:53+00:00" }, { "name": "thecodingmachine/safe", @@ -6344,7 +7129,9 @@ "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": ">=8.1 <8.4.0" + "php": ">=8.1 <8.4.0", + "ext-posix": "*", + "ext-zend-opcache": "*" }, "platform-dev": [], "plugin-api-version": "2.6.0" diff --git a/config/routes.yaml b/config/routes.yaml index f6324ca..5b3693a 100644 --- a/config/routes.yaml +++ b/config/routes.yaml @@ -1,3 +1,3 @@ -atoolo_deployment: - resource: '@AtooloDeploymentBundle/Controller/' +controller: + resource: '@AtooloRuntimeCheckBundle/Controller/' type: attribute diff --git a/config/services.yaml b/config/services.yaml index 2c6a44d..db0e9f5 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -1,25 +1,91 @@ +parameters: + atoolo_runtime_check.worker_status_file: '%kernel.cache_dir%/%atoolo_resource.resource_host%_schedule_check.json' + env(SUPERVISOR_GROUP_NAME): default + ## The period in minutes after which the worker status file is updated. + atoolo_runtime_check.worker_status_update_period: 10 + atoolo_runtime_check.checker.monolog.max_log_file_size: '100M' + atoolo_runtime_check.checker.monolog.max_log_dir_size: '10G' + atoolo_runtime_check.checker.monolog.max_log_file_rotations: 10 + services: _defaults: autowire: true autoconfigure: true + _instanceof: Symfony\Component\Console\Command\Command: tags: ['command'] - Atoolo\Deployment\Service\StopWorkerOnRedeployListener: + Atoolo\Runtime\Check\Service\Cli\FastCgiStatusFactory: arguments: - - '@logger' + - [ + '/var/run/php-fpm.sock', + '/var/run/php/php-fpm.sock', + '/var/run/php*.sock', + '/var/run/php/*.sock' + ] + - '%kernel.project_dir%/public/index.php' + - '%atoolo_resource.resource_root%' + - '%atoolo_resource.resource_host%' + + Atoolo\Runtime\Check\Service\ProcessStatus: ~ + + Atoolo\Runtime\Check\Service\Checker\PhpStatus: + tags: + - { name: 'atoolo_runtime_check.checker' } - Atoolo\Deployment\Service\Deployer: + Atoolo\Runtime\Check\Service\Checker\ProcessStatus: + tags: + - { name: 'atoolo_runtime_check.checker' } + + Atoolo\Runtime\Check\Service\Checker\MonologChecker: arguments: - - !tagged_iterator { tag: 'atoolo_deployment.deploy_executor' } + - '%atoolo_runtime_check.checker.monolog.max_log_file_size%' + - '%atoolo_runtime_check.checker.monolog.max_log_dir_size%' + - '%atoolo_runtime_check.checker.monolog.max_log_file_rotations%' - '@logger' + tags: + - { name: 'atoolo_runtime_check.checker' } + + Atoolo\Runtime\Check\Service\Checker\CheckerCollection: + arguments: + - !tagged_iterator atoolo_runtime_check.checker + + Atoolo\Runtime\Check\Service\Cli\RuntimeCheck: + arguments: + - '@Atoolo\Runtime\Check\Service\Checker\CheckerCollection' + - '@Atoolo\Runtime\Check\Service\Cli\FastCgiStatusFactory' + - '@Atoolo\Runtime\Check\Service\Worker\WorkerStatusFile' + + Atoolo\Runtime\Check\Service\FpmFcgi\CliStatus: + arguments: + - '%kernel.project_dir%/bin/console' + - '%atoolo_resource.resource_root%' + - '%atoolo_resource.resource_host%' + + Atoolo\Runtime\Check\Service\FpmFcgi\RuntimeCheck: + arguments: + - '@Atoolo\Runtime\Check\Service\Checker\CheckerCollection' + - '@Atoolo\Runtime\Check\Service\FpmFcgi\CliStatus' + - '@Atoolo\Runtime\Check\Service\Worker\WorkerStatusFile' + + + Atoolo\Runtime\Check\Service\Worker\WorkerStatusFile: + arguments: + - '%atoolo_runtime_check.worker_status_file%' + - '%atoolo_runtime_check.worker_status_update_period%' + + Atoolo\Runtime\Check\Service\Worker\WorkerCheckScheduler: + arguments: + - '@Atoolo\Runtime\Check\Service\Worker\WorkerStatusFile' + - '@Atoolo\Runtime\Check\Service\Checker\CheckerCollection' + - '%atoolo_resource.resource_host%' - Atoolo\Deployment\Console\Command\DeployCommand: + Atoolo\Runtime\Check\Console\Command\CheckCommand: arguments: - - '@Atoolo\Deployment\Service\Deployer' + - '@Atoolo\Runtime\Check\Service\Cli\RuntimeCheck' - Atoolo\Deployment\Controller\DeployController: + Atoolo\Runtime\Check\Controller\CheckController: arguments: - - '@Atoolo\Deployment\Service\Deployer' + - '@Atoolo\Runtime\Check\Service\FpmFcgi\RuntimeCheck' diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 0a889e0..8eeb9bb 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -3,3 +3,20 @@ parameters: tmpDir: var/cache/phpstan paths: - src + typeAliases: + CheckStatusData: ''' + array{ + success?: ?mixed, + reports?: array>, + messages?: array> + } + ''' + RuntimeStatusData: ''' + array{ + success?: ?mixed, + cli: CheckStatusData, + fpm-fcgi: CheckStatusData, + worker: CheckStatusData, + messages?: array + } + ''' diff --git a/src/AtooloRuntimeCheckBundle.php b/src/AtooloRuntimeCheckBundle.php index 40aa548..b9c8d33 100644 --- a/src/AtooloRuntimeCheckBundle.php +++ b/src/AtooloRuntimeCheckBundle.php @@ -5,11 +5,11 @@ namespace Atoolo\Runtime\Check; use Exception; +use Symfony\Component\Config\FileLocator; use Symfony\Component\Config\Loader\GlobFileLoader; use Symfony\Component\Config\Loader\LoaderResolver; -use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\Config\FileLocator; +use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; use Symfony\Component\HttpKernel\Bundle\Bundle; /** diff --git a/src/Console/Command/CheckCommand.php b/src/Console/Command/CheckCommand.php index 649ab2b..df9f295 100644 --- a/src/Console/Command/CheckCommand.php +++ b/src/Console/Command/CheckCommand.php @@ -4,12 +4,15 @@ namespace Atoolo\Runtime\Check\Console\Command; -use Atoolo\Deployment\Service\Deployer; -use Psr\Log\LoggerAwareInterface; -use Psr\Log\LoggerAwareTrait; +use Atoolo\Runtime\Check\Console\Command\Io\TypifiedInput; +use Atoolo\Runtime\Check\Service\Cli\RuntimeCheck; +use Atoolo\Runtime\Check\Service\RuntimeStatus; +use Atoolo\Runtime\Check\Service\RuntimeType; +use JsonException; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; #[AsCommand( @@ -18,15 +21,133 @@ )] final class CheckCommand extends Command { - public function __construct() - { + public function __construct( + private readonly RuntimeCheck $runtimeCheck + ) { parent::__construct(); } + protected function configure(): void + { + $runtimeTypes = implode( + ', ', + array_map( + fn(RuntimeType $type) => $type->value, + RuntimeType::cases() + ) + ); + $this + ->setHelp('Command to performs a check of the runtime environment') + ->addOption( + 'skip', + null, + InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, + 'Skip check for different runtime-types' + . ' (' . $runtimeTypes . ')' + . ' and scopes (e.g. php, logging, ...)', + [] + ) + ->addOption( + 'fpm-socket', + null, + InputOption::VALUE_REQUIRED, + 'fpm FastCGI socket like 127.0.0.1:9000 or ' + . '/var/run/php/php8.3-fpm.sock' + ) + ->addOption( + 'fail-on-error', + null, + InputOption::VALUE_OPTIONAL, + 'returns the exit code 1 if an error occurs', + true + ) + ->addOption( + 'json', + null, + InputOption::VALUE_NEGATABLE, + 'output result in json.', + false + ) + ; + } + + /** + * @throws JsonException + */ protected function execute( InputInterface $input, OutputInterface $output ): int { - return Command::SUCCESS; + + $typedInput = new TypifiedInput($input); + + $runtimeStatus = $this->runtimeCheck->execute( + $typedInput->getArrayOption('skip'), + $typedInput->getStringOption('fpm-socket') + ); + + $this->outputResults( + $output, + $runtimeStatus, + $typedInput->getBoolOption('json') + ); + + $failOnError = $typedInput->getBoolOption('fail-on-error'); + return $runtimeStatus->isSuccess() || !$failOnError + ? Command::SUCCESS + : Command::FAILURE; + } + + /** + * @throws JsonException + */ + private function outputResults( + OutputInterface $output, + RuntimeStatus $runtimeStatus, + bool $json + ): void { + if ($json) { + $output->writeln( + json_encode( + $runtimeStatus->serialize(), + JSON_THROW_ON_ERROR + | JSON_PRETTY_PRINT + | JSON_UNESCAPED_SLASHES + ) + ); + } else { + foreach (RuntimeType::cases() as $type) { + $status = $runtimeStatus->getStatus($type); + if ($status === null) { + continue; + } + foreach ($status->getReports() as $scope => $value) { + $value = json_encode( + $value, + JSON_THROW_ON_ERROR + | JSON_PRETTY_PRINT + | JSON_UNESCAPED_SLASHES + ); + $output->writeln( + '' + . $type->value + . '/' + . $scope + . '' + ); + $output->writeln($value); + $output->writeln(''); + } + } + + if ($runtimeStatus->isSuccess()) { + $output->writeln('Success'); + } else { + $output->writeln('Failure'); + foreach ($runtimeStatus->getMessages() as $scope => $message) { + $output->writeln('' . $message . ''); + } + } + } } } diff --git a/src/Console/Command/Io/TypifiedInput.php b/src/Console/Command/Io/TypifiedInput.php new file mode 100644 index 0000000..1a3a804 --- /dev/null +++ b/src/Console/Command/Io/TypifiedInput.php @@ -0,0 +1,57 @@ +input->getOption($name); + if ($value === null) { + return null; + } + if (!is_string($value)) { + throw new InvalidArgumentException( + 'option ' . $name . ' must be a string: ' . $value + ); + } + return $value; + } + + /** + * @return array + */ + public function getArrayOption(string $name): array + { + $value = $this->input->getOption($name); + if ($value === null) { + return []; + } + if (!is_array($value)) { + throw new InvalidArgumentException( + 'option ' . $name . ' must be a array: ' . $value + ); + } + return $value; + } + + + public function getBoolOption(string $name): bool + { + $value = $this->input->getOption($name); + return filter_var($value, FILTER_VALIDATE_BOOLEAN); + } +} diff --git a/src/Controller/CheckController.php b/src/Controller/CheckController.php index 326b81e..9772fe5 100644 --- a/src/Controller/CheckController.php +++ b/src/Controller/CheckController.php @@ -4,19 +4,50 @@ namespace Atoolo\Runtime\Check\Controller; +use Atoolo\Runtime\Check\Service\FpmFcgi\RuntimeCheck; +use JsonException; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\ExpressionLanguage\Expression; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; +use Symfony\Component\Security\Http\Attribute\IsGranted; final class CheckController extends AbstractController { + public function __construct( + private readonly RuntimeCheck $runtimeCheck, + ) { + } + /** + * @throws JsonException */ - #[Route('/api/admin/runtime-check', name: 'atoolo_runtime_check')] - public function deploy(Request $request): Response + #[Route('/api/runtime-check', name: 'atoolo_runtime_check')] + #[IsGranted( + attribute: new Expression( + 'is_granted("ROLE_SYSTEM_AUDITOR") ' + . 'or "127.0.0.1" == subject.getClientIp()' + ), + subject: new Expression('request') + )] + public function check(Request $request): Response { - return new JsonResponse(['success' => true]); + $skip = $request->get('skip') ?? []; + if (is_string($skip)) { + $skip = explode(',', $skip); + } elseif (!is_array($skip)) { + $skip = []; + } + + $runtimeStatus = $this->runtimeCheck->execute($skip); + + $result = $runtimeStatus->serialize(); + $success = $runtimeStatus->isSuccess(); + + $res = new JsonResponse($result); + $res->setStatusCode($success ? 200 : 500); + return $res; } } diff --git a/src/Service/CheckStatus.php b/src/Service/CheckStatus.php new file mode 100644 index 0000000..6f30a34 --- /dev/null +++ b/src/Service/CheckStatus.php @@ -0,0 +1,152 @@ +> + */ + private array $messages = []; + + /** + * @var array> + */ + private array $reports = []; + + public function __construct(public bool $success) + { + } + + public static function createSuccess(): self + { + return new self(true); + } + + public static function createFailure(): self + { + return new self(false); + } + + public function addMessage(string $scope, string $message): self + { + return $this->addMessages($scope, [$message]); + } + + /** + * @param array $messages + */ + public function addMessages(string $scope, array $messages): self + { + $this->messages[$scope] = array_merge( + $this->messages[$scope] ?? [], + $messages + ); + return $this; + } + + /** + * @param array $result + */ + public function addReport(string $scope, array $result): self + { + if (isset($this->reports[$scope])) { + throw new InvalidArgumentException("Scope $scope already exists"); + } + $this->reports[$scope] = $result; + return $this; + } + + /** + * @param array $result + */ + public function replaceReport(string $scope, array $result): self + { + $this->reports[$scope] = $result; + return $this; + } + + /** + * @return array + */ + public function getReport(string $scope): array + { + return $this->reports[$scope] ?? []; + } + + /** + * @return array> + */ + public function getReports(): array + { + return $this->reports; + } + + /** + * @return array> + */ + public function getMessages(): array + { + return $this->messages; + } + + public function apply(CheckStatus $status): self + { + $this->success = $this->success && $status->success; + $this->messages = array_merge_recursive( + $this->messages, + $status->messages + ); + foreach ($status->reports as $scope => $result) { + $this->addReport($scope, $result); + } + return $this; + } + + /** + * @return CheckStatusData + */ + public function serialize(): array + { + $data = [ + 'success' => $this->success, + 'reports' => $this->reports + ]; + if (!empty($this->messages)) { + $data['messages'] = $this->messages; + } + return $data; + } + + /** + * @param CheckStatusData $data + * @throws JsonException + */ + public static function deserialize(array $data): CheckStatus + { + if (!isset($data['success'])) { + $data = [ + 'success' => false, + 'messages' => [ + 'deserialize' => [ + json_encode( + $data, + JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES + ) + ] + ] + ]; + } + $success = is_bool($data['success']) && $data['success']; + + $status = new self($success); + $status->reports = $data['reports'] ?? []; + $status->messages = $data['messages'] ?? []; + return $status; + } +} diff --git a/src/Service/Checker/Checker.php b/src/Service/Checker/Checker.php new file mode 100644 index 0000000..902cebb --- /dev/null +++ b/src/Service/Checker/Checker.php @@ -0,0 +1,14 @@ + $checkers + */ + public function __construct( + private readonly iterable $checkers + ) { + } + + /** + * @param array $scopesToSkip + */ + public function check(array $scopesToSkip = []): CheckStatus + { + $status = CheckStatus::createSuccess(); + foreach ($this->checkers as $checker) { + if (in_array($checker->getScope(), $scopesToSkip, true)) { + continue; + } + $status->apply($checker->check()); + } + return $status; + } +} diff --git a/src/Service/Checker/MonologChecker.php b/src/Service/Checker/MonologChecker.php new file mode 100644 index 0000000..a9ae9ec --- /dev/null +++ b/src/Service/Checker/MonologChecker.php @@ -0,0 +1,278 @@ +logger instanceof Logger)) { + $status = CheckStatus::createFailure(); + $status->addMessage( + $this->getScope(), + 'unknown: unsupported logger ' . get_class($this->logger) + ); + return $status; + } + + $handlers = $this->getStreamHandlers($this->logger); + if (empty($handlers)) { + $status = CheckStatus::createFailure(); + $status->addMessage( + $this->getScope(), + 'unknown: no stream handler found' + ); + return $status; + } + + $status = CheckStatus::createSuccess(); + foreach ($handlers as $handler) { + $status = $this->checkHandler($status, $handler); + } + return $status; + } + + private function checkHandler( + CheckStatus $mergedStatus, + StreamHandler $handler + ): CheckStatus { + + $reportData = $this->getReportData($handler); + $file = $handler->getUrl(); + + $errors = []; + $logfileError = $this->checkLogfile($file); + if ($logfileError !== null) { + $errors[] = $logfileError; + } + $rotatingErrors = $this->checkLogRotating($reportData); + $errors = array_merge($errors, $rotatingErrors); + + if (!empty($errors)) { + return $this->createFailure( + $mergedStatus, + $reportData, + $errors + ); + } + + return $this->createSuccess( + $mergedStatus, + $reportData + ); + } + + /** + * @return ReportData + */ + private function getReportData(StreamHandler $handler): array + { + $file = $handler->getUrl(); + $reportData = [ + 'logfile' => $file, + 'level' => $handler->getLevel()->getName(), + ]; + + if ($file === null || !file_exists($file) || !is_readable($file)) { + return $reportData; + } + + $dir = dirname($file); + + $fileSize = filesize($file) ?: 0; + $reportData['logfile-size'] = $fileSize; + + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($dir) + ); + + $dirSize = 0; + /** @var SplFileInfo $fileInfo */ + foreach ($iterator as $fileInfo) { + if ($fileInfo->isDir()) { + continue; + } + $dirSize += $fileInfo->getSize() ?: 0; + } + $reportData['logdir-size'] = $dirSize; + + $rotations = + count(glob($file . '.*.gz') ?: []) + + count(glob($file . '.[0-9]') ?: []); + $reportData['logfile-rotations'] = $rotations; + + return $reportData; + } + + private function checkLogfile(?string $file): ?string + { + if ($file === null) { + return'logfile not set'; + } + + if (!file_exists($file)) { + $dir = dirname($file); + if (!is_dir($dir)) { + if (!@mkdir($dir, 0777, true) && !is_dir($dir)) { + return 'log directory cannot be created: ' . $dir; + } + } + + if (!@touch($file)) { + return 'logfile cannot be created: ' . $file; + } + } + + if (!is_writable($file)) { + return 'logfile not writable: ' . $file; + } + + return null; + } + + /** + * @param ReportData $reportData + * @return array + */ + private function checkLogRotating(array $reportData): array + { + $errors = []; + + $maxLogFileSize = $this->memoryStringToInteger($this->maxLogFileSize); + if ( + $maxLogFileSize > 0 + && ($reportData['logfile-size'] ?? 0) > $maxLogFileSize + ) { + $errors[] = 'logfile size exceeds ' + . $this->maxLogFileSize . ' bytes'; + } + + $maxLogDirSize = $this->memoryStringToInteger($this->maxLogDirSize); + if ( + $maxLogDirSize > 0 + && ($reportData['logdir-size'] ?? 0) > $maxLogDirSize + ) { + $errors[] = 'logdir size exceeds ' + . $this->maxLogDirSize . ' bytes'; + } + if ( + $this->maxLogFileRotations > 0 + && ($reportData['logfile-rotations'] ?? 0) + > $this->maxLogFileRotations + ) { + $errors[] = 'logfile rotations exceed ' + . $this->maxLogFileRotations; + } + + return $errors; + } + + /** + * @param ReportData $reportData + * @param array $messages + */ + private function createFailure( + CheckStatus $mergedStatus, + array $reportData, + array $messages + ): CheckStatus { + $status = $mergedStatus->apply(CheckStatus::createFailure()); + $status->addMessages($this->getScope(), $messages); + return $this->applyStatusReport($status, $reportData); + } + + /** + * @param ReportData $reportData + */ + private function createSuccess( + CheckStatus $mergedStatus, + array $reportData, + ): CheckStatus { + $status = $mergedStatus->apply(CheckStatus::createSuccess()); + return $this->applyStatusReport($status, $reportData); + } + + /** + * @param ReportData $reportData + */ + private function applyStatusReport( + CheckStatus $status, + array $reportData + ): CheckStatus { + /** + * @var array{ + * handler: ReportData + * } $report + */ + $report = $status->getReport($this->getScope()); + $report['handler'][] = $reportData; + $status->replaceReport($this->getScope(), $report); + return $status; + } + + /** + * @param Logger $logger + * @return array + */ + private function getStreamHandlers( + Logger $logger + ): array { + $handlers = []; + foreach ($logger->getHandlers() as $handler) { + if ($handler instanceof FingersCrossedHandler) { + $handler = $handler->getHandler(); + } + if ($handler instanceof StreamHandler) { + $handlers[] = $handler; + } + } + return $handlers; + } + + private function memoryStringToInteger(string $memory): int + { + [$number, $suffix] = sscanf($memory, '%u%c') ?? [null, null]; + if (!is_string($suffix)) { + return (int)$memory; + } + + $pos = stripos(' KMG', $suffix); + if (!is_int($pos) || !is_int($number)) { + return 0; + } + return $number * (1024 ** $pos); + } +} diff --git a/src/Service/Checker/PhpStatus.php b/src/Service/Checker/PhpStatus.php new file mode 100644 index 0000000..ce2a424 --- /dev/null +++ b/src/Service/Checker/PhpStatus.php @@ -0,0 +1,195 @@ +, + * fpm: array{ + * configDirs: array, + * status: array, + * }, + * opcache: array + * } + */ +class PhpStatus +{ + /** + * @var Config + */ + private readonly array $config; + + /** + * @throws \JsonException + */ + public function __construct( + string $config = __DIR__ . '/phpStatus.json', + private readonly string $sapi = PHP_SAPI, + private readonly Platform $platform = new Platform() + ) { + $configContent = @file_get_contents($config); + if ($configContent === false) { + throw new RuntimeException( + 'Unable to read config file: ' . $config + ); + } + /** @var Config $data */ + $data = json_decode( + $configContent, + true, + 512, + JSON_THROW_ON_ERROR + ); + $this->config = $data; + } + + public function getScope(): string + { + return 'php'; + } + + public function check(): CheckStatus + { + $report = [ + 'version' => $this->platform->getVersion(), + 'ini' => $this->getIniSettings($this->config['ini'] ?? []) + ]; + + if ($this->sapi === 'fpm-fcgi') { + $fpm = []; + $fpm['config'] = $this->getFpmConfig( + $this->config['fpm']['configDirs'] ?? [] + ); + $fpm['status'] = $this->getFpmPoolStatus( + $this->config['fpm']['status'] ?? [] + ); + $report['fpm'] = $fpm; + $report['opcache'] = $this->getOpcacheStatus( + $this->config['opcache'] ?? [] + ); + } + + $status = CheckStatus::createSuccess(); + $status->addReport($this->getScope(), $report); + return $status; + } + + /** + * @param array $names + * @return array + */ + private function getIniSettings(array $names): array + { + $result = [ + 'file' => $this->platform->getPhpIniLoadedFile() + ]; + foreach ($names as $name) { + $result[$name] = $this->platform->getIni($name); + } + return $result; + } + + /** + * @param array $names + * @return array + */ + private function getFpmPoolStatus(array $names): array + { + $status = $this->platform->getFpmPoolStatus(); + /** @var array $result */ + $result = []; + foreach ($names as $name) { + $result[$name] = $status[$name]; + } + return $result; + } + + /** + * @param array $configDirs + * @return array + */ + private function getFpmConfig(array $configDirs): array + { + $fpmConfigFile = $this->getFpmConfigFile($configDirs); + if ($fpmConfigFile === null) { + return []; + } + + $globalConfig = @parse_ini_file($fpmConfigFile, true); + if ($globalConfig === false) { + throw new RuntimeException( + 'Unable to parse FPM config file: ' . $fpmConfigFile + ); + } + + $configs = []; + if (isset($globalConfig['global']['include'])) { + $include = $globalConfig['global']['include']; + if ($include[0] !== '/') { + $include = dirname($fpmConfigFile) . '/../' . $include; + } + + foreach (glob($include) ?: [] as $file) { + $config = @parse_ini_file($file, true); + if ($config === false) { + throw new RuntimeException( + 'Unable to parse FPM config file: ' . $file + ); + } + $configs[] = $config; + } + } + + return array_merge_recursive($globalConfig, ...$configs); + } + + /** + * @param array $configDirs + */ + private function getFpmConfigFile(array $configDirs): ?string + { + $version = explode('.', $this->platform->getVersion()); + foreach ($configDirs as $configDir) { + $configDir = str_replace( + [ + '{PHP_VERSION_MAJOR}', + '{PHP_VERSION_MINOR}', + '{PHP_VERSION_PATCH}' + ], + [ + $version[0], + $version[1], + $version[2] + ], + $configDir + ); + $iniFilePath = $configDir . '/php-fpm.conf'; + if (file_exists($iniFilePath)) { + return $iniFilePath; + } + } + + return null; + } + + /** + * @param array $names + * @return array + */ + private function getOpcacheStatus(array $names): array + { + $status = $this->platform->getOpcacheGetStatus(); + /** @var array $result */ + $result = []; + foreach ($names as $name) { + $result[$name] = $status[$name]; + } + return $result; + } +} diff --git a/src/Service/Checker/ProcessStatus.php b/src/Service/Checker/ProcessStatus.php new file mode 100644 index 0000000..6a5bfa2 --- /dev/null +++ b/src/Service/Checker/ProcessStatus.php @@ -0,0 +1,38 @@ +addReport($this->getScope(), [ + 'script' => $_SERVER['SCRIPT_FILENAME'] ?? 'n/a', + 'user' => $this->platform->getUser(), + 'group' => $this->platform->getGroup(), + ]); + + return $status; + } +} diff --git a/src/Service/Checker/phpStatus.json b/src/Service/Checker/phpStatus.json new file mode 100644 index 0000000..815baaf --- /dev/null +++ b/src/Service/Checker/phpStatus.json @@ -0,0 +1,30 @@ +{ + "ini" : [ + "date.timezone", + "file_uploads", + "post_max_size", + "upload_max_filesize", + "session.cache_expire", + "opcache.enable", + "opcache.revalidate_path", + "memory_limit" + ], + "fpm" : { + "configDirs" : [ + "/usr/local/etc", + "/etc/php/{PHP_VERSION_MAJOR}.{PHP_VERSION_MINOR}/fpm" + ], + "status" : [ + "pool", + "process-manager", + "idle-processes", + "active-processes", + "total-processes" + ] + }, + "opcache" : [ + "cache_full", + "memory_usage", + "opcache_statistics" + ] +} \ No newline at end of file diff --git a/src/Service/Cli/FastCgiStatus.php b/src/Service/Cli/FastCgiStatus.php new file mode 100644 index 0000000..dad25fa --- /dev/null +++ b/src/Service/Cli/FastCgiStatus.php @@ -0,0 +1,115 @@ +socket; + } + + /** + * @internal For testing purposes only + * @codeCoverageIgnore + */ + public function getConnection(): ConfiguresSocketConnection + { + return $this->connection; + } + + /** + * @param array $skip + */ + public function request(array $skip): CheckStatus + { + foreach (RuntimeType::otherCases(RuntimeType::FPM_FCGI) as $type) { + $skip[] = $type->value; + } + $content = http_build_query(['skip' => $skip]); + + $request = new PostRequest($this->frontControllerPath, $content); + $request->setRequestUri('/api/runtime-check'); + $request->setRemoteAddress('127.0.0.1'); + $request->setServerName($this->resourceHost); + $request->setCustomVar('RESOURCE_ROOT', $this->resourceRoot); + $request->setCustomVar( + 'DOCUMENT_ROOT', + dirname($this->frontControllerPath) + ); + + try { + $res = $this->client->sendRequest( + $this->connection, + $request + ); + + $body = $res->getBody(); + try { + /** @var RuntimeStatusData $data */ + $data = json_decode( + $body, + true, + 512, + JSON_THROW_ON_ERROR + ); + } catch (JsonException $e) { + return CheckStatus::createFailure() + ->addMessage( + 'fpm-fcgi', + sprintf( + "JSON error: %s\n%s", + $e->getMessage(), + $body + ) + ); + } + + $status = RuntimeStatus::deserialize($data) + ->getStatus(RuntimeType::FPM_FCGI); + return $status ?? CheckStatus::createFailure() + ->addMessage( + 'fpm-fcgi', + 'No FastCGI status found in response.' + ); + } catch (Throwable $e) { + return CheckStatus::createFailure() + ->addMessage( + 'fpm-fcgi', + sprintf( + 'FastCGI error: %s (%s)', + $e->getMessage(), + $this->socket + ) + ); + } + } +} diff --git a/src/Service/Cli/FastCgiStatusFactory.php b/src/Service/Cli/FastCgiStatusFactory.php new file mode 100644 index 0000000..3ff8294 --- /dev/null +++ b/src/Service/Cli/FastCgiStatusFactory.php @@ -0,0 +1,101 @@ + $possibleSocketFilePatterns + * of patterns matching php socket files + */ + public function __construct( + private readonly array $possibleSocketFilePatterns, + private readonly string $frontControllerPath, + ?string $resourceRoot, + ?string $resourceHost + ) { + if (empty($resourceRoot)) { + throw new RuntimeException( + <<resourceRoot = $resourceRoot; + $this->resourceHost = $resourceHost; + } + + public function create(?string $socket = null): FastCgiStatus + { + $socket = $socket ?? $this->determineSocket(); + $client = new Client(); + $connection = $this->createConnection($socket); + return new FastCgiStatus( + $socket, + $client, + $connection, + $this->frontControllerPath, + $this->resourceRoot, + $this->resourceHost + ); + } + + private function determineSocket(): string + { + foreach ( + $this->possibleSocketFilePatterns as $possibleSocketFilePattern + ) { + $files = glob($possibleSocketFilePattern) ?: []; + $possibleSocketFile = current($files); + if ( + $possibleSocketFile !== false + && file_exists($possibleSocketFile) + ) { + return $possibleSocketFile; + } + } + return '127.0.0.1:9000'; + } + + private function createConnection( + string $socket + ): ConfiguresSocketConnection { + $last = strrpos($socket, ':'); + if ($last !== false) { + $port = substr($socket, $last + 1, strlen($socket)); + $host = substr($socket, 0, $last); + + return new NetworkSocket( + $host, + (int)$port, + 5000, # Connect timeout in milliseconds (default: 5000) + 120000 # Read/write timeout in milliseconds (default: 5000) + ); + } + + return new UnixDomainSocket( + $socket, + 5000, # Connect timeout in milliseconds (default: 5000) + 120000 # Read/write timeout in milliseconds (default: 5000) + ); + } +} diff --git a/src/Service/Cli/RuntimeCheck.php b/src/Service/Cli/RuntimeCheck.php new file mode 100644 index 0000000..9eb1ed9 --- /dev/null +++ b/src/Service/Cli/RuntimeCheck.php @@ -0,0 +1,76 @@ + $skip + */ + public function execute(array $skip, ?string $fpmSocket): RuntimeStatus + { + $runtimeStatus = new RuntimeStatus(); + if (!in_array(RuntimeType::CLI->value, $skip, true)) { + $runtimeStatus->addStatus( + RuntimeType::CLI, + $this->getCliStatus($skip) + ); + } + if (!in_array(RuntimeType::FPM_FCGI->value, $skip, true)) { + $runtimeStatus->addStatus( + RuntimeType::FPM_FCGI, + $this->getFpmFcgiStatus($skip, $fpmSocket) + ); + } + if (!in_array(RuntimeType::WORKER->value, $skip, true)) { + $runtimeStatus->addStatus( + RuntimeType::WORKER, + $this->getWorkerStatus($skip) + ); + } + + return $runtimeStatus; + } + + /** + * @param array $skip + */ + private function getCliStatus(array $skip): CheckStatus + { + return $this->checkerCollection->check($skip); + } + + /** + * @param array $skip + */ + private function getFpmFcgiStatus( + array $skip, + ?string $fpmSocket + ): CheckStatus { + $fastCgi = $this->fastCgiStatusFactory->create($fpmSocket); + return $fastCgi->request($skip); + } + + /** + * @param array $skip + */ + private function getWorkerStatus(array $skip): CheckStatus + { + return $this->workerStatusFile->read(); + } +} diff --git a/src/Service/FpmFcgi/CliStatus.php b/src/Service/FpmFcgi/CliStatus.php new file mode 100644 index 0000000..ab2c47f --- /dev/null +++ b/src/Service/FpmFcgi/CliStatus.php @@ -0,0 +1,70 @@ + $skip + * @throws JsonException + */ + public function execute(array $skip): CheckStatus + { + foreach (RuntimeType::otherCases(RuntimeType::CLI) as $type) { + $skip[] = $type->value; + } + + $process = new Process([ + $this->consoleBinPath, + 'runtime:check', + '--fail-on-error', 'false', + '--skip', implode(',', $skip), + '--json' + ]); + $process->setEnv(['RESOURCE_ROOT' => $this->resourceRoot]); + try { + $process->run(); + // @codeCoverageIgnoreStart + // found no way to test this + } catch (Exception $e) { + return CheckStatus::createFailure() + ->addMessage('cli', trim($e->getMessage())); + } + // @codeCoverageIgnoreEnd + + if (!$process->isSuccessful()) { + return CheckStatus::createFailure() + ->addMessage('cli', trim($process->getErrorOutput())); + } + + /** @var RuntimeStatusData $data */ + $data = json_decode( + $process->getOutput(), + true, + 512, + JSON_THROW_ON_ERROR + ); + $status = RuntimeStatus::deserialize($data) + ->getStatus(RuntimeType::CLI); + if ($status === null) { + return CheckStatus::createFailure() + ->addMessage('cli', 'No CLI status found in response.'); + } + return $status; + } +} diff --git a/src/Service/FpmFcgi/RuntimeCheck.php b/src/Service/FpmFcgi/RuntimeCheck.php new file mode 100644 index 0000000..fc08d18 --- /dev/null +++ b/src/Service/FpmFcgi/RuntimeCheck.php @@ -0,0 +1,82 @@ + $skip + * @throws JsonException + */ + public function execute(array $skip): RuntimeStatus + { + $runtimeStatus = new RuntimeStatus(); + if (!in_array(RuntimeType::FPM_FCGI->value, $skip, true)) { + $runtimeStatus->addStatus( + RuntimeType::FPM_FCGI, + $this->getFpmFcgiStatus($skip) + ); + } + if (!in_array(RuntimeType::CLI->value, $skip, true)) { + $runtimeStatus->addStatus( + RuntimeType::CLI, + $this->getCliStatus($skip) + ); + } + if (!in_array(RuntimeType::WORKER->value, $skip, true)) { + $runtimeStatus->addStatus( + RuntimeType::WORKER, + $this->getWorkerStatus($skip) + ); + } + + return $runtimeStatus; + } + + /** + * @param array $skip + */ + private function getFpmFcgiStatus( + array $skip + ): CheckStatus { + $status = $this->checkerCollection->check($skip); + $status->addReport('server', [ + 'host' => $_SERVER['SERVER_NAME'] ?? 'unknown' + ]); + return $status; + } + + /** + * @param array $skip + * @throws JsonException + */ + private function getCliStatus(array $skip): CheckStatus + { + return $this->cliStatus->execute($skip); + } + + /** + * @param array $skip + * @throws JsonException + */ + private function getWorkerStatus(array $skip): CheckStatus + { + return $this->workerStatusFile->read(); + } +} diff --git a/src/Service/Platform.php b/src/Service/Platform.php new file mode 100644 index 0000000..0fef505 --- /dev/null +++ b/src/Service/Platform.php @@ -0,0 +1,61 @@ + (string)posix_geteuid()])['name']; + } + + public function getGroup(): string + { + return (posix_getgrgid(posix_getegid()) + ?: ['name' => (string)posix_getegid()])['name']; + } + + /** + * @return array + */ + public function getFpmPoolStatus(): array + { + return fpm_get_status() ?: []; + } + + public function getVersion(): string + { + return PHP_VERSION; + } + + public function getPhpIniLoadedFile(): false|string + { + return php_ini_loaded_file(); + } + + public function getIni(string $name): string|false + { + return ini_get($name); + } + + /** + * @return array + */ + public function getOpcacheGetStatus(): array + { + return opcache_get_status() ?: []; + } +} diff --git a/src/Service/RuntimeStatus.php b/src/Service/RuntimeStatus.php new file mode 100644 index 0000000..75cee03 --- /dev/null +++ b/src/Service/RuntimeStatus.php @@ -0,0 +1,97 @@ + + */ + private array $status = []; + + public function addStatus(RuntimeType $type, CheckStatus $status): void + { + $this->status[$type->value] = $status; + } + + public function getStatus(RuntimeType $type): ?CheckStatus + { + return $this->status[$type->value] ?? null; + } + + /** + * @return array + */ + public function getTypes(): array + { + return array_map( + function (string $type) { + return RuntimeType::from($type); + }, + array_keys($this->status) + ); + } + + public function isSuccess(): bool + { + foreach ($this->status as $status) { + if (!$status->success) { + return false; + } + } + return true; + } + + /** + * @return array + */ + public function getMessages(): array + { + $messages = []; + foreach ($this->status as $type => $status) { + foreach ($status->getMessages() as $scope => $scopeMessages) { + foreach ($scopeMessages as $message) { + $prefix = $type === $scope ? $type : $type . '/' . $scope; + $messages[] = sprintf('%s: %s', $prefix, $message); + } + } + } + return $messages; + } + + /** + * @return RuntimeStatusData $data + */ + public function serialize(): array + { + $data = []; + foreach ($this->status as $type => $status) { + $data[$type] = $status->serialize(); + } + $data['success'] = $this->isSuccess(); + $messages = $this->getMessages(); + if (!empty($messages)) { + $data['messages'] = $messages; + } + /** @var RuntimeStatusData $data */ + return $data; + } + + /** + * @param RuntimeStatusData $data + */ + public static function deserialize(array $data): RuntimeStatus + { + $runtimeStatus = new RuntimeStatus(); + foreach (RuntimeType::cases() as $type) { + if (!array_key_exists($type->value, $data)) { + continue; + } + $status = CheckStatus::deserialize($data[$type->value]); + $runtimeStatus->addStatus($type, $status); + } + return $runtimeStatus; + } +} diff --git a/src/Service/RuntimeType.php b/src/Service/RuntimeType.php new file mode 100644 index 0000000..7eb142d --- /dev/null +++ b/src/Service/RuntimeType.php @@ -0,0 +1,26 @@ + + */ + public static function otherCases(RuntimeType $type): array + { + $cases = []; + foreach (self::cases() as $other) { + if ($type !== $other) { + $cases[] = $other; + } + } + return $cases; + } +} diff --git a/src/Service/Worker/OneTimeTrigger.php b/src/Service/Worker/OneTimeTrigger.php new file mode 100644 index 0000000..817ef18 --- /dev/null +++ b/src/Service/Worker/OneTimeTrigger.php @@ -0,0 +1,36 @@ +run === null) { + return 'one time'; + } + return 'one time (already running)'; + } + + /** + * @inheritDoc + */ + public function getNextRunDate(\DateTimeImmutable $run): ?\DateTimeImmutable + { + if (null === $this->run) { + $this->run = $run; + return $run; + } + return null; + } +} diff --git a/src/Service/Worker/WorkerCheckEvent.php b/src/Service/Worker/WorkerCheckEvent.php new file mode 100644 index 0000000..0b098a9 --- /dev/null +++ b/src/Service/Worker/WorkerCheckEvent.php @@ -0,0 +1,12 @@ +schedule ??= (new Schedule()) + ->add( + RecurringMessage::trigger( + new OneTimeTrigger(), + new WorkerCheckEvent() + ), + RecurringMessage::every( + $this->workerStatusFile->updatePeriodInMinutes . ' minutes', + new WorkerCheckEvent() + ), + )->lock($this->lockFactory->createLock( + 'runtime-check-scheduler-' . $this->host + )); + } + + /** + * @throws JsonException + */ + public function __invoke(WorkerCheckEvent $message): void + { + $status = $this->checkerCollection->check([]); + $this->workerStatusFile->write($status); + } +} diff --git a/src/Service/Worker/WorkerStatusFile.php b/src/Service/Worker/WorkerStatusFile.php new file mode 100644 index 0000000..a6b0c24 --- /dev/null +++ b/src/Service/Worker/WorkerStatusFile.php @@ -0,0 +1,107 @@ +workerStatusFile)) { + return CheckStatus::createFailure() + ->addMessage('worker', 'worker not running'); + } + + $toleranceInMinutes = $this->updatePeriodInMinutes / 2; + + $workerStatusFileContent = @file_get_contents($this->workerStatusFile); + if ($workerStatusFileContent === false) { + throw new RuntimeException( + 'Unable to read file ' . $this->workerStatusFile + ); + } + + /** @var CheckStatusData $result */ + $result = json_decode( + $workerStatusFileContent, + true, + 512, + JSON_THROW_ON_ERROR + ); + + $allowedTime = + $this->platform->time() - + (($this->updatePeriodInMinutes + $toleranceInMinutes) * 60); + $formattedLastRun = $result['reports']['scheduler']['last-run'] + ?? 'unknown'; + $lastRun = is_string($formattedLastRun) + ? strtotime($formattedLastRun) + : 0; + + $checkStatus = CheckStatus::deserialize($result); + if ($lastRun < $allowedTime) { + $checkStatus->success = false; + if (isset($result['reports']['scheduler'])) { + $checkStatus + ->replaceReport( + 'scheduler', + $result['reports']['scheduler'] + ); + } + $checkStatus + ->addMessage( + 'scheduler', + 'The worker did not run in the last ' + . $this->updatePeriodInMinutes + . ' minutes. Last run: ' + . $formattedLastRun + ); + } + + return $checkStatus; + } + + /** + * @throws JsonException + */ + public function write(CheckStatus $status): void + { + $now = new DateTime(); + $now->setTimestamp($this->platform->time()); // testable + if (isset($_SERVER['SUPERVISOR_ENABLED'])) { + $status->addReport('supervisor', [ + 'group' => $_SERVER['SUPERVISOR_GROUP_NAME'] ?? '', + 'process' => $_SERVER['SUPERVISOR_PROCESS_NAME'] ?? '', + ]); + } + $status->addReport('scheduler', [ + 'last-run' => $now->format('d.m.Y H:i:s') + ]); + + file_put_contents( + $this->workerStatusFile, + json_encode( + $status->serialize(), + JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR + ) + ); + } +} diff --git a/test/Console/Command/CheckCommandTest.php b/test/Console/Command/CheckCommandTest.php index f2b0d18..c48ca76 100644 --- a/test/Console/Command/CheckCommandTest.php +++ b/test/Console/Command/CheckCommandTest.php @@ -5,8 +5,15 @@ namespace Atoolo\Runtime\Check\Test\Console\Command; use Atoolo\Runtime\Check\Console\Command\CheckCommand; +use Atoolo\Runtime\Check\Service\Checker\ProcessStatus; +use Atoolo\Runtime\Check\Service\CheckStatus; +use Atoolo\Runtime\Check\Service\Cli\FastCGIStatus; +use Atoolo\Runtime\Check\Service\Cli\FastCgiStatusFactory; +use Atoolo\Runtime\Check\Service\Cli\RuntimeCheck; +use Atoolo\Runtime\Check\Service\RuntimeStatus; +use Atoolo\Runtime\Check\Service\RuntimeType; +use Atoolo\Runtime\Check\Service\Worker\WorkerStatusFile; use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Tester\CommandTester; @@ -15,15 +22,101 @@ class CheckCommandTest extends TestCase { private CommandTester $commandTester; + private RuntimeCheck $untimeCheck; + public function setUp(): void { - $command = new CheckCommand(); + $this->runtimeCheck = $this->createStub( + RuntimeCheck::class + ); + + $command = new CheckCommand( + $this->runtimeCheck, + ); $this->commandTester = new CommandTester($command); } - public function testExecute(): void + public function testExecuteSuccess(): void + { + $checkStatus = CheckStatus::createSuccess(); + $checkStatus->addReport('test', ['a' => 'b']); + $runtimeStatus = new RuntimeStatus(); + $runtimeStatus->addStatus(RuntimeType::CLI, $checkStatus); + $this->runtimeCheck->method('execute')->willReturn( + $runtimeStatus + ); + + $this->commandTester->execute([]); + $this->assertEquals( + <<commandTester->getDisplay(), + 'Command should display failure message' + ); + } + + public function testExecuteFailure(): void { + $status = CheckStatus::createFailure(); + $status->addMessage('test', 'test message'); + + $runtimeStatus = new RuntimeStatus(); + $runtimeStatus->addStatus(RuntimeType::CLI, $status); + + $this->runtimeCheck->method('execute')->willReturn( + $runtimeStatus + ); + $this->commandTester->execute([]); + $this->assertEquals( + <<commandTester->getDisplay(), + 'Command should display failure message' + ); + } + + public function testJsonResult(): void + { + $checkStatus = CheckStatus::createSuccess(); + $checkStatus->addReport('test', ['a' => 'b']); + $runtimeStatus = new RuntimeStatus(); + $runtimeStatus->addStatus(RuntimeType::CLI, $checkStatus); + $this->runtimeCheck->method('execute')->willReturn( + $runtimeStatus + ); + + $this->commandTester->execute( + ['--json' => true] + ); + $this->assertEquals( + <<commandTester->getDisplay(), + 'Command should display json result' + ); } } diff --git a/test/Console/Command/Io/TypifiedInputTest.php b/test/Console/Command/Io/TypifiedInputTest.php new file mode 100644 index 0000000..4fcce07 --- /dev/null +++ b/test/Console/Command/Io/TypifiedInputTest.php @@ -0,0 +1,160 @@ +createStub(InputInterface::class); + $symfonyInput + ->method('getOption') + ->willReturn('abc'); + + $input = new TypifiedInput($symfonyInput); + + $this->assertEquals( + 'abc', + $input->getStringOption('a'), + 'unexpected option value' + ); + } + + /** + * @throws Exception + */ + public function testGetStringMissingOption(): void + { + $symfonyInput = $this->createStub(InputInterface::class); + $symfonyInput + ->method('getOption') + ->willReturn(null); + + $input = new TypifiedInput($symfonyInput); + + $this->assertNull( + $input->getStringOption('a'), + 'unexpected option value' + ); + } + + /** + * @throws Exception + */ + public function testGetStringOptWithInvalidValue(): void + { + $symfonyInput = $this->createStub(InputInterface::class); + $symfonyInput + ->method('getOption') + ->willReturn(123); + + $input = new TypifiedInput($symfonyInput); + + $this->expectException(InvalidArgumentException::class); + $input->getStringOption('a'); + } + + /** + * @throws Exception + */ + public function testGetBoolOption(): void + { + $symfonyInput = $this->createStub(InputInterface::class); + $symfonyInput + ->method('getOption') + ->willReturn(true); + + $input = new TypifiedInput($symfonyInput); + + $this->assertEquals( + true, + $input->getBoolOption('a'), + 'unexpected option value' + ); + } + + /** + * @throws Exception + */ + public function testGetBoolOptWithInvalidValue(): void + { + $symfonyInput = $this->createStub(InputInterface::class); + $symfonyInput + ->method('getOption') + ->willReturn('abc'); + + $input = new TypifiedInput($symfonyInput); + + $this->assertEquals( + false, + $input->getBoolOption('a'), + 'unexpected option value' + ); + } + + /** + * @throws Exception + */ + public function testGetArrayOption(): void + { + $symfonyInput = $this->createStub(InputInterface::class); + $symfonyInput + ->method('getOption') + ->willReturn(['abc']); + + $input = new TypifiedInput($symfonyInput); + + $this->assertEquals( + ['abc'], + $input->getArrayOption('a'), + 'unexpected option value' + ); + } + + /** + * @throws Exception + */ + public function testGetArrayOptionMissingValue(): void + { + $symfonyInput = $this->createStub(InputInterface::class); + $symfonyInput + ->method('getOption') + ->willReturn(null); + + $input = new TypifiedInput($symfonyInput); + + $this->assertEquals( + [], + $input->getArrayOption('a'), + 'unexpected option value' + ); + } + + /** + * @throws Exception + */ + public function testGetArrayOptionInvaludValue(): void + { + $symfonyInput = $this->createStub(InputInterface::class); + $symfonyInput + ->method('getOption') + ->willReturn('abc'); + + $input = new TypifiedInput($symfonyInput); + $this->expectException(InvalidArgumentException::class); + $input->getArrayOption('a'); + } +} diff --git a/test/Controller/CheckControllerTest.php b/test/Controller/CheckControllerTest.php index 5b0f806..186ff33 100644 --- a/test/Controller/CheckControllerTest.php +++ b/test/Controller/CheckControllerTest.php @@ -5,7 +5,15 @@ namespace Atoolo\Runtime\Check\Test\Controller; use Atoolo\Runtime\Check\Controller\CheckController; +use Atoolo\Runtime\Check\Service\Checker\ProcessStatus; +use Atoolo\Runtime\Check\Service\CheckStatus; +use Atoolo\Runtime\Check\Service\FpmFcgi\CliStatus; +use Atoolo\Runtime\Check\Service\FpmFcgi\RuntimeCheck; +use Atoolo\Runtime\Check\Service\RuntimeStatus; +use Atoolo\Runtime\Check\Service\RuntimeType; +use JsonException; use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\MockObject\Exception; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; @@ -13,16 +21,118 @@ #[CoversClass(CheckController::class)] class CheckControllerTest extends TestCase { - private CheckController $controller; + private RuntimeCheck&MockObject $runtimeCheck; + /** + * @throws Exception + */ public function setUp(): void { - $this->controller = new CheckController(); + $this->runtimeCheck = $this->createMock(RuntimeCheck::class); + $this->controller = new CheckController($this->runtimeCheck); } public function testCheck(): void { + $checkStatus = CheckStatus::createSuccess(); + $checkStatus->addReport('test', [ + 'a' => 'b' + ]); + $runtimeStatus = new RuntimeStatus(); + $runtimeStatus->addStatus(RuntimeType::CLI, $checkStatus); + + $this->runtimeCheck->method('execute')->willReturn( + $runtimeStatus + ); + $request = $this->createMock(Request::class); - $this->controller->deploy($request); + $response = $this->controller->check($request); + $json = json_decode( + $response->getContent(), + true, + 512, + JSON_THROW_ON_ERROR + ); + $this->assertEquals( + [ + 'success' => true, + 'cli' => [ + 'reports' => [ + 'test' => [ + 'a' => 'b' + ] + ], + 'success' => true + ] + ], + $json, + 'Unexpected response content' + ); + } + + /** + * @throws Exception + * @throws JsonException + */ + public function testCheckWithSkipArray(): void + { + $this->runtimeCheck->expects($this->once()) + ->method('execute') + ->with( + [ + RuntimeType::CLI->value, + ] + ); + $request = $this->createMock(Request::class); + $request->method('get') + ->with('skip') + ->willReturn( + [ + RuntimeType::CLI->value + ] + ); + + $this->controller->check($request); + } + + /** + * @throws Exception + * @throws JsonException + */ + public function testCheckWithSkipString(): void + { + $this->runtimeCheck->expects($this->once()) + ->method('execute') + ->with([ + RuntimeType::CLI->value, + RuntimeType::WORKER->value + ]); + $request = $this->createMock(Request::class); + $request->method('get') + ->with('skip') + ->willReturn( + RuntimeType::CLI->value . ',' . RuntimeType::WORKER->value + ); + + $this->controller->check($request); + } + + /** + * @throws Exception + * @throws JsonException + */ + public function testCheckWithUnsupportedSkipType(): void + { + $this->runtimeCheck->expects($this->once()) + ->method('execute') + ->with([]); + $request = $this->createMock(Request::class); + $request->method('get') + ->with('skip') + ->willReturn( + true + ); + + $this->controller->check($request); } } diff --git a/test/Service/CheckStatusTest.php b/test/Service/CheckStatusTest.php new file mode 100644 index 0000000..699d65f --- /dev/null +++ b/test/Service/CheckStatusTest.php @@ -0,0 +1,183 @@ +success, 'Status is not successful'); + } + + public function testCreateFailure(): void + { + $status = CheckStatus::createFailure(); + self::assertFalse($status->success, 'Status is not a failure'); + } + + public function testAddMessage(): void + { + $status = new CheckStatus(true); + $status->addMessage('scope', 'message'); + self::assertSame( + [ + 'scope' => ['message'] + ], + $status->getMessages(), + 'Message was not added correctly' + ); + } + + public function testAddReport(): void + { + $status = new CheckStatus(true); + $status->addReport('scope', ['result']); + self::assertSame( + [ + 'scope' => ['result'] + ], + $status->getReports(), + 'Result was not added correctly' + ); + } + + public function testReplaceReport(): void + { + $status = new CheckStatus(true); + $status->addReport('scope', ['result']); + $status->replaceReport('scope', ['result2']); + self::assertSame( + [ + 'scope' => ['result2'] + ], + $status->getReports(), + 'Result was not replaced' + ); + } + + public function testGetReport(): void + { + $status = new CheckStatus(true); + $status->addReport('scope', ['result']); + self::assertSame( + ['result'], + $status->getReport('scope'), + 'unexpected result' + ); + } + + public function testGetMissingReport(): void + { + $status = new CheckStatus(true); + self::assertSame( + [], + $status->getReport('scope'), + 'unexpected result' + ); + } + + public function testAddResultWithScopeAlreadyExists(): void + { + $status = new CheckStatus(true); + $status->addReport('scope', ['result']); + + $this->expectExceptionMessage('Scope scope already exists'); + $status->addReport('scope', ['result']); + } + + public function testApply(): void + { + $status = new CheckStatus(true); + $toApply = CheckStatus::createFailure(); + $toApply->addReport('scope', ['result']); + $status->apply($toApply); + + $expected = new CheckStatus(false); + $expected->addReport('scope', ['result']); + self::assertEquals( + $expected, + $status, + 'Status was not applied correctly' + ); + } + + public function testSerialize(): void + { + $status = new CheckStatus(true); + $status->addMessage('scope', 'message'); + $status->addReport('scope', ['a' => 'b']); + $serialized = $status->serialize(); + self::assertSame( + [ + 'success' => true, + 'reports' => [ + 'scope' => [ + 'a' => 'b' + ], + ], + 'messages' => [ + 'scope' => ['message'] + ], + ], + $serialized, + 'Status was not serialized correctly' + ); + } + + public function testDeserialize(): void + { + $serialized = [ + 'success' => true, + 'reports' => [ + 'scope' => [ + 'a' => 'b' + ], + ], + 'messages' => [ + 'scope' => ['message'] + ], + ]; + $status = CheckStatus::deserialize($serialized); + $expected = new CheckStatus(true); + $expected->addMessage('scope', 'message'); + $expected->addReport('scope', ['a' => 'b']); + + self::assertEquals( + $expected, + $status, + 'Status was not deserialized correctly' + ); + } + + /** + * @throws \JsonException + */ + public function testWithUnknownJson(): void + { + $serialized = [ + 'code' => 401, + 'message' => "JWT Token not found" + ]; + $status = CheckStatus::deserialize($serialized); + + $expected = new CheckStatus(false); + $expected->addMessage( + 'deserialize', + json_encode($serialized, JSON_THROW_ON_ERROR) + ); + + self::assertEquals( + $expected, + $status, + 'Status was not deserialized correctly' + ); + } +} diff --git a/test/Service/Checker/CheckerCollectionTest.php b/test/Service/Checker/CheckerCollectionTest.php new file mode 100644 index 0000000..dc98218 --- /dev/null +++ b/test/Service/Checker/CheckerCollectionTest.php @@ -0,0 +1,56 @@ +createStub(Checker::class); + $checkerA->method('check')->willReturn( + CheckStatus::createSuccess()->addReport('a', ['a' => 'b']) + ); + $checkerA->method('getScope')->willReturn('a'); + + $checkerCollection = new CheckerCollection([$checkerA]); + $status = $checkerCollection->check([]); + + $expected = CheckStatus::createSuccess() + ->addReport('a', ['a' => 'b']); + + $this->assertEquals( + $expected, + $status, + 'Status is not as expected' + ); + } + + public function testSkipCheck(): void + { + $checker = $this->createStub(Checker::class); + $checker->method('check')->willReturn( + CheckStatus::createSuccess()->addReport('a', ['a' => 'b']) + ); + $checker->method('getScope')->willReturn('a'); + + $checkerCollection = new CheckerCollection([$checker]); + $status = $checkerCollection->check(['a']); + + $expected = CheckStatus::createSuccess(); + + $this->assertEquals( + $expected, + $status, + 'checker should be skipped' + ); + } +} diff --git a/test/Service/Checker/MonologCheckerTest.php b/test/Service/Checker/MonologCheckerTest.php new file mode 100644 index 0000000..01a1aa8 --- /dev/null +++ b/test/Service/Checker/MonologCheckerTest.php @@ -0,0 +1,354 @@ +testDir)) { + if (!mkdir($this->testDir, 0777, true)) { + throw new \RuntimeException('Cannot create test directory'); + } + } + } + + /** + * @throws Exception + */ + public function testCheckWithWrongLogger(): void + { + $logger = $this->createStub(LoggerInterface::class); + $checker = new MonologChecker('', '', 0, $logger); + $status = $checker->check([]); + + $expected = CheckStatus::createFailure(); + $expected->addMessage( + 'logging', + 'unknown: unsupported logger ' . get_class($logger) + ); + $this->assertEquals( + $expected, + $status, + 'Status is not as expected' + ); + } + + /** + * @throws Exception + */ + public function testCheckWithoutStreamHandler(): void + { + $logger = $this->createStub(Logger::class); + $checker = new MonologChecker('', '', 0, $logger); + $status = $checker->check([]); + + $expected = CheckStatus::createFailure(); + $expected->addMessage( + 'logging', + 'unknown: no stream handler found' + ); + $this->assertEquals( + $expected, + $status, + 'Status is not as expected' + ); + } + + public function testCheckWithoutLogfile(): void + { + $logger = $this->createStub(Logger::class); + $handler = $this->createStub(StreamHandler::class); + $handler->method('getUrl')->willReturn(null); + $handler->method('getLevel')->willReturn(Level::Warning); + $logger->method('getHandlers')->willReturn([$handler]); + + $checker = new MonologChecker('', '', 0, $logger); + $status = $checker->check([]); + + $expected = CheckStatus::createFailure(); + $expected->addMessage( + 'logging', + 'logfile not set' + ); + $expected = CheckStatus::createFailure(); + $expected->addReport('logging', [ + 'handler' => [ + [ + 'logfile' => null, + 'level' => 'WARNING' + ] + ] + ]); + $expected->addMessage( + 'logging', + 'logfile not set' + ); + $this->assertEquals( + $expected, + $status, + 'Status is not as expected' + ); + } + + /** + * @throws Exception + */ + public function testCheckLogfileNotWritable(): void + { + $filesystem = new Filesystem(); + $file = $this->testDir . '/non-writable.log'; + $filesystem->touch($file); + $filesystem->chmod($file, 0444); + + try { + $handler = $this->createStub(StreamHandler::class); + $handler->method('getUrl')->willReturn($file); + $handler->method('getLevel')->willReturn(Level::Warning); + $logger = $this->createStub(Logger::class); + $logger->method('getHandlers')->willReturn([$handler]); + + $checker = new MonologChecker('', '', 0, $logger); + $status = $checker->check([]); + + $expected = CheckStatus::createFailure(); + $expected->addReport('logging', [ + 'handler' => [ + [ + 'logfile' => $file, + 'level' => 'WARNING', + 'logfile-size' => 0, + 'logdir-size' => 0, + 'logfile-rotations' => 0 + ] + ] + ]); + $expected->addMessage( + 'logging', + 'logfile not writable: ' . $file + ); + $this->assertEquals( + $expected, + $status, + 'Status is not as expected' + ); + } finally { + $filesystem->chmod($file, 0666); + $filesystem->remove($file); + } + } + + public function testCheckDirNotCreateable(): void + { + $filesystem = new Filesystem(); + $dir = $this->testDir . '/not-writable/not-createable/logging.log'; + $file = $dir . '/not-createable/logging.log'; + $filesystem->mkdir($dir); + $filesystem->chmod($dir, 0444); + try { + $handler = $this->createStub(StreamHandler::class); + $handler->method('getUrl')->willReturn($file); + $handler->method('getLevel')->willReturn(Level::Warning); + $logger = $this->createStub(Logger::class); + $logger->method('getHandlers')->willReturn([$handler]); + + $checker = new MonologChecker('', '', 0, $logger); + $status = $checker->check([]); + + $expected = CheckStatus::createFailure(); + $expected->addReport('logging', [ + 'handler' => [ + [ + 'logfile' => $file, + 'level' => 'WARNING' + ] + ] + ]); + $expected->addMessage( + 'logging', + 'log directory cannot be created: ' . dirname($file) + ); + $this->assertEquals( + $expected, + $status, + 'Status is not as expected' + ); + } finally { + $filesystem->chmod($dir, 0777); + $filesystem->remove($dir); + } + } + + public function testCheckFileNotCreateable(): void + { + $filesystem = new Filesystem(); + $dir = $this->testDir . '/not-writable'; + $file = $dir . '/logging.log'; + $filesystem->mkdir($dir); + $filesystem->chmod($dir, 0444); + + try { + $handler = $this->createStub(StreamHandler::class); + $handler->method('getUrl')->willReturn($file); + $handler->method('getLevel')->willReturn(Level::Warning); + $logger = $this->createStub(Logger::class); + $logger->method('getHandlers')->willReturn([$handler]); + + $checker = new MonologChecker('', '', 0, $logger); + $status = $checker->check([]); + + $expected = CheckStatus::createFailure(); + $expected->addReport('logging', [ + 'handler' => [ + [ + 'logfile' => $file, + 'level' => 'WARNING' + ] + ] + ]); + $expected->addMessage( + 'logging', + 'logfile cannot be created: ' . $file + ); + $this->assertEquals( + $expected, + $status, + 'Status is not as expected' + ); + } finally { + $filesystem->chmod($dir, 0777); + $filesystem->remove($dir); + } + } + + public function testCheckSuccessfully(): void + { + $file = $this->resourceDir . '/logging.log'; + $handler = $this->createStub(StreamHandler::class); + $handler->method('getUrl')->willReturn($file); + $handler->method('getLevel')->willReturn(Level::Warning); + $logger = $this->createStub(Logger::class); + $logger->method('getHandlers')->willReturn([$handler]); + + $checker = new MonologChecker('10M', '10X', 0, $logger); + $status = $checker->check([]); + + $expected = CheckStatus::createSuccess(); + $expected->addReport('logging', [ + 'handler' => [ + [ + 'logfile' => $file, + 'level' => 'WARNING', + 'logfile-size' => 18, + 'logdir-size' => 70, + 'logfile-rotations' => 4 + ] + ] + ]); + $this->assertEquals( + $expected, + $status, + 'Status is not as expected' + ); + } + + public function testWithFingersCrossedHandler(): void + { + $file = $this->resourceDir . '/logging.log'; + + + $handler = $this->createStub(StreamHandler::class); + $handler->method('getUrl')->willReturn($file); + $handler->method('getLevel')->willReturn(Level::Warning); + + $fingersCrossedHandler = $this->createStub( + FingersCrossedHandler::class + ); + $fingersCrossedHandler->method('getHandler') + ->willReturn($handler); + + $logger = $this->createStub(Logger::class); + $logger->method('getHandlers')->willReturn([$fingersCrossedHandler]); + + $checker = new MonologChecker('', '', 0, $logger); + $status = $checker->check([]); + + $expected = CheckStatus::createSuccess(); + $expected->addReport('logging', [ + 'handler' => [ + [ + 'logfile' => $file, + 'level' => 'WARNING', + 'logfile-size' => 18, + 'logdir-size' => 70, + 'logfile-rotations' => 4 + ] + ] + ]); + $this->assertEquals( + $expected, + $status, + 'Status is not as expected' + ); + } + + public function testCheckLogRotatingWithErrors(): void + { + $file = $this->resourceDir . '/logging.log'; + $handler = $this->createStub(StreamHandler::class); + $handler->method('getUrl')->willReturn($file); + $handler->method('getLevel')->willReturn(Level::Warning); + $logger = $this->createStub(Logger::class); + $logger->method('getHandlers')->willReturn([$handler]); + + $checker = new MonologChecker('10', '20', 1, $logger); + $status = $checker->check([]); + + $expected = CheckStatus::createFailure(); + $expected->addReport('logging', [ + 'handler' => [ + [ + 'logfile' => $file, + 'level' => 'WARNING', + 'logfile-size' => 18, + 'logdir-size' => 70, + 'logfile-rotations' => 4 + ] + ] + ]); + $expected->addMessages( + 'logging', + [ + 'logfile size exceeds 10 bytes', + 'logdir size exceeds 20 bytes', + 'logfile rotations exceed 1' + ] + ); + $this->assertEquals( + $expected, + $status, + 'Status is not as expected' + ); + } +} diff --git a/test/Service/Checker/PhpStatusTest.php b/test/Service/Checker/PhpStatusTest.php new file mode 100644 index 0000000..f0818db --- /dev/null +++ b/test/Service/Checker/PhpStatusTest.php @@ -0,0 +1,165 @@ +platform = $this->createStub(Platform::class); + $this->platform->method('getPhpIniLoadedFile') + ->willReturn('php.ini'); + $this->platform->method('getIni') + ->willReturnMap([ + ['date.timezone', 'Timezone'] + ]); + $this->platform->method('getVersion') + ->willReturn('8.3.0'); + $this->platform->method('getOpcacheGetStatus') + ->willReturn([ + 'memory_usage' => '123', + 'other' => 'test' + ]); + $this->platform->method('getFpmPoolStatus') + ->willReturn([ + 'pool' => 'www', + 'other' => 'test' + ]); + } + + /** + * @throws \JsonException + */ + public function testGetStatus(): void + { + $phpStatus = new PhpStatus( + $this->resourceDir . '/phpStatus.json', + 'fpm-fcgi', + $this->platform + ); + $status = $phpStatus->check(); + + $expected = CheckStatus::createSuccess(); + $expected->addReport('php', [ + 'version' => '8.3.0', + 'ini' => [ + 'file' => 'php.ini', + 'date.timezone' => 'Timezone' + ], + 'fpm' => [ + 'config' => [ + 'section' => [ + 'key' => 'value', + 'includetest' => 'test' + ], + 'global' => [ + 'include' => 'fpm/conf.d/*.conf' + ] + ], + 'status' => [ + 'pool' => 'www' + ] + ], + 'opcache' => [ + 'memory_usage' => '123' + ] + ]); + $this->assertEquals( + $expected, + $status, + 'Unexpected status' + ); + } + + public function testGetStatusUnreadablePhpFpmConf(): void + { + $this->expectException(RuntimeException::class); + $phpStatus = new PhpStatus( + $this->resourceDir . '/phpStatus-unreadable-php-fpm-conf.json', + 'fpm-fcgi', + $this->platform + ); + $phpStatus->check(); + } + + /** + * @throws JsonException + */ + public function testGetStatusUnreadablePhpFpmConfInclude(): void + { + $this->expectException(RuntimeException::class); + $phpStatus = new PhpStatus( + $this->resourceDir + . '/phpStatus-unreadable-php-fpm-conf-include.json', + 'fpm-fcgi', + $this->platform + ); + $phpStatus->check(); + } + + public function testGetStatusWithEmptyConfig(): void + { + $phpStatus = new PhpStatus( + $this->resourceDir . '/empty-phpStatus.json', + 'fpm-fcgi', + $this->platform + ); + $status = $phpStatus->check(); + $expected = CheckStatus::createSuccess(); + $expected->addReport('php', [ + 'version' => '8.3.0', + 'ini' => [ + 'file' => 'php.ini', + ], + 'fpm' => [ + 'config' => [ + ], + 'status' => [ + ] + ], + 'opcache' => [ + ] + ]); + + $this->assertEquals( + $expected, + $status, + 'Unexpected status' + ); + } + + /** + * @throws JsonException + */ + public function testGetStatusWithUnreadableConfig(): void + { + $this->expectException(RuntimeException::class); + new PhpStatus( + $this->resourceDir . '/non-exists-phpStatus.json', + 'fpm-fcgi', + $this->platform + ); + } +} diff --git a/test/Service/Checker/ProcessStatusTest.php b/test/Service/Checker/ProcessStatusTest.php new file mode 100644 index 0000000..947b635 --- /dev/null +++ b/test/Service/Checker/ProcessStatusTest.php @@ -0,0 +1,55 @@ +originScriptFilename; + } + + public function setUp(): void + { + $this->originScriptFilename = $_SERVER['SCRIPT_FILENAME'] ?? ''; + $_SERVER['SCRIPT_FILENAME'] = '/path/to/bin/console'; + $this->platform = $this->createStub(Platform::class); + $this->platform->method('getUser') + ->willReturn('user'); + $this->platform->method('getGroup') + ->willReturn('group'); + } + + public function testGetStatus(): void + { + $processStatus = new ProcessStatus( + $this->platform + ); + $status = $processStatus->check(); + + $expected = CheckStatus::createSuccess(); + $expected->addReport('process', [ + 'user' => 'user', + 'group' => 'group', + 'script' => '/path/to/bin/console', + ]); + $this->assertEquals( + $expected, + $status, + 'Unexpected status' + ); + } +} diff --git a/test/Service/Cli/FastCGIStatusTest.php b/test/Service/Cli/FastCGIStatusTest.php new file mode 100644 index 0000000..7e91fdd --- /dev/null +++ b/test/Service/Cli/FastCGIStatusTest.php @@ -0,0 +1,139 @@ +client = $this->createMock(Client::class); + $this->connection = $this->createMock( + ConfiguresSocketConnection::class + ); + $this->response = $this->createStub(ProvidesResponseData::class); + + $this->fastCGIStatus = new FastCGIStatus( + 'mysocket', + $this->client, + $this->connection, + '/path/to/front-controller', + '', + '' + ); + } + + public function testRequest(): void + { + $this->response->method('getBody') + ->willReturn(json_encode([ + 'success' => true, + RuntimeType::FPM_FCGI->value => [ + 'success' => true, + 'reports' => [ + 'test' => [ + 'a' => 'b' + ] + ] + ] + ], JSON_THROW_ON_ERROR)); + $this->client->method('sendRequest') + ->willReturn($this->response); + + + $status = $this->fastCGIStatus->request([]); + + $expected = CheckStatus::createSuccess(); + $expected->addReport('test', [ + 'a' => 'b' + ]); + + $this->assertEquals( + $expected, + $status, + 'Unexpected CheckStatus' + ); + } + + public function testRequestMissingFpmFcgi(): void + { + $this->response->method('getBody') + ->willReturn(json_encode([ + 'success' => true, + ], JSON_THROW_ON_ERROR)); + $this->client->method('sendRequest') + ->willReturn($this->response); + + $status = $this->fastCGIStatus->request([]); + + $expected = CheckStatus::createFailure(); + $expected->addMessage( + 'fpm-fcgi', + 'No FastCGI status found in response.' + ); + + $this->assertEquals( + $expected, + $status, + 'Unexpected CheckStatus' + ); + } + public function testRequestWithInvalidJson(): void + { + $this->response->method('getBody') + ->willReturn('invalid-json'); + $this->client->method('sendRequest') + ->willReturn($this->response); + + $status = $this->fastCGIStatus->request([]); + + $expected = CheckStatus::createFailure(); + $expected->addMessage( + 'fpm-fcgi', + "JSON error: Syntax error\ninvalid-json" + ); + + $this->assertEquals( + $expected, + $status, + 'Unexpected CheckStatus' + ); + } + + public function testRequestWithException(): void + { + $this->client->method('sendRequest') + ->willThrowException(new RuntimeException('error')); + + $status = $this->fastCGIStatus->request([]); + + $expected = CheckStatus::createFailure(); + $expected->addMessage( + 'fpm-fcgi', + 'FastCGI error: error (mysocket)' + ); + + $this->assertEquals( + $expected, + $status, + 'Unexpected CheckStatus' + ); + } +} diff --git a/test/Service/Cli/FastCgiStatusFactoryTest.php b/test/Service/Cli/FastCgiStatusFactoryTest.php new file mode 100644 index 0000000..54195e9 --- /dev/null +++ b/test/Service/Cli/FastCgiStatusFactoryTest.php @@ -0,0 +1,93 @@ +create(); + + $this->assertEquals( + '127.0.0.1:9000', + $status->getSocket(), + 'Unexpected socket' + ); + } + + public function testCreateWithGivenSocket(): void + { + $factory = new FastCgiStatusFactory( + possibleSocketFilePatterns: [], + frontControllerPath: 'test', + resourceRoot: 'test', + resourceHost: 'test' + ); + $status = $factory->create('1.2.3.4:9000'); + + $this->assertEquals( + '1.2.3.4:9000', + $status->getSocket(), + 'Unexpected socket' + ); + } + + public function testCreateWithUnixSocket(): void + { + $factory = new FastCgiStatusFactory( + possibleSocketFilePatterns: [ + $this->resourceDir . '/unix-*' + ], + frontControllerPath: 'test', + resourceRoot: 'test', + resourceHost: 'test' + ); + + $status = $factory->create(); + + $this->assertEquals( + $this->resourceDir . '/unix-socket', + $status->getSocket(), + 'Unexpected socket' + ); + } + + public function testWithNullResourceRoot(): void + { + $this->expectException(RuntimeException::class); + new FastCgiStatusFactory( + possibleSocketFilePatterns: [], + frontControllerPath: 'test', + resourceRoot: null, + resourceHost: 'test' + ); + } + + public function testWithNullResourceHost(): void + { + $this->expectException(RuntimeException::class); + new FastCgiStatusFactory( + possibleSocketFilePatterns: [], + frontControllerPath: 'test', + resourceRoot: 'test', + resourceHost: null + ); + } +} diff --git a/test/Service/Cli/RuntimeCheckTest.php b/test/Service/Cli/RuntimeCheckTest.php new file mode 100644 index 0000000..bfb2c2d --- /dev/null +++ b/test/Service/Cli/RuntimeCheckTest.php @@ -0,0 +1,94 @@ +addReport('cli-status', ['a' => 'b']); + $fastCgiCheckStatus = CheckStatus::createSuccess() + ->addReport('fpm-status', ['a' => 'b']); + $workerCheckStatus = CheckStatus::createSuccess() + ->addReport('worker-status', ['a' => 'b']); + $checkerCollection = $this->createStub(CheckerCollection::class); + $checkerCollection->method('check') + ->willReturn($cliCheckStatus); + $fastCgiStatus = $this->createStub(FastCgiStatus::class); + $fastCgiStatus->method('request') + ->willReturn($fastCgiCheckStatus); + $fastCgiStatusFactory = $this->createStub(FastCgiStatusFactory::class); + $fastCgiStatusFactory->method('create') + ->willReturn($fastCgiStatus); + $workerStatusFile = $this->createStub(WorkerStatusFile::class); + $workerStatusFile->method('read') + ->willReturn($workerCheckStatus); + + $runtimeCheck = new RuntimeCheck( + $checkerCollection, + $fastCgiStatusFactory, + $workerStatusFile + ); + $runtimeStatus = $runtimeCheck->execute([], null); + + $expected = new RuntimeStatus(); + $expected->addStatus(RuntimeType::CLI, $cliCheckStatus); + $expected->addStatus(RuntimeType::FPM_FCGI, $fastCgiCheckStatus); + $expected->addStatus(RuntimeType::WORKER, $workerCheckStatus); + + $this->assertEquals( + $expected, + $runtimeStatus, + 'Runtime status is not as expected' + ); + } + + public function testCheckSkip(): void + { + $checkerCollection = $this->createStub(CheckerCollection::class); + $fastCgiStatus = $this->createStub(FastCgiStatus::class); + $fastCgiStatusFactory = $this->createStub(FastCgiStatusFactory::class); + $fastCgiStatusFactory->method('create') + ->willReturn($fastCgiStatus); + $workerStatusFile = $this->createStub(WorkerStatusFile::class); + + $runtimeCheck = new RuntimeCheck( + $checkerCollection, + $fastCgiStatusFactory, + $workerStatusFile + ); + $runtimeStatus = $runtimeCheck->execute([ + RuntimeType::CLI->value, + RuntimeType::FPM_FCGI->value, + RuntimeType::WORKER->value, + ], null); + + $expected = new RuntimeStatus(); + + $this->assertEquals( + $expected, + $runtimeStatus, + 'Runtime status is not as expected' + ); + } +} diff --git a/test/Service/FpmFcgi/CliStatusTest.php b/test/Service/FpmFcgi/CliStatusTest.php new file mode 100644 index 0000000..2ef662e --- /dev/null +++ b/test/Service/FpmFcgi/CliStatusTest.php @@ -0,0 +1,48 @@ +resourceDir . '/console-success'; + $cliStatus = new CliStatus($consoleBinPath, 'resource-root'); + $checkStatus = $cliStatus->execute([]); + $this->assertTrue( + $checkStatus->success, + 'CheckStatus should be successful' + ); + } + + public function testExecuteMissingCliStatus(): void + { + $consoleBinPath = $this->resourceDir . '/console-missing-cli'; + $cliStatus = new CliStatus($consoleBinPath, 'resource-root'); + $checkStatus = $cliStatus->execute([]); + $this->assertFalse( + $checkStatus->success, + 'CheckStatus should not be successful' + ); + } + + public function testExecuteWithExitCode1(): void + { + $consoleBinPath = $this->resourceDir . '/console-exitcode-1'; + $cliStatus = new CliStatus($consoleBinPath, 'resource-root'); + $checkStatus = $cliStatus->execute([]); + $this->assertFalse( + $checkStatus->success, + 'CheckStatus should be successful' + ); + } +} diff --git a/test/Service/FpmFcgi/RuntimeCheckTest.php b/test/Service/FpmFcgi/RuntimeCheckTest.php new file mode 100644 index 0000000..acdac72 --- /dev/null +++ b/test/Service/FpmFcgi/RuntimeCheckTest.php @@ -0,0 +1,82 @@ +addReport('fpm-status', ['a' => 'b']); + $cliCheckStatus = CheckStatus::createSuccess() + ->addReport('cli-status', ['a' => 'b']); + $workerCheckStatus = CheckStatus::createSuccess() + ->addReport('worker-status', ['a' => 'b']); + $checkerCollection = $this->createStub(CheckerCollection::class); + $checkerCollection->method('check') + ->willReturn($fpmCheckStatus); + $cliStatus = $this->createStub(CliStatus::class); + $cliStatus->method('execute') + ->willReturn($cliCheckStatus); + $workerStatusFile = $this->createStub(WorkerStatusFile::class); + $workerStatusFile->method('read') + ->willReturn($workerCheckStatus); + + $runtimeCheck = new RuntimeCheck( + $checkerCollection, + $cliStatus, + $workerStatusFile + ); + $runtimeStatus = $runtimeCheck->execute([]); + + $expected = new RuntimeStatus(); + $expected->addStatus(RuntimeType::FPM_FCGI, $fpmCheckStatus); + $expected->addStatus(RuntimeType::CLI, $cliCheckStatus); + $expected->addStatus(RuntimeType::WORKER, $workerCheckStatus); + + $this->assertEquals( + $expected, + $runtimeStatus, + 'Runtime status is not as expected' + ); + } + + public function testCheckSkip(): void + { + $checkerCollection = $this->createStub(CheckerCollection::class); + $cliStatus = $this->createStub(CliStatus::class); + $workerStatusFile = $this->createStub(WorkerStatusFile::class); + + $runtimeCheck = new RuntimeCheck( + $checkerCollection, + $cliStatus, + $workerStatusFile + ); + $runtimeStatus = $runtimeCheck->execute([ + RuntimeType::FPM_FCGI->value, + RuntimeType::CLI->value, + RuntimeType::WORKER->value, + ]); + + $expected = new RuntimeStatus(); + + $this->assertEquals( + $expected, + $runtimeStatus, + 'Runtime status is not as expected' + ); + } +} diff --git a/test/Service/RuntimeStatusTest.php b/test/Service/RuntimeStatusTest.php new file mode 100644 index 0000000..40a6d62 --- /dev/null +++ b/test/Service/RuntimeStatusTest.php @@ -0,0 +1,142 @@ +addReport('test', ['a' => 'b']); + $runtimeStatus = new RuntimeStatus(); + $runtimeStatus->addStatus(RuntimeType::CLI, $checkStatus); + + $this->assertEquals( + $checkStatus, + $runtimeStatus->getStatus(RuntimeType::CLI), + 'Status is not as expected' + ); + } + + public function testGetTypes(): void + { + $runtimeStatus = new RuntimeStatus(); + $runtimeStatus->addStatus( + RuntimeType::CLI, + CheckStatus::createSuccess() + ); + $runtimeStatus->addStatus( + RuntimeType::WORKER, + CheckStatus::createSuccess() + ); + + $this->assertEquals( + [RuntimeType::CLI, RuntimeType::WORKER], + $runtimeStatus->getTypes(), + 'Unexpected types' + ); + } + + public function testIsSuccess(): void + { + $runtimeStatus = new RuntimeStatus(); + $runtimeStatus->addStatus( + RuntimeType::CLI, + CheckStatus::createSuccess() + ); + + $this->assertTrue( + $runtimeStatus->isSuccess(), + 'Status should be successful' + ); + } + + public function testIsNotSuccess(): void + { + $runtimeStatus = new RuntimeStatus(); + $runtimeStatus->addStatus( + RuntimeType::CLI, + CheckStatus::createSuccess() + ); + $runtimeStatus->addStatus( + RuntimeType::WORKER, + CheckStatus::createFailure() + ); + + $this->assertFalse( + $runtimeStatus->isSuccess(), + 'Status should not be successful' + ); + } + + public function testGetMessages(): void + { + $checkStatus = CheckStatus::createSuccess() + ->addMessage('test', 'message'); + $runtimeStatus = new RuntimeStatus(); + $runtimeStatus->addStatus(RuntimeType::CLI, $checkStatus); + + $this->assertEquals( + ['cli/test: message'], + $runtimeStatus->getMessages(), + 'Messages are not as expected' + ); + } + + public function testSerialize(): void + { + $checkStatus = CheckStatus::createSuccess() + ->addReport('test', ['a' => 'b']) + ->addMessage('test', 'message'); + $runtimeStatus = new RuntimeStatus(); + $runtimeStatus->addStatus(RuntimeType::CLI, $checkStatus); + + $this->assertEquals( + [ + 'cli' => [ + 'success' => true, + 'reports' => ['test' => ['a' => 'b']], + 'messages' => ['test' => ['message']], + ], + 'success' => true, + 'messages' => ['cli/test: message'] + ], + $runtimeStatus->serialize(), + 'Status is not as expected' + ); + } + + public function testDeserialize(): void + { + $data = [ + 'cli' => [ + 'success' => true, + 'reports' => ['test' => ['a' => 'b']], + 'messages' => ['test' => ['message']], + ], + 'success' => true, + 'messages' => ['cli/test: message'] + ]; + + $checkStatus = CheckStatus::createSuccess() + ->addReport('test', ['a' => 'b']) + ->addMessage('test', 'message'); + $expected = new RuntimeStatus(); + $expected->addStatus(RuntimeType::CLI, $checkStatus); + + $this->assertEquals( + $expected, + RuntimeStatus::deserialize($data), + 'Status is not as expected' + ); + } +} diff --git a/test/Service/RuntimeTypeTest.php b/test/Service/RuntimeTypeTest.php new file mode 100644 index 0000000..dfc576a --- /dev/null +++ b/test/Service/RuntimeTypeTest.php @@ -0,0 +1,23 @@ +assertEquals( + [RuntimeType::FPM_FCGI, RuntimeType::WORKER], + $cases, + 'Cases are not as expected' + ); + } +} diff --git a/test/Service/Worker/OneTimeTriggerTest.php b/test/Service/Worker/OneTimeTriggerTest.php new file mode 100644 index 0000000..12a1289 --- /dev/null +++ b/test/Service/Worker/OneTimeTriggerTest.php @@ -0,0 +1,58 @@ +assertEquals( + 'one time', + (string) $trigger + ); + } + + public function testToStringAlreadyRunning(): void + { + $trigger = new OneTimeTrigger(); + $trigger->getNextRunDate(new \DateTimeImmutable()); + + $this->assertEquals( + 'one time (already running)', + (string) $trigger + ); + } + + public function testGetNextRunDate(): void + { + $trigger = new OneTimeTrigger(); + + $run = new \DateTimeImmutable(); + $this->assertEquals( + $run, + $trigger->getNextRunDate($run), + 'The first call to getNextRunDate should return the same date' + ); + } + + public function testSecondGetNextRunDate(): void + { + $trigger = new OneTimeTrigger(); + $run = new \DateTimeImmutable(); + $trigger->getNextRunDate($run); + + $this->assertNull( + $trigger->getNextRunDate($run), + 'The second call to getNextRunDate should return null' + ); + } +} diff --git a/test/Service/Worker/WorkerCheckSchedulerTest.php b/test/Service/Worker/WorkerCheckSchedulerTest.php new file mode 100644 index 0000000..c58284f --- /dev/null +++ b/test/Service/Worker/WorkerCheckSchedulerTest.php @@ -0,0 +1,61 @@ +createStub(CheckerCollection::class); + $lockFactory = $this->createStub(LockFactory::class); + $workerCheckScheduler = new WorkerCheckScheduler( + $workerStatusFile, + $checkerCollection, + 'www', + $lockFactory + ); + + $schedule = $workerCheckScheduler->getSchedule(); + + $this->assertEquals( + 2, + count($schedule->getRecurringMessages()) + ); + } + + public function testInvoke(): void + { + $checkStatus = CheckStatus::createSuccess(); + + $workerStatusFile = $this->createMock(WorkerStatusFile::class); + $checkerCollection = $this->createStub(CheckerCollection::class); + $checkerCollection->method('check') + ->willReturn($checkStatus); + $lockFactory = $this->createStub(LockFactory::class); + $workerCheckScheduler = new WorkerCheckScheduler( + $workerStatusFile, + $checkerCollection, + 'www', + $lockFactory + ); + + + $workerStatusFile->expects($this->once()) + ->method('write') + ->with($checkStatus); + $workerCheckScheduler->__invoke(new WorkerCheckEvent()); + } +} diff --git a/test/Service/Worker/WorkerStatusFileTest.php b/test/Service/Worker/WorkerStatusFileTest.php new file mode 100644 index 0000000..5bb536a --- /dev/null +++ b/test/Service/Worker/WorkerStatusFileTest.php @@ -0,0 +1,269 @@ +testDir)) { + mkdir($this->testDir, 0777, true); + } + } + + public function tearDown(): void + { + if (is_dir($this->testDir)) { + foreach (scandir($this->testDir) as $file) { + if ($file === '.' || $file === '..') { + continue; + } + unlink($this->testDir . '/' . $file); + } + rmdir($this->testDir); + } + } + + public function testRead(): void + { + $date = new DateTime( + '2024-06-10 13:31:00' + ); + $platform = $this->createStub(Platform::class); + $platform->method('time') + ->willReturn($date->getTimestamp()); + + $statusFile = new WorkerStatusFile( + $this->resourceDir . '/statusFile.json', + 10, + $platform + ); + $status = $statusFile->read(); + + $expected = CheckStatus::createSuccess(); + $expected->addReport('scheduler', [ + 'last-run' => '10.06.2024 13:16:00' + ]); + + $this->assertEquals( + $expected, + $status, + 'Unexpected status' + ); + } + + public function testReadWithEmptyStatusFile(): void + { + $date = new DateTime( + '2024-06-10 13:31:00' + ); + $platform = $this->createStub(Platform::class); + $platform->method('time') + ->willReturn($date->getTimestamp()); + + $statusFile = new WorkerStatusFile( + $this->resourceDir . '/empty-statusFile.json', + 10, + $platform + ); + $status = $statusFile->read(); + + $expected = CheckStatus::createFailure(); + $expected->addMessage( + 'scheduler', + 'The worker did not run in the last 10 minutes.' + . ' Last run: unknown' + ); + + $this->assertEquals( + $expected, + $status, + 'Unexpected status' + ); + } + public function testReadFileNotExists(): void + { + $statusFile = new WorkerStatusFile( + $this->resourceDir . '/no-exists.json', + 10, + ); + $status = $statusFile->read(); + + $expected = CheckStatus::createFailure(); + $expected->addMessage('worker', 'worker not running'); + + $this->assertEquals( + $expected, + $status, + 'Unexpected status' + ); + } + + public function testReadFileNotReadable(): void + { + $testFile = $this->testDir . '/not-readable'; + touch($testFile); + chmod($testFile, 0000); + $this->expectException(RuntimeException::class); + try { + $statusFile = new WorkerStatusFile( + $testFile, + 10, + ); + $statusFile->read(); + } finally { + chmod($testFile, 0777); + unlink($testFile); + } + } + + + public function testReadFileLastRunExpired(): void + { + $date = new DateTime( + '2024-06-10 13:32:00' + ); + $platform = $this->createStub(Platform::class); + $platform->method('time') + ->willReturn($date->getTimestamp()); + + $statusFile = new WorkerStatusFile( + $this->resourceDir . '/statusFile.json', + 10, + $platform + ); + $status = $statusFile->read(); + + $expected = CheckStatus::createFailure(); + $expected->addReport('scheduler', [ + 'last-run' => '10.06.2024 13:16:00' + ]); + $expected->addMessage( + 'scheduler', + 'The worker did not run in the last 10 minutes.' + . ' Last run: 10.06.2024 13:16:00' + ); + + $this->assertEquals( + $expected, + $status, + 'Unexpected status' + ); + } + + public function testReadWithInvalidLastRun(): void + { + $date = new DateTime( + '2024-06-10 13:16:00' + ); + $platform = $this->createStub(Platform::class); + $platform->method('time') + ->willReturn($date->getTimestamp()); + + $statusFile = new WorkerStatusFile( + $this->resourceDir . '/statusFileWithInvalidLastRun.json', + 10, + $platform + ); + $status = $statusFile->read(); + + $expected = CheckStatus::createFailure(); + $expected->addReport('scheduler', [ + 'last-run' => 123 + ]); + $expected->addMessage( + 'scheduler', + 'The worker did not run in the last 10 minutes.' + . ' Last run: 123' + ); + + $this->assertEquals( + $expected, + $status, + 'Unexpected status' + ); + } + + /** + * @throws Exception + * @throws \JsonException + */ + public function testWrite(): void + { + + $date = new DateTime( + '2024-06-10 13:32:00' + ); + $platform = $this->createStub(Platform::class); + $platform->method('time') + ->willReturn($date->getTimestamp()); + + $file = $this->testDir . '/statusFile.json'; + $statusFile = new WorkerStatusFile( + $file, + 10, + $platform + ); + + $checkStatus = CheckStatus::createSuccess(); + $checkStatus->addReport('test', [ + 'a' => 'b' + ]); + + try { + $_SERVER['SUPERVISOR_ENABLED'] = '1'; + $_SERVER['SUPERVISOR_GROUP_NAME'] = 'worker'; + $_SERVER['SUPERVISOR_PROCESS_NAME'] = 'worker_01'; + $statusFile->write($checkStatus); + } finally { + unset($_SERVER['SUPERVISOR_ENABLED']); + unset($_SERVER['SUPERVISOR_GROUP_NAME']); + unset($_SERVER['SUPERVISOR_PROCESS_NAME']); + } + + $writtenData = json_decode( + file_get_contents($file), + true, + 512, + JSON_THROW_ON_ERROR + ); + + $this->assertEquals( + [ + 'success' => true, + 'reports' => [ + 'test' => [ + 'a' => 'b' + ], + 'supervisor' => [ + 'group' => 'worker', + 'process' => 'worker_01' + ], + 'scheduler' => [ + 'last-run' => '10.06.2024 13:32:00' + ], + ] + ], + $writtenData, + 'Unexpected status' + ); + } +} diff --git a/test/resources/Service/Checker/MonologCheckerTest/logging.log b/test/resources/Service/Checker/MonologCheckerTest/logging.log new file mode 100644 index 0000000..4e18c57 --- /dev/null +++ b/test/resources/Service/Checker/MonologCheckerTest/logging.log @@ -0,0 +1 @@ +one log file entry \ No newline at end of file diff --git a/test/resources/Service/Checker/MonologCheckerTest/logging.log.1 b/test/resources/Service/Checker/MonologCheckerTest/logging.log.1 new file mode 100644 index 0000000..4e18c57 --- /dev/null +++ b/test/resources/Service/Checker/MonologCheckerTest/logging.log.1 @@ -0,0 +1 @@ +one log file entry \ No newline at end of file diff --git a/test/resources/Service/Checker/MonologCheckerTest/logging.log.1.gz b/test/resources/Service/Checker/MonologCheckerTest/logging.log.1.gz new file mode 100644 index 0000000..b824260 --- /dev/null +++ b/test/resources/Service/Checker/MonologCheckerTest/logging.log.1.gz @@ -0,0 +1 @@ +dummy gz \ No newline at end of file diff --git a/test/resources/Service/Checker/MonologCheckerTest/logging.log.2 b/test/resources/Service/Checker/MonologCheckerTest/logging.log.2 new file mode 100644 index 0000000..4e18c57 --- /dev/null +++ b/test/resources/Service/Checker/MonologCheckerTest/logging.log.2 @@ -0,0 +1 @@ +one log file entry \ No newline at end of file diff --git a/test/resources/Service/Checker/MonologCheckerTest/logging.log.2.gz b/test/resources/Service/Checker/MonologCheckerTest/logging.log.2.gz new file mode 100644 index 0000000..b824260 --- /dev/null +++ b/test/resources/Service/Checker/MonologCheckerTest/logging.log.2.gz @@ -0,0 +1 @@ +dummy gz \ No newline at end of file diff --git a/test/resources/Service/Checker/PhpStatusTest/empty-phpStatus.json b/test/resources/Service/Checker/PhpStatusTest/empty-phpStatus.json new file mode 100644 index 0000000..7a73a41 --- /dev/null +++ b/test/resources/Service/Checker/PhpStatusTest/empty-phpStatus.json @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/test/resources/Service/Checker/PhpStatusTest/fpm-config-include-unreadable/conf.d/include.conf b/test/resources/Service/Checker/PhpStatusTest/fpm-config-include-unreadable/conf.d/include.conf new file mode 100644 index 0000000..8e2f0be --- /dev/null +++ b/test/resources/Service/Checker/PhpStatusTest/fpm-config-include-unreadable/conf.d/include.conf @@ -0,0 +1 @@ +[ \ No newline at end of file diff --git a/test/resources/Service/Checker/PhpStatusTest/fpm-config-include-unreadable/php-fpm.conf b/test/resources/Service/Checker/PhpStatusTest/fpm-config-include-unreadable/php-fpm.conf new file mode 100644 index 0000000..8cc1390 --- /dev/null +++ b/test/resources/Service/Checker/PhpStatusTest/fpm-config-include-unreadable/php-fpm.conf @@ -0,0 +1,4 @@ +[section] +key = value +[global] +include = fpm-config-include-unreadable/conf.d/*.conf \ No newline at end of file diff --git a/test/resources/Service/Checker/PhpStatusTest/fpm-config-unreadable/php-fpm.conf b/test/resources/Service/Checker/PhpStatusTest/fpm-config-unreadable/php-fpm.conf new file mode 100644 index 0000000..8e2f0be --- /dev/null +++ b/test/resources/Service/Checker/PhpStatusTest/fpm-config-unreadable/php-fpm.conf @@ -0,0 +1 @@ +[ \ No newline at end of file diff --git a/test/resources/Service/Checker/PhpStatusTest/fpm/conf.d/include.conf b/test/resources/Service/Checker/PhpStatusTest/fpm/conf.d/include.conf new file mode 100644 index 0000000..f02c8f3 --- /dev/null +++ b/test/resources/Service/Checker/PhpStatusTest/fpm/conf.d/include.conf @@ -0,0 +1,2 @@ +[section] +includetest = test \ No newline at end of file diff --git a/test/resources/Service/Checker/PhpStatusTest/fpm/php-fpm.conf b/test/resources/Service/Checker/PhpStatusTest/fpm/php-fpm.conf new file mode 100644 index 0000000..7cae0f7 --- /dev/null +++ b/test/resources/Service/Checker/PhpStatusTest/fpm/php-fpm.conf @@ -0,0 +1,4 @@ +[section] +key = value +[global] +include = fpm/conf.d/*.conf \ No newline at end of file diff --git a/test/resources/Service/Checker/PhpStatusTest/phpStatus-unreadable-php-fpm-conf-include.json b/test/resources/Service/Checker/PhpStatusTest/phpStatus-unreadable-php-fpm-conf-include.json new file mode 100644 index 0000000..ae4c8ed --- /dev/null +++ b/test/resources/Service/Checker/PhpStatusTest/phpStatus-unreadable-php-fpm-conf-include.json @@ -0,0 +1,5 @@ +{ + "fpm" : { + "configDirs" : ["test/resources/Service/Checker/PhpStatusTest/fpm-config-include-unreadable"] + } +} \ No newline at end of file diff --git a/test/resources/Service/Checker/PhpStatusTest/phpStatus-unreadable-php-fpm-conf.json b/test/resources/Service/Checker/PhpStatusTest/phpStatus-unreadable-php-fpm-conf.json new file mode 100644 index 0000000..8eb3a26 --- /dev/null +++ b/test/resources/Service/Checker/PhpStatusTest/phpStatus-unreadable-php-fpm-conf.json @@ -0,0 +1,5 @@ +{ + "fpm" : { + "configDirs" : ["test/resources/Service/Checker/PhpStatusTest/fpm-config-unreadable"] + } +} \ No newline at end of file diff --git a/test/resources/Service/Checker/PhpStatusTest/phpStatus.json b/test/resources/Service/Checker/PhpStatusTest/phpStatus.json new file mode 100644 index 0000000..4cb606a --- /dev/null +++ b/test/resources/Service/Checker/PhpStatusTest/phpStatus.json @@ -0,0 +1,14 @@ +{ + "ini" : [ + "date.timezone" + ], + "fpm" : { + "configDirs" : ["test/resources/Service/Checker/PhpStatusTest/fpm"], + "status" : [ + "pool" + ] + }, + "opcache" : [ + "memory_usage" + ] +} \ No newline at end of file diff --git a/test/resources/Service/Cli/FastCgiStatusFactoryTest/unix-socket b/test/resources/Service/Cli/FastCgiStatusFactoryTest/unix-socket new file mode 100644 index 0000000..e4cc651 --- /dev/null +++ b/test/resources/Service/Cli/FastCgiStatusFactoryTest/unix-socket @@ -0,0 +1 @@ +# dummy file \ No newline at end of file diff --git a/test/resources/Service/FpmFcgi/CliStatusTest/console-exitcode-1 b/test/resources/Service/FpmFcgi/CliStatusTest/console-exitcode-1 new file mode 100755 index 0000000..afcacbf --- /dev/null +++ b/test/resources/Service/FpmFcgi/CliStatusTest/console-exitcode-1 @@ -0,0 +1,2 @@ +#!/bin/bash +exit 1; diff --git a/test/resources/Service/FpmFcgi/CliStatusTest/console-missing-cli b/test/resources/Service/FpmFcgi/CliStatusTest/console-missing-cli new file mode 100755 index 0000000..3f6d515 --- /dev/null +++ b/test/resources/Service/FpmFcgi/CliStatusTest/console-missing-cli @@ -0,0 +1,6 @@ +#!/bin/bash +cat << EOF +{ + "success": true +} +EOF \ No newline at end of file diff --git a/test/resources/Service/FpmFcgi/CliStatusTest/console-success b/test/resources/Service/FpmFcgi/CliStatusTest/console-success new file mode 100755 index 0000000..682a592 --- /dev/null +++ b/test/resources/Service/FpmFcgi/CliStatusTest/console-success @@ -0,0 +1,9 @@ +#!/bin/bash +cat << EOF +{ + "success": true, + "cli": { + "success": true + } +} +EOF \ No newline at end of file diff --git a/test/resources/Service/Worker/WorkerStatusFileTest/empty-statusFile.json b/test/resources/Service/Worker/WorkerStatusFileTest/empty-statusFile.json new file mode 100644 index 0000000..86a346c --- /dev/null +++ b/test/resources/Service/Worker/WorkerStatusFileTest/empty-statusFile.json @@ -0,0 +1,3 @@ +{ + "success": true +} \ No newline at end of file diff --git a/test/resources/Service/Worker/WorkerStatusFileTest/statusFile.json b/test/resources/Service/Worker/WorkerStatusFileTest/statusFile.json new file mode 100644 index 0000000..ded6e7b --- /dev/null +++ b/test/resources/Service/Worker/WorkerStatusFileTest/statusFile.json @@ -0,0 +1,8 @@ +{ + "success" : true, + "reports" : { + "scheduler": { + "last-run": "10.06.2024 13:16:00" + } + } +} \ No newline at end of file diff --git a/test/resources/Service/Worker/WorkerStatusFileTest/statusFileWithInvalidLastRun.json b/test/resources/Service/Worker/WorkerStatusFileTest/statusFileWithInvalidLastRun.json new file mode 100644 index 0000000..3cc34de --- /dev/null +++ b/test/resources/Service/Worker/WorkerStatusFileTest/statusFileWithInvalidLastRun.json @@ -0,0 +1,8 @@ +{ + "success" : true, + "reports" : { + "scheduler": { + "last-run": 123 + } + } +}