From 8dd39b7e6e03b18e8c5b3600386c91720b6049b8 Mon Sep 17 00:00:00 2001 From: "Abdulmhsen B. A. A" Date: Sat, 19 Feb 2022 20:56:51 +0300 Subject: [PATCH] As the API stabilizing, we started adding tests. --- .dockerignore | 1 + .gitignore | 1 + composer.json | 11 +- composer.lock | 1955 ++++++++++++++++++++++++- phpunit.xml.dist | 11 + public/index.php | 90 +- src/Libs/Entity/StateEntity.php | 6 + src/Libs/Entity/StateInterface.php | 1 + src/Libs/Storage/PDO/PDOAdapter.php | 415 +++--- src/Libs/Storage/StorageException.php | 12 + src/Libs/Storage/StorageInterface.php | 65 +- src/Libs/helpers.php | 91 +- tests/Storage/PDOAdapterTest.php | 313 ++++ tests/bootstrap.php | 15 + 14 files changed, 2626 insertions(+), 361 deletions(-) create mode 100644 phpunit.xml.dist create mode 100644 src/Libs/Storage/StorageException.php create mode 100644 tests/Storage/PDOAdapterTest.php create mode 100644 tests/bootstrap.php diff --git a/.dockerignore b/.dockerignore index e9d9b9c9..dde3d033 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,3 +5,4 @@ !./docker/config/.gitignore ./var/* !./var/.gitignore +.phpunit.result.cache diff --git a/.gitignore b/.gitignore index 1eaa3875..db782c29 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /.idea/* /vendor/* +.phpunit.result.cache diff --git a/composer.json b/composer.json index 5cacc2a2..9a4faff4 100644 --- a/composer.json +++ b/composer.json @@ -10,6 +10,9 @@ "*": "dist" } }, + "scripts": { + "test": "vendor/bin/phpunit --colors=always" + }, "require": { "php": ">=8.1", "ext-pdo": "*", @@ -33,7 +36,13 @@ "require-dev": { "roave/security-advisories": "dev-latest", "symfony/var-dumper": "^6.0", - "perftools/php-profiler": "^1.0" + "perftools/php-profiler": "^1.0", + "phpunit/phpunit": "^9.5" + }, + "autoload-dev": { + "psr-4": { + "Tests\\": "tests" + } }, "autoload": { "files": [ diff --git a/composer.lock b/composer.lock index 01064b95..3e7db2bc 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "2154f627531b8a6fc293d6d2b3cba1a1", + "content-hash": "661be76a15d00ad6df2adbe8d3b96059", "packages": [ { "name": "dragonmantank/cron-expression", @@ -1836,6 +1836,186 @@ } ], "packages-dev": [ + { + "name": "doctrine/instantiator", + "version": "1.4.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/d56bf6102915de5702778fe20f2de3b2fe570b5b", + "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^8.0", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^0.13 || 1.0.0-alpha2", + "phpstan/phpstan": "^0.12", + "phpstan/phpstan-phpunit": "^0.12", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/1.4.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2020-11-10T18:47:58+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.10.2", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/776f831124e9c62e1a2c601ecc52e776d8bb7220", + "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/collections": "^1.0", + "doctrine/common": "^2.6", + "phpunit/phpunit": "^7.1" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.10.2" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2020-11-13T09:40:50+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v4.13.2", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "210577fe3cf7badcc5814d99455df46564f3c077" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/210577fe3cf7badcc5814d99455df46564f3c077", + "reference": "210577fe3cf7badcc5814d99455df46564f3c077", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=7.0" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v4.13.2" + }, + "time": "2021-11-30T19:35:32+00:00" + }, { "name": "perftools/php-profiler", "version": "1.0.0", @@ -1898,6 +2078,765 @@ }, "time": "2021-10-18T01:40:28+00:00" }, + { + "name": "phar-io/manifest", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "97803eca37d319dfa7826cc2437fc020857acb53" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53", + "reference": "97803eca37d319dfa7826cc2437fc020857acb53", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.3" + }, + "time": "2021-07-20T11:28:43+00:00" + }, + { + "name": "phar-io/version", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "15a90844ad40f127afd244c0cad228de2a80052a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/15a90844ad40f127afd244c0cad228de2a80052a", + "reference": "15a90844ad40f127afd244c0cad228de2a80052a", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.1.1" + }, + "time": "2022-02-07T21:56:48+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + }, + "time": "2020-06-27T09:03:43+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "5.3.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "622548b623e81ca6d78b721c5e029f4ce664f170" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/622548b623e81ca6d78b721c5e029f4ce664f170", + "reference": "622548b623e81ca6d78b721c5e029f4ce664f170", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^7.2 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^1.3", + "webmozart/assert": "^1.9.1" + }, + "require-dev": { + "mockery/mockery": "~1.3.2", + "psalm/phar": "^4.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "account@ijaap.nl" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.3.0" + }, + "time": "2021-10-19T17:43:47+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "1.6.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "93ebd0014cab80c4ea9f5e297ea48672f1b87706" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/93ebd0014cab80c4ea9f5e297ea48672f1b87706", + "reference": "93ebd0014cab80c4ea9f5e297ea48672f1b87706", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "phpdocumentor/reflection-common": "^2.0" + }, + "require-dev": { + "ext-tokenizer": "*", + "psalm/phar": "^4.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "support": { + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.6.0" + }, + "time": "2022-01-04T19:58:01+00:00" + }, + { + "name": "phpspec/prophecy", + "version": "v1.15.0", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy.git", + "reference": "bbcd7380b0ebf3961ee21409db7b38bc31d69a13" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/bbcd7380b0ebf3961ee21409db7b38bc31d69a13", + "reference": "bbcd7380b0ebf3961ee21409db7b38bc31d69a13", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.2", + "php": "^7.2 || ~8.0, <8.2", + "phpdocumentor/reflection-docblock": "^5.2", + "sebastian/comparator": "^3.0 || ^4.0", + "sebastian/recursion-context": "^3.0 || ^4.0" + }, + "require-dev": { + "phpspec/phpspec": "^6.0 || ^7.0", + "phpunit/phpunit": "^8.0 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Prophecy\\": "src/Prophecy" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "email": "marcello.duarte@gmail.com" + } + ], + "description": "Highly opinionated mocking framework for PHP 5.3+", + "homepage": "https://github.com/phpspec/prophecy", + "keywords": [ + "Double", + "Dummy", + "fake", + "mock", + "spy", + "stub" + ], + "support": { + "issues": "https://github.com/phpspec/prophecy/issues", + "source": "https://github.com/phpspec/prophecy/tree/v1.15.0" + }, + "time": "2021-12-08T12:19:24+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "9.2.11", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "665a1ac0a763c51afc30d6d130dac0813092b17f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/665a1ac0a763c51afc30d6d130dac0813092b17f", + "reference": "665a1ac0a763c51afc30d6d130dac0813092b17f", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.13.0", + "php": ">=7.3", + "phpunit/php-file-iterator": "^3.0.3", + "phpunit/php-text-template": "^2.0.2", + "sebastian/code-unit-reverse-lookup": "^2.0.2", + "sebastian/complexity": "^2.0", + "sebastian/environment": "^5.1.2", + "sebastian/lines-of-code": "^1.0.3", + "sebastian/version": "^3.0.1", + "theseer/tokenizer": "^1.2.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcov": "*", + "ext-xdebug": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.2-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 that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.11" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-02-18T12:46:09+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "3.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.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": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2021-12-02T12:48:52+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-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": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:58:55+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "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": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T05:33:50+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "5.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.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": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:16:10+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "9.5.14", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "1883687169c017d6ae37c58883ca3994cfc34189" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/1883687169c017d6ae37c58883ca3994cfc34189", + "reference": "1883687169c017d6ae37c58883ca3994cfc34189", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.3.1", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.10.1", + "phar-io/manifest": "^2.0.3", + "phar-io/version": "^3.0.2", + "php": ">=7.3", + "phpspec/prophecy": "^1.12.1", + "phpunit/php-code-coverage": "^9.2.7", + "phpunit/php-file-iterator": "^3.0.5", + "phpunit/php-invoker": "^3.1.1", + "phpunit/php-text-template": "^2.0.3", + "phpunit/php-timer": "^5.0.2", + "sebastian/cli-parser": "^1.0.1", + "sebastian/code-unit": "^1.0.6", + "sebastian/comparator": "^4.0.5", + "sebastian/diff": "^4.0.3", + "sebastian/environment": "^5.1.3", + "sebastian/exporter": "^4.0.3", + "sebastian/global-state": "^5.0.1", + "sebastian/object-enumerator": "^4.0.3", + "sebastian/resource-operations": "^3.0.3", + "sebastian/type": "^2.3.4", + "sebastian/version": "^3.0.2" + }, + "require-dev": { + "ext-pdo": "*", + "phpspec/prophecy-phpunit": "^2.0.1" + }, + "suggest": { + "ext-soap": "*", + "ext-xdebug": "*" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.14" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-02-18T12:54:07+00:00" + }, { "name": "roave/security-advisories", "version": "dev-latest", @@ -2349,6 +3288,970 @@ ], "time": "2022-02-17T14:18:41+00:00" }, + { + "name": "sebastian/cli-parser", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.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 parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:08:49+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "1.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.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": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:08:54+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:30:19+00:00" + }, + { + "name": "sebastian/comparator", + "version": "4.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "55f4261989e546dc112258c7a75935a81a7ce382" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55f4261989e546dc112258c7a75935a81a7ce382", + "reference": "55f4261989e546dc112258c7a75935a81a7ce382", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/diff": "^4.0", + "sebastian/exporter": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T15:49:45+00:00" + }, + { + "name": "sebastian/complexity", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "739b35e53379900cc9ac327b2147867b8b6efd88" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88", + "reference": "739b35e53379900cc9ac327b2147867b8b6efd88", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.7", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "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 calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T15:52:27+00:00" + }, + { + "name": "sebastian/diff", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3461e3fccc7cfdfc2720be910d3bd73c69be590d", + "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:10:38+00:00" + }, + { + "name": "sebastian/environment", + "version": "5.1.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "388b6ced16caa751030f6a69e588299fa09200ac" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/388b6ced16caa751030f6a69e588299fa09200ac", + "reference": "388b6ced16caa751030f6a69e588299fa09200ac", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:52:38+00:00" + }, + { + "name": "sebastian/exporter", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "65e8b7db476c5dd267e65eea9cab77584d3cfff9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/65e8b7db476c5dd267e65eea9cab77584d3cfff9", + "reference": "65e8b7db476c5dd267e65eea9cab77584d3cfff9", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2021-11-11T14:18:36+00:00" + }, + { + "name": "sebastian/global-state", + "version": "5.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/0ca8db5a5fc9c8646244e629625ac486fa286bf2", + "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "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": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-02-14T08:28:10+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.6", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.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", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-28T06:42:11+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.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/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:12:34+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.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/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:14:26+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/cd9d8cf3c5804de4341c283ed787f099f5506172", + "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "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": "http://www.github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:17:30+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", + "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "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": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "support": { + "issues": "https://github.com/sebastianbergmann/resource-operations/issues", + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:45:17+00:00" + }, + { + "name": "sebastian/type", + "version": "2.3.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "b8cd8a1c753c90bc1a0f5372170e3e489136f914" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/b8cd8a1c753c90bc1a0f5372170e3e489136f914", + "reference": "b8cd8a1c753c90bc1a0f5372170e3e489136f914", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.3-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": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/2.3.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2021-06-15T12:49:02+00:00" + }, + { + "name": "sebastian/version", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c6c1022351a901512170118436c764e473f6de8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", + "reference": "c6c1022351a901512170118436c764e473f6de8c", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.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 that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:39:44+00:00" + }, { "name": "symfony/var-dumper", "version": "v6.0.3", @@ -2436,6 +4339,56 @@ } ], "time": "2022-01-17T16:30:44+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e", + "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2021-07-28T10:34:58+00:00" } ], "aliases": [], diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 00000000..38c3f198 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,11 @@ + + + + + tests + + + + diff --git a/public/index.php b/public/index.php index 42160982..47667bad 100644 --- a/public/index.php +++ b/public/index.php @@ -2,15 +2,7 @@ declare(strict_types=1); -use App\Libs\Config; -use App\Libs\Container; -use App\Libs\HttpException; -use App\Libs\Servers\ServerInterface; -use App\Libs\Storage\StorageInterface; -use Nyholm\Psr7\Response; -use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -use Psr\Log\LoggerInterface; error_reporting(E_ALL); ini_set('error_reporting', 'On'); @@ -41,82 +33,8 @@ if (!file_exists(__DIR__ . '/../vendor/autoload.php')) { require __DIR__ . '/../vendor/autoload.php'; -$fn = function (ServerRequestInterface $request): ResponseInterface { - try { - if (true !== Config::get('webhook.enabled', false)) { - throw new HttpException('Webhook is disabled via config.', 500); - } - - if (null === Config::get('webhook.apikey', null)) { - throw new HttpException('No webhook.apikey is set in config.', 500); - } - - // -- get apikey from header or query. - $apikey = $request->getHeaderLine('x-apikey'); - if (empty($apikey)) { - $apikey = ag($request->getQueryParams(), 'apikey', ''); - if (empty($apikey)) { - throw new HttpException('No API key was given.', 400); - } - } - - if (!hash_equals(Config::get('webhook.apikey'), $apikey)) { - throw new HttpException('Invalid API key was given.', 401); - } - - if (null === ($type = ag($request->getQueryParams(), 'type', null))) { - throw new HttpException('No type was given via type= query.', 400); - } - - $types = Config::get('supported', []); - - if (null === ($backend = ag($types, $type))) { - throw new HttpException('Invalid server type was given.', 400); - } - - $class = new ReflectionClass($backend); - - if (!$class->implementsInterface(ServerInterface::class)) { - throw new HttpException('Invalid Parser Class.', 500); - } - - /** @var ServerInterface $backend */ - $entity = $backend::parseWebhook($request); - - if (null === $entity || !$entity->hasGuids()) { - return new Response(status: 200, headers: ['X-Status' => 'No GUIDs.']); - } - - $storage = Container::get(StorageInterface::class); - - if (null === ($backend = $storage->get($entity))) { - $storage->insert($entity); - return jsonResponse(status: 200, body: $entity->getAll()); - } - - if ($backend->updated > $entity->updated) { - return new Response( - status: 200, - headers: ['X-Status' => 'Entity date is older than what available in storage.'] - ); - } - - if ($backend->apply($entity)->isChanged()) { - $backend = $storage->update($backend); - - return jsonResponse(status: 200, body: $backend->getAll()); - } - - return new Response(status: 200, headers: ['X-Status' => 'Entity is unchanged.']); - } catch (HttpException $e) { - Container::get(LoggerInterface::class)->error($e->getMessage()); - - if (200 === $e->getCode()) { - return new Response(status: $e->getCode(), headers: ['X-Status' => $e->getMessage()]); - } - - return jsonResponse(status: $e->getCode(), body: ['error' => true, 'message' => $e->getMessage()]); +(new App\Libs\KernelConsole())->boot()->runHttp( + function (ServerRequestInterface $request) { + return serveHttpRequest($request); } -}; - -(new App\Libs\KernelConsole())->boot()->runHttp($fn); +); diff --git a/src/Libs/Entity/StateEntity.php b/src/Libs/Entity/StateEntity.php index 5f07c742..db2041fd 100644 --- a/src/Libs/Entity/StateEntity.php +++ b/src/Libs/Entity/StateEntity.php @@ -136,6 +136,12 @@ final class StateEntity implements StateInterface return $this; } + public function updateOriginal(): StateInterface + { + $this->data = $this->getAll(); + return $this; + } + private function isEqual(StateInterface $entity): bool { foreach ($this->getAll() as $key => $val) { diff --git a/src/Libs/Entity/StateInterface.php b/src/Libs/Entity/StateInterface.php index f1b16b7b..72799674 100644 --- a/src/Libs/Entity/StateInterface.php +++ b/src/Libs/Entity/StateInterface.php @@ -81,4 +81,5 @@ interface StateInterface */ public function apply(StateInterface $entity, bool $guidOnly = false): StateInterface; + public function updateOriginal(): StateInterface; } diff --git a/src/Libs/Storage/PDO/PDOAdapter.php b/src/Libs/Storage/PDO/PDOAdapter.php index 94def5ed..241b81e9 100644 --- a/src/Libs/Storage/PDO/PDOAdapter.php +++ b/src/Libs/Storage/PDO/PDOAdapter.php @@ -6,6 +6,7 @@ namespace App\Libs\Storage\PDO; use App\Libs\Container; use App\Libs\Entity\StateInterface; +use App\Libs\Storage\StorageException; use App\Libs\Storage\StorageInterface; use Closure; use DateTimeInterface; @@ -14,7 +15,6 @@ use PDO; use PDOException; use PDOStatement; use Psr\Log\LoggerInterface; -use RuntimeException; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -28,38 +28,26 @@ final class PDOAdapter implements StorageInterface ]; private PDO|null $pdo = null; - private string|null $driver = null; private bool $viaCommit = false; - private PDOStatement|null $stmtInsert = null; - private PDOStatement|null $stmtUpdate = null; - private PDOStatement|null $stmtDelete = null; + /** + * Cache Prepared Statements. + * + * @var array + */ + private array $stmt = [ + 'insert' => null, + 'update' => null, + ]; public function __construct(private LoggerInterface $logger) { } - public function getAll(DateTimeInterface|null $date = null): array - { - $arr = []; - - $sql = "SELECT * FROM state"; - - if (null !== $date) { - $sql .= ' WHERE updated > ' . $date->getTimestamp(); - } - - foreach ($this->pdo->query($sql) as $row) { - $arr[] = Container::get(StateInterface::class)::fromArray($row); - } - - return $arr; - } - public function setUp(array $opts): StorageInterface { if (null === ($opts['dsn'] ?? null)) { - throw new RuntimeException('No storage.opts.dsn (Data Source Name) was provided.'); + throw new StorageException('No storage.opts.dsn (Data Source Name) was provided.', 10); } $this->pdo = new PDO( @@ -75,13 +63,13 @@ final class PDOAdapter implements StorageInterface ) ); - $this->driver = $this->getDriver(); + $driver = $this->getDriver(); - if (!in_array($this->driver, $this->supported)) { - throw new RuntimeException(sprintf('%s Driver is not supported.', $this->driver)); + if (!in_array($driver, $this->supported)) { + throw new StorageException(sprintf('%s Driver is not supported.', $driver), 11); } - if (null !== ($exec = ag($opts, "exec.{$this->driver}")) && is_array($exec)) { + if (null !== ($exec = ag($opts, "exec.{$driver}")) && is_array($exec)) { foreach ($exec as $cmd) { $this->pdo->exec($cmd); } @@ -90,17 +78,10 @@ final class PDOAdapter implements StorageInterface return $this; } - public function setLogger(LoggerInterface $logger): StorageInterface - { - $this->logger = $logger; - - return $this; - } - public function insert(StateInterface $entity): StateInterface { if (null === $this->pdo) { - throw new RuntimeException('Setup(): method was not called.'); + throw new StorageException('Setup(): method was not called.', StorageException::SETUP_NOT_CALLED); } try { @@ -111,24 +92,24 @@ final class PDOAdapter implements StorageInterface } if (null !== $data['id']) { - throw new RuntimeException( - sprintf('Trying to insert already saved entity #%s', $data['id']) + throw new StorageException( + sprintf('Trying to insert already saved entity #%s', $data['id']), 21 ); } unset($data['id']); - if (null === $this->stmtInsert) { - $this->stmtInsert = $this->pdo->prepare( - $this->pdoInsert('state', array_keys($data)) + if (null === ($this->stmt['insert'] ?? null)) { + $this->stmt['insert'] = $this->pdo->prepare( + $this->pdoInsert('state', StateInterface::ENTITY_KEYS) ); } - $this->stmtInsert->execute($data); + $this->stmt['insert']->execute($data); $entity->id = (int)$this->pdo->lastInsertId(); } catch (PDOException $e) { - $this->stmtInsert = null; + $this->stmt['insert'] = null; if (false === $this->viaCommit) { $this->logger->error($e->getMessage(), $entity->meta ?? []); return $entity; @@ -136,13 +117,56 @@ final class PDOAdapter implements StorageInterface throw $e; } - return $entity; + return $entity->updateOriginal(); + } + + public function get(StateInterface $entity): StateInterface|null + { + if (null === $this->pdo) { + throw new StorageException('Setup(): method was not called.', StorageException::SETUP_NOT_CALLED); + } + + $arr = array_intersect_key( + $entity->getAll(), + array_flip(StateInterface::ENTITY_GUIDS) + ); + + if (null !== $entity->id) { + $arr['id'] = $entity->id; + } + + return $this->matchAnyId($arr, $entity); + } + + public function getAll(DateTimeInterface|null $date = null, StateInterface|null $class = null): array + { + if (null === $this->pdo) { + throw new StorageException('Setup(): method was not called.', StorageException::SETUP_NOT_CALLED); + } + + $arr = []; + + $sql = 'SELECT * FROM state'; + + if (null !== $date) { + $sql .= ' WHERE updated > ' . $date->getTimestamp(); + } + + if (null === $class) { + $class = Container::get(StateInterface::class); + } + + foreach ($this->pdo->query($sql) as $row) { + $arr[] = $class::fromArray($row); + } + + return $arr; } public function update(StateInterface $entity): StateInterface { if (null === $this->pdo) { - throw new RuntimeException('Setup(): method was not called.'); + throw new StorageException('Setup(): method was not called.', StorageException::SETUP_NOT_CALLED); } try { @@ -153,16 +177,18 @@ final class PDOAdapter implements StorageInterface } if (null === $data['id']) { - throw new RuntimeException('Trying to update unsaved entity'); + throw new StorageException('Trying to update unsaved entity', 51); } - if (null === $this->stmtUpdate) { - $this->stmtUpdate = $this->pdo->prepare($this->pdoUpdate('state', array_keys($data))); + if (null === ($this->stmt['update'] ?? null)) { + $this->stmt['update'] = $this->pdo->prepare( + $this->pdoUpdate('state', StateInterface::ENTITY_KEYS) + ); } - $this->stmtUpdate->execute($data); + $this->stmt['update']->execute($data); } catch (PDOException $e) { - $this->stmtUpdate = null; + $this->stmt['update'] = null; if (false === $this->viaCommit) { $this->logger->error($e->getMessage(), $entity->meta ?? []); return $entity; @@ -170,31 +196,35 @@ final class PDOAdapter implements StorageInterface throw $e; } - return $entity; + return $entity->updateOriginal(); } - public function get(StateInterface $entity): StateInterface|null + public function matchAnyId(array $ids, StateInterface|null $class = null): StateInterface|null { if (null === $this->pdo) { - throw new RuntimeException('Setup(): method was not called.'); + throw new StorageException('Setup(): method was not called.', StorageException::SETUP_NOT_CALLED); } - if (null !== $entity->id) { - $stmt = $this->pdo->query("SELECT * FROM state WHERE id = " . (int)$entity->id); + if (null === $class) { + $class = Container::get(StateInterface::class); + } + + if (null !== ($ids['id'] ?? null)) { + $stmt = $this->pdo->query("SELECT * FROM state WHERE id = " . (int)$ids['id']); if (false === ($row = $stmt->fetch(PDO::FETCH_ASSOC))) { return null; } - return Container::get(StateInterface::class)::fromArray($row); + return $class::fromArray($row); } $cond = $where = []; foreach (StateInterface::ENTITY_GUIDS as $key) { - if (null === $entity->{$key}) { + if (null === ($ids[$key] ?? null)) { continue; } - $cond[$key] = $entity->{$key}; + $cond[$key] = $ids[$key]; } if (empty($cond)) { @@ -202,106 +232,47 @@ final class PDOAdapter implements StorageInterface } foreach ($cond as $key => $_) { - $where[] = $this->escapeIdentifier($key) . ' = :' . $key; + $where[] = $key . ' = :' . $key; } $sqlWhere = implode(' OR ', $where); - $stmt = $this->pdo->prepare( - sprintf( - "SELECT * FROM %s WHERE %s LIMIT 1", - $this->escapeIdentifier('state'), - $sqlWhere - ) - ); + $cachedKey = md5($sqlWhere); - if (false === $stmt->execute($cond)) { - throw new RuntimeException('Unable to prepare sql statement'); - } + try { + if (null === ($this->stmt[$cachedKey] ?? null)) { + $this->stmt[$cachedKey] = $this->pdo->prepare("SELECT * FROM state WHERE {$sqlWhere}"); + } - if (false === ($row = $stmt->fetch(PDO::FETCH_ASSOC))) { - return null; - } + if (false === $this->stmt[$cachedKey]->execute($cond)) { + $this->stmt[$cachedKey] = null; + throw new StorageException('Failed to execute sql query.', 61); + } - return Container::get(StateInterface::class)::fromArray($row); - } - - public function matchAnyId(array $ids): StateInterface|null - { - if (null === $this->pdo) { - throw new RuntimeException('Setup(): method was not called.'); - } - - if (null !== ($ids['id'] ?? null)) { - $stmt = $this->pdo->prepare( - sprintf( - 'SELECT * FROM %s WHERE %s = :id LIMIT 1', - $this->escapeIdentifier('state'), - $this->escapeIdentifier('id'), - ) - ); - - if (false === ($stmt->execute(['id' => $ids['id']]))) { + if (false === ($row = $this->stmt[$cachedKey]->fetch(PDO::FETCH_ASSOC))) { return null; } - if (false === ($row = $stmt->fetch(PDO::FETCH_ASSOC))) { - return null; - } - - return Container::get(StateInterface::class)::fromArray($row); + return $class::fromArray($row); + } catch (PDOException|StorageException $e) { + $this->stmt[$cachedKey] = null; + throw $e; } - - $cond = $where = []; - - foreach ($ids as $_val) { - if (null === $_val || !str_starts_with($_val, 'guid_')) { - continue; - } - - [$key, $val] = explode('://', $_val); - - $cond[$key] = $val; - } - - if (empty($cond)) { - return null; - } - - foreach ($cond as $key => $_) { - $where[] = $this->escapeIdentifier($key) . ' = :' . $key; - } - - $sqlWhere = implode(' OR ', $where); - - $stmt = $this->pdo->prepare( - sprintf( - "SELECT * FROM %s WHERE %s LIMIT 1", - $this->escapeIdentifier('state'), - $sqlWhere - ) - ); - - if (false === $stmt->execute($cond)) { - throw new RuntimeException('Unable to prepare sql statement'); - } - - if (false === ($row = $stmt->fetch(PDO::FETCH_ASSOC))) { - return null; - } - - return Container::get(StateInterface::class)::fromArray($row); } public function remove(StateInterface $entity): bool { + if (null === $this->pdo) { + throw new StorageException('Setup(): method was not called.', StorageException::SETUP_NOT_CALLED); + } + if (null === $entity->id && !$entity->hasGuids()) { return false; } try { if (null === $entity->id) { - if (null === $dbEntity = $this->get($entity)) { + if (null === ($dbEntity = $this->get($entity))) { return false; } $id = $dbEntity->id; @@ -309,20 +280,9 @@ final class PDOAdapter implements StorageInterface $id = $entity->id; } - if (null === $this->stmtDelete) { - $this->stmtDelete = $this->pdo->prepare( - sprintf( - 'DELETE FROM %s WHERE %s = :id', - $this->escapeIdentifier('state'), - $this->escapeIdentifier('id'), - ) - ); - } - - $this->stmtDelete->execute(['id' => $id]); + $this->pdo->query('DELETE FROM state WHERE id = ' . (int)$id); } catch (PDOException $e) { $this->logger->error($e->getMessage()); - $this->stmtDelete = null; return false; } @@ -332,7 +292,7 @@ final class PDOAdapter implements StorageInterface public function commit(array $entities): array { if (null === $this->pdo) { - throw new RuntimeException('Setup(): method was not called.'); + throw new StorageException('Setup(): method was not called.', StorageException::SETUP_NOT_CALLED); } return $this->transactional(function () use ($entities) { @@ -377,6 +337,45 @@ final class PDOAdapter implements StorageInterface }); } + public function migrations(string $dir, InputInterface $input, OutputInterface $output, array $opts = []): mixed + { + if (null === $this->pdo) { + throw new StorageException('Setup(): method was not called.', StorageException::SETUP_NOT_CALLED); + } + + $class = new PDOMigrations($this->pdo); + + return match (strtolower($dir)) { + StorageInterface::MIGRATE_UP => $class->up($input, $output), + StorageInterface::MIGRATE_DOWN => $class->down($output), + default => throw new StorageException(sprintf('Unknown direction \'%s\' was given.', $dir), 91), + }; + } + + /** + * @throws Exception + */ + public function makeMigration(string $name, OutputInterface $output, array $opts = []): mixed + { + if (null === $this->pdo) { + throw new StorageException('Setup(): method was not called.'); + } + + return (new PDOMigrations($this->pdo))->make($name, $output); + } + + public function maintenance(InputInterface $input, OutputInterface $output, array $opts = []): mixed + { + return (new PDOMigrations($this->pdo))->runMaintenance(); + } + + public function setLogger(LoggerInterface $logger): StorageInterface + { + $this->logger = $logger; + + return $this; + } + /** * Wrap Transaction. * @@ -409,6 +408,22 @@ final class PDOAdapter implements StorageInterface } } + /** + * Get PDO Driver. + * + * @return string + */ + private function getDriver(): string + { + $driver = $this->pdo->getAttribute($this->pdo::ATTR_DRIVER_NAME); + + if (empty($driver) || !is_string($driver)) { + $driver = 'unknown'; + } + + return strtolower($driver); + } + /** * Generate SQL Insert Statement. * @@ -418,7 +433,7 @@ final class PDOAdapter implements StorageInterface */ private function pdoInsert(string $table, array $columns): string { - $queryString = 'INSERT INTO ' . $this->escapeIdentifier($table) . ' (%{columns}) VALUES(%{values})'; + $queryString = "INSERT INTO {$table} (%(columns)) VALUES(%(values))"; $sql_columns = $sql_placeholder = []; @@ -427,12 +442,12 @@ final class PDOAdapter implements StorageInterface continue; } - $sql_columns[] = $this->escapeIdentifier($column, true); - $sql_placeholder[] = ':' . $this->escapeIdentifier($column, false); + $sql_columns[] = $column; + $sql_placeholder[] = ':' . $column; } $queryString = str_replace( - ['%{columns}', '%{values}'], + ['%(columns)', '%(values)'], [implode(', ', $sql_columns), implode(', ', $sql_placeholder)], $queryString ); @@ -449,11 +464,8 @@ final class PDOAdapter implements StorageInterface */ private function pdoUpdate(string $table, array $columns): string { - $queryString = sprintf( - 'UPDATE %s SET ${place} = ${holder} WHERE %s = :id', - $this->escapeIdentifier($table, true), - $this->escapeIdentifier('id', true) - ); + /** @noinspection SqlWithoutWhere */ + $queryString = "UPDATE {$table} SET %(place) = %(holder) WHERE id = :id"; $placeholders = []; @@ -461,95 +473,14 @@ final class PDOAdapter implements StorageInterface if ('id' === $column) { continue; } - $placeholders[] = sprintf( - '%1$s = :%2$s', - $this->escapeIdentifier($column, true), - $this->escapeIdentifier($column, false) - ); + $placeholders[] = sprintf('%1$s = :%1$s', $column); } - return trim(str_replace('${place} = ${holder}', implode(', ', $placeholders), $queryString)); - } - - private function escapeIdentifier(string $text, bool $quote = true): string - { - // table or column has to be valid ASCII name. - // this is opinionated, but we only allow [a-zA-Z0-9_] in column/table name. - if (!preg_match('#\w#', $text)) { - throw new RuntimeException( - sprintf( - 'Invalid identifier "%s": Column/table must be valid ASCII code.', - $text - ) - ); - } - - // The first character cannot be [0-9]: - if (preg_match('/^\d/', $text)) { - throw new RuntimeException( - sprintf( - 'Invalid identifier "%s": Must begin with a letter or underscore.', - $text - ) - ); - } - - if (!$quote) { - return $text; - } - - return match ($this->driver) { - 'mssql' => '[' . $text . ']', - 'mysql' => '`' . $text . '`', - default => '"' . $text . '"', - }; + return trim(str_replace('%(place) = %(holder)', implode(', ', $placeholders), $queryString)); } public function __destruct() { - $this->stmtDelete = $this->stmtUpdate = $this->stmtInsert = null; - } - - public function migrations(string $dir, InputInterface $input, OutputInterface $output, array $opts = []): mixed - { - if (null === $this->pdo) { - throw new RuntimeException('Setup(): method was not called.'); - } - - $class = new PDOMigrations($this->pdo); - - return match ($dir) { - StorageInterface::MIGRATE_UP => $class->up($input, $output), - StorageInterface::MIGRATE_DOWN => $class->down($output), - default => throw new RuntimeException(sprintf('Unknown direction \'%s\' was given.', $dir)), - }; - } - - /** - * @throws Exception - */ - public function makeMigration(string $name, OutputInterface $output, array $opts = []): void - { - if (null === $this->pdo) { - throw new RuntimeException('Setup(): method was not called.'); - } - - (new PDOMigrations($this->pdo))->make($name, $output); - } - - public function maintenance(InputInterface $input, OutputInterface $output, array $opts = []): mixed - { - return (new PDOMigrations($this->pdo))->runMaintenance(); - } - - private function getDriver(): string - { - $driver = $this->pdo->getAttribute($this->pdo::ATTR_DRIVER_NAME); - - if (empty($driver) || !is_string($driver)) { - $driver = 'unknown'; - } - - return strtolower($driver); + $this->stmt = []; } } diff --git a/src/Libs/Storage/StorageException.php b/src/Libs/Storage/StorageException.php new file mode 100644 index 00000000..9491088e --- /dev/null +++ b/src/Libs/Storage/StorageException.php @@ -0,0 +1,12 @@ + + */ + public function getAll(DateTimeInterface|null $date = null, StateInterface|null $class = null): array; + /** * Update Entity immediately. * @@ -52,13 +63,14 @@ interface StorageInterface public function update(StateInterface $entity): StateInterface; /** - * Get Entity. + * Get Entity Using array of ids. * - * @param StateInterface $entity + * @param array $ids + * @param StateInterface|null $class Create object based on given class, if null use default class. * * @return StateInterface|null */ - public function get(StateInterface $entity): StateInterface|null; + public function matchAnyId(array $ids, StateInterface|null $class = null): StateInterface|null; /** * Remove Entity. @@ -78,24 +90,6 @@ interface StorageInterface */ public function commit(array $entities): array; - /** - * Load All Entities From backend. - * - * @param DateTimeInterface|null $date Get Entities That has changed since given time. - * - * @return array - */ - public function getAll(DateTimeInterface|null $date = null): array; - - /** - * Get Entity Using array of ids. - * - * @param array $ids - * - * @return StateInterface|null - */ - public function matchAnyId(array $ids): StateInterface|null; - /** * Migrate Backend Storage Schema. * @@ -124,6 +118,17 @@ interface StorageInterface * @param string $name * @param OutputInterface $output * @param array $opts + * + * @return mixed can return migration file name in supported cases. */ - public function makeMigration(string $name, OutputInterface $output, array $opts = []): void; + public function makeMigration(string $name, OutputInterface $output, array $opts = []): mixed; + + /** + * Inject Logger. + * + * @param LoggerInterface $logger + * @return $this + */ + public function setLogger(LoggerInterface $logger): self; + } diff --git a/src/Libs/helpers.php b/src/Libs/helpers.php index ef19502a..db9b5378 100644 --- a/src/Libs/helpers.php +++ b/src/Libs/helpers.php @@ -3,10 +3,15 @@ declare(strict_types=1); use App\Libs\Config; +use App\Libs\Container; use App\Libs\Extends\Date; +use App\Libs\HttpException; +use App\Libs\Servers\ServerInterface; +use App\Libs\Storage\StorageInterface; use Nyholm\Psr7\Response; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; +use Psr\Log\LoggerInterface; use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; use Symfony\Contracts\HttpClient\ResponseStreamInterface; @@ -112,7 +117,7 @@ if (!function_exists('ag_set')) { if (is_array($at)) { $at[array_shift($keys)] = $value; } else { - throw new RuntimeException("Can not set value at this path ($path) because is not array."); + throw new RuntimeException("Can not set value at this path ($path) because its not array."); } } else { $path = array_shift($keys); @@ -268,3 +273,87 @@ if (!function_exists('httpClientChunks')) { } } } + +if (!function_exists('serveHttpRequest')) { + /** + * @throws ReflectionException + */ + function serveHttpRequest(ServerRequestInterface $request): ResponseInterface + { + try { + if (true !== Config::get('webhook.enabled', false)) { + throw new HttpException('Webhook is disabled via config.', 500); + } + + if (null === Config::get('webhook.apikey', null)) { + throw new HttpException('No webhook.apikey is set in config.', 500); + } + + // -- get apikey from header or query. + $apikey = $request->getHeaderLine('x-apikey'); + if (empty($apikey)) { + $apikey = ag($request->getQueryParams(), 'apikey', ''); + if (empty($apikey)) { + throw new HttpException('No API key was given.', 400); + } + } + + if (!hash_equals(Config::get('webhook.apikey'), $apikey)) { + throw new HttpException('Invalid API key was given.', 401); + } + + if (null === ($type = ag($request->getQueryParams(), 'type', null))) { + throw new HttpException('No type was given via type= query.', 400); + } + + $types = Config::get('supported', []); + + if (null === ($backend = ag($types, $type))) { + throw new HttpException('Invalid server type was given.', 400); + } + + $class = new ReflectionClass($backend); + + if (!$class->implementsInterface(ServerInterface::class)) { + throw new HttpException('Invalid Parser Class.', 500); + } + + /** @var ServerInterface $backend */ + $entity = $backend::parseWebhook($request); + + if (null === $entity || !$entity->hasGuids()) { + return new Response(status: 200, headers: ['X-Status' => 'No GUIDs.']); + } + + $storage = Container::get(StorageInterface::class); + + if (null === ($backend = $storage->get($entity))) { + $storage->insert($entity); + return jsonResponse(status: 200, body: $entity->getAll()); + } + + if ($backend->updated > $entity->updated) { + return new Response( + status: 200, + headers: ['X-Status' => 'Entity date is older than what available in storage.'] + ); + } + + if ($backend->apply($entity)->isChanged()) { + $backend = $storage->update($backend); + + return jsonResponse(status: 200, body: $backend->getAll()); + } + + return new Response(status: 200, headers: ['X-Status' => 'Entity is unchanged.']); + } catch (HttpException $e) { + Container::get(LoggerInterface::class)->error($e->getMessage()); + + if (200 === $e->getCode()) { + return new Response(status: $e->getCode(), headers: ['X-Status' => $e->getMessage()]); + } + + return jsonResponse(status: $e->getCode(), body: ['error' => true, 'message' => $e->getMessage()]); + } + } +} diff --git a/tests/Storage/PDOAdapterTest.php b/tests/Storage/PDOAdapterTest.php new file mode 100644 index 00000000..e9303b2c --- /dev/null +++ b/tests/Storage/PDOAdapterTest.php @@ -0,0 +1,313 @@ + null, + 'type' => StateInterface::TYPE_EPISODE, + 'updated' => 0, + 'watched' => 1, + 'meta' => [], + 'guid_plex' => StateInterface::TYPE_EPISODE . '/1', + 'guid_imdb' => StateInterface::TYPE_EPISODE . '/2', + 'guid_tvdb' => StateInterface::TYPE_EPISODE . '/3', + 'guid_tmdb' => StateInterface::TYPE_EPISODE . '/4', + 'guid_tvmaze' => StateInterface::TYPE_EPISODE . '/5', + 'guid_tvrage' => StateInterface::TYPE_EPISODE . '/6', + 'guid_anidb' => StateInterface::TYPE_EPISODE . '/7', + ]; + + private array $testMovie = [ + 'id' => null, + 'type' => StateInterface::TYPE_MOVIE, + 'updated' => 1, + 'watched' => 1, + 'meta' => [], + 'guid_plex' => StateInterface::TYPE_MOVIE . '/10', + 'guid_imdb' => StateInterface::TYPE_MOVIE . '/20', + 'guid_tvdb' => StateInterface::TYPE_MOVIE . '/30', + 'guid_tmdb' => StateInterface::TYPE_MOVIE . '/40', + 'guid_tvmaze' => StateInterface::TYPE_MOVIE . '/50', + 'guid_tvrage' => StateInterface::TYPE_MOVIE . '/60', + 'guid_anidb' => StateInterface::TYPE_MOVIE . '/70', + ]; + + private StorageInterface|null $storage = null; + + public function setUp(): void + { + $this->output = new NullOutput(); + $this->input = new ArrayInput([]); + + $this->storage = new PDOAdapter(new CliLogger($this->output)); + $this->storage->setUp(['dsn' => 'sqlite::memory:']); + $this->storage->migrations('up', $this->input, $this->output); + } + + /** StorageInterface::setUp */ + public function test_setup_throw_exception_if_no_dsn(): void + { + $this->expectException(StorageException::class); + $this->expectExceptionCode(10); + $storage = new PDOAdapter(new CliLogger($this->output)); + $storage->setUp([]); + } + + public function test_setup_throw_exception_if_invalid_dsn(): void + { + $this->expectException(PDOException::class); + $storage = new PDOAdapter(new CliLogger($this->output)); + $storage->setUp(['dsn' => 'not_real_driver::foo']); + } + + /** StorageInterface::insert */ + public function test_insert_call_without_setup_exception(): void + { + $this->expectException(StorageException::class); + $this->expectExceptionCode(StorageException::SETUP_NOT_CALLED); + $storage = new PDOAdapter(new CliLogger($this->output)); + + $storage->insert(new StateEntity([])); + } + + public function test_insert_throw_exception_if_has_id(): void + { + $this->expectException(StorageException::class); + $this->expectExceptionCode(21); + $item = new StateEntity($this->testEpisode); + $this->storage->insert($item); + $this->storage->insert($item); + } + + public function test_insert_successful(): void + { + $item = $this->storage->insert(new StateEntity($this->testEpisode)); + $this->assertSame(1, $item->id); + } + + /** StorageInterface::get */ + public function test_get_call_without_setup_exception(): void + { + $this->expectException(StorageException::class); + $this->expectExceptionCode(StorageException::SETUP_NOT_CALLED); + $storage = new PDOAdapter(new CliLogger($this->output)); + $storage->get(new StateEntity([])); + } + + public function test_get_conditions(): void + { + $item = new StateEntity($this->testEpisode); + + // -- db should be empty at this stage. as such we expect null. + $this->assertNull($this->storage->get($item)); + + // -- insert and return object and assert it's the same + $modified = $this->storage->insert(clone $item); + + $this->assertSame($modified->getAll(), $this->storage->get($item)->getAll()); + + // -- look up based on id + $this->assertSame($modified->getAll(), $this->storage->get($modified)->getAll()); + } + + /** StorageInterface::getAll */ + public function test_getAll_call_without_setup_exception(): void + { + $this->expectException(StorageException::class); + $this->expectExceptionCode(StorageException::SETUP_NOT_CALLED); + $storage = new PDOAdapter(new CliLogger($this->output)); + $storage->getAll(); + } + + public function test_getAll_call_without_initialized_container(): void + { + $this->expectException(Error::class); + $this->expectExceptionMessage('Call to a member function'); + $this->storage->getAll(); + } + + public function test_getAll_conditions(): void + { + $item = new StateEntity($this->testEpisode); + + $this->assertSame([], $this->storage->getAll(class: $item)); + + $this->storage->insert($item); + + $this->assertCount(1, $this->storage->getAll(class: $item)); + + // -- future date should be 0. + $this->assertCount(0, $this->storage->getAll(date: new DateTimeImmutable('now'), class: $item)); + } + + /** StorageInterface::update */ + public function test_update_call_without_setup_exception(): void + { + $this->expectException(StorageException::class); + $this->expectExceptionCode(StorageException::SETUP_NOT_CALLED); + $storage = new PDOAdapter(new CliLogger($this->output)); + $storage->update(new StateEntity([])); + } + + public function test_update_call_without_id_exception(): void + { + $this->expectException(StorageException::class); + $this->expectExceptionCode(51); + $item = new StateEntity($this->testEpisode); + + $this->storage->update($item); + } + + public function test_update_conditions(): void + { + $item = $this->storage->insert(new StateEntity($this->testEpisode)); + $item->guid_plex = StateInterface::TYPE_EPISODE . '/1000'; + + $updatedItem = $this->storage->update($item); + + $this->assertSame($item, $updatedItem); + $this->assertSame($updatedItem->getAll(), $this->storage->get($item)->getAll()); + } + + /** StorageInterface::update */ + public function test_matchAnyId_call_without_setup_exception(): void + { + $this->expectException(StorageException::class); + $this->expectExceptionCode(StorageException::SETUP_NOT_CALLED); + $storage = new PDOAdapter(new CliLogger($this->output)); + $storage->matchAnyId([]); + } + + public function test_matchAnyId_call_without_initialized_container(): void + { + $this->expectException(Error::class); + $this->expectExceptionMessage('Call to a member function'); + $this->storage->matchAnyId([]); + } + + public function test_matchAnyId_conditions(): void + { + $item1 = new StateEntity($this->testEpisode); + $item2 = new StateEntity($this->testMovie); + + $this->assertNull( + $this->storage->matchAnyId( + array_intersect_key($item1->getAll(), array_flip(StateInterface::ENTITY_GUIDS)), + $item1 + ) + ); + + $newItem1 = $this->storage->insert($item1); + $newItem2 = $this->storage->insert($item2); + + $this->assertSame( + $newItem1->getAll(), + $this->storage->matchAnyId( + array_intersect_key($item1->getAll(), array_flip(StateInterface::ENTITY_GUIDS)), + $item1 + )->getAll() + ); + + $this->assertSame( + $newItem2->getAll(), + $this->storage->matchAnyId( + array_intersect_key($item2->getAll(), array_flip(StateInterface::ENTITY_GUIDS)), + $item2 + )->getAll() + ); + } + + /** StorageInterface::remove */ + public function test_remove_call_without_setup_exception(): void + { + $this->expectException(StorageException::class); + $this->expectExceptionCode(StorageException::SETUP_NOT_CALLED); + $storage = new PDOAdapter(new CliLogger($this->output)); + $storage->remove(new StateEntity([])); + } + + public function test_remove_conditions(): void + { + $item1 = new StateEntity($this->testEpisode); + $item2 = new StateEntity($this->testMovie); + $item3 = new StateEntity([]); + + $this->assertFalse($this->storage->remove($item1)); + + $item1 = $this->storage->insert($item1); + $this->storage->insert($item2); + + $this->assertTrue($this->storage->remove($item1)); + $this->assertInstanceOf(StateInterface::class, $this->storage->get($item2)); + + // -- remove without id pointer. + $this->assertTrue($this->storage->remove($item2)); + $this->assertFalse($this->storage->remove($item3)); + } + + /** StorageInterface::commit */ + public function test_commit_call_without_setup_exception(): void + { + $this->expectException(StorageException::class); + $this->expectExceptionCode(StorageException::SETUP_NOT_CALLED); + $storage = new PDOAdapter(new CliLogger($this->output)); + $storage->commit([]); + } + + public function test_commit_conditions(): void + { + $item1 = new StateEntity($this->testEpisode); + $item2 = new StateEntity($this->testMovie); + + $this->assertSame( + [ + StateInterface::TYPE_MOVIE => ['added' => 1, 'updated' => 0, 'failed' => 0], + StateInterface::TYPE_EPISODE => ['added' => 1, 'updated' => 0, 'failed' => 0], + ], + $this->storage->commit([$item1, $item2]) + ); + + $item1->guid_anidb = StateInterface::TYPE_EPISODE . '/1'; + $item2->guid_anidb = StateInterface::TYPE_MOVIE . '/1'; + + $this->assertSame( + [ + StateInterface::TYPE_MOVIE => ['added' => 0, 'updated' => 1, 'failed' => 0], + StateInterface::TYPE_EPISODE => ['added' => 0, 'updated' => 1, 'failed' => 0], + ], + $this->storage->commit([$item1, $item2]) + ); + } + + /** StorageInterface::migrations */ + public function test_migrations_call_without_setup_exception(): void + { + $this->expectException(StorageException::class); + $this->expectExceptionCode(StorageException::SETUP_NOT_CALLED); + $storage = new PDOAdapter(new CliLogger($this->output)); + $storage->migrations('f', new ArrayInput([]), new NullOutput()); + } + public function test_migrations_call_with_wrong_direction_exception(): void + { + $this->expectException(StorageException::class); + $this->expectExceptionCode(91); + $this->storage->migrations('not_dd', new ArrayInput([]), new NullOutput()); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 00000000..9f2d88bd --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,15 @@ +