From 50e7a4b1c6a712aedf0c7a72cc9cfcce97bb831d Mon Sep 17 00:00:00 2001 From: amsprost Date: Fri, 11 Sep 2020 01:00:47 +0200 Subject: [PATCH 01/13] Tests env setup, Table archiver base class --- .env.test | 4 + .gitignore | 11 + Dockerfile | 2 +- bin/phpunit | 13 + composer.json | 5 +- composer.lock | 1969 ++++++++++++++++++++++- phpunit.xml.dist | 27 + src/Manager/TableArchiver.php | 62 + symfony.lock | 125 ++ tests/Manager/TableArchiverTest.php | 37 + tests/TestHelpers/DbSetupAwareTrait.php | 71 + tests/bootstrap.php | 5 + 12 files changed, 2325 insertions(+), 6 deletions(-) create mode 100644 .env.test create mode 100755 bin/phpunit create mode 100644 phpunit.xml.dist create mode 100644 src/Manager/TableArchiver.php create mode 100644 tests/Manager/TableArchiverTest.php create mode 100644 tests/TestHelpers/DbSetupAwareTrait.php create mode 100755 tests/bootstrap.php diff --git a/.env.test b/.env.test new file mode 100644 index 0000000..24a43c0 --- /dev/null +++ b/.env.test @@ -0,0 +1,4 @@ +# define your env variables for the test env here +KERNEL_CLASS='App\Kernel' +APP_SECRET='$ecretf0rt3st' +SYMFONY_DEPRECATIONS_HELPER=999999 diff --git a/.gitignore b/.gitignore index 4d7c717..5fbbc72 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,14 @@ /vendor/ /.idea ###< symfony/framework-bundle ### + +###> phpunit/phpunit ### +/phpunit.xml +.phpunit.result.cache +###< phpunit/phpunit ### + +###> symfony/phpunit-bridge ### +.phpunit +.phpunit.result.cache +/phpunit.xml +###< symfony/phpunit-bridge ### diff --git a/Dockerfile b/Dockerfile index 91fc110..5483829 100755 --- a/Dockerfile +++ b/Dockerfile @@ -9,7 +9,7 @@ RUN apt-get update && \ rm -rf /var/lib/apt/lists/* && \ mkdir /app -RUN docker-php-ext-install opcache && docker-php-ext-enable opcache +RUN docker-php-ext-install opcache mysqli pdo pdo_mysql && docker-php-ext-enable opcache mysqli pdo pdo_mysql RUN curl -sSL https://github.com/krakjoe/parallel/archive/develop.zip -o /tmp/parallel.zip \ && unzip /tmp/parallel.zip -d /tmp \ diff --git a/bin/phpunit b/bin/phpunit new file mode 100755 index 0000000..4d1ed05 --- /dev/null +++ b/bin/phpunit @@ -0,0 +1,13 @@ +#!/usr/bin/env 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" + ], + "time": "2020-08-30T16:15:20+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/85265efd3af7ba3ca4b2a2c34dbfc5788dd29133", + "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133", + "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)", + "time": "2020-06-27T14:33:11+00:00" + }, + { + "name": "phar-io/version", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "c6bb6825def89e0a32220f88337f8ceaf1975fa0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/c6bb6825def89e0a32220f88337f8ceaf1975fa0", + "reference": "c6bb6825def89e0a32220f88337f8ceaf1975fa0", + "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", + "time": "2020-06-27T14:39:04+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" + ], + "time": "2020-06-27T09:03:43+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "5.2.1", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "d870572532cd70bc3fab58f2e23ad423c8404c44" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/d870572532cd70bc3fab58f2e23ad423c8404c44", + "reference": "d870572532cd70bc3fab58f2e23ad423c8404c44", + "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" + }, + "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.", + "time": "2020-08-15T11:14:08+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "e878a14a65245fbe78f8080eba03b47c3b705651" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/e878a14a65245fbe78f8080eba03b47c3b705651", + "reference": "e878a14a65245fbe78f8080eba03b47c3b705651", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "phpdocumentor/reflection-common": "^2.0" + }, + "require-dev": { + "ext-tokenizer": "*" + }, + "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", + "time": "2020-06-27T10:12:23+00:00" + }, + { + "name": "phpspec/prophecy", + "version": "1.11.1", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy.git", + "reference": "b20034be5efcdab4fb60ca3a29cba2949aead160" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/b20034be5efcdab4fb60ca3a29cba2949aead160", + "reference": "b20034be5efcdab4fb60ca3a29cba2949aead160", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.2", + "php": "^7.2", + "phpdocumentor/reflection-docblock": "^5.0", + "sebastian/comparator": "^3.0 || ^4.0", + "sebastian/recursion-context": "^3.0 || ^4.0" + }, + "require-dev": { + "phpspec/phpspec": "^6.0", + "phpunit/phpunit": "^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.11.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" + ], + "time": "2020-07-08T12:44:21+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "9.1.7", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "2ef92bec3186a827faf7362ff92ae4e8ec2e49d2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2ef92bec3186a827faf7362ff92ae4e8ec2e49d2", + "reference": "2ef92bec3186a827faf7362ff92ae4e8ec2e49d2", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.8", + "php": "^7.3 || ^8.0", + "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", + "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.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": "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" + ], + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-03T07:09:19+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "3.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "25fefc5b19835ca653877fe081644a3f8c1d915e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/25fefc5b19835ca653877fe081644a3f8c1d915e", + "reference": "25fefc5b19835ca653877fe081644a3f8c1d915e", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "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", + "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" + ], + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-07-11T05:18:21+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "3.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "7a85b66acc48cacffdf87dadd3694e7123674298" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/7a85b66acc48cacffdf87dadd3694e7123674298", + "reference": "7a85b66acc48cacffdf87dadd3694e7123674298", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^9.0" + }, + "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" + ], + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-08-06T07:04:15+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "6ff9c8ea4d3212b88fcf74e25e516e2c51c99324" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/6ff9c8ea4d3212b88fcf74e25e516e2c51c99324", + "reference": "6ff9c8ea4d3212b88fcf74e25e516e2c51c99324", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "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" + ], + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T11:55:37+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "cc49734779cbb302bf51a44297dab8c4bbf941e7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/cc49734779cbb302bf51a44297dab8c4bbf941e7", + "reference": "cc49734779cbb302bf51a44297dab8c4bbf941e7", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.2" + }, + "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" + ], + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T11:58:13+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "9.3.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "93d78d8e2a06393a0d0c1ead6fe9984f1af1f88c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/93d78d8e2a06393a0d0c1ead6fe9984f1af1f88c", + "reference": "93d78d8e2a06393a0d0c1ead6fe9984f1af1f88c", + "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.1", + "phar-io/version": "^3.0.2", + "php": "^7.3 || ^8.0", + "phpspec/prophecy": "^1.11.1", + "phpunit/php-code-coverage": "^9.1.5", + "phpunit/php-file-iterator": "^3.0.4", + "phpunit/php-invoker": "^3.1", + "phpunit/php-text-template": "^2.0.2", + "phpunit/php-timer": "^5.0.1", + "sebastian/cli-parser": "^1.0", + "sebastian/code-unit": "^1.0.5", + "sebastian/comparator": "^4.0.3", + "sebastian/diff": "^4.0.2", + "sebastian/environment": "^5.1.2", + "sebastian/exporter": "^4.0.2", + "sebastian/global-state": "^5.0", + "sebastian/object-enumerator": "^4.0.2", + "sebastian/resource-operations": "^3.0.2", + "sebastian/type": "^2.2.1", + "sebastian/version": "^3.0.1" + }, + "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.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ], + "files": [ + "src/Framework/Assert/Functions.php" + ] + }, + "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" + ], + "funding": [ + { + "url": "https://phpunit.de/donate.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-08-27T06:30:58+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "2a4a38c56e62f7295bedb8b1b7439ad523d4ea82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2a4a38c56e62f7295bedb8b1b7439ad523d4ea82", + "reference": "2a4a38c56e62f7295bedb8b1b7439ad523d4ea82", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "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", + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-08-12T10:49:21+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "c1e2df332c905079980b119c4db103117e5e5c90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/c1e2df332c905079980b119c4db103117e5e5c90", + "reference": "c1e2df332c905079980b119c4db103117e5e5c90", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "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", + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T12:50:45+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "ee51f9bb0c6d8a43337055db3120829fa14da819" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ee51f9bb0c6d8a43337055db3120829fa14da819", + "reference": "ee51f9bb0c6d8a43337055db3120829fa14da819", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "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/", + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T12:04:00+00:00" + }, + { + "name": "sebastian/comparator", + "version": "4.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "dcc580eadfaa4e7f9d2cf9ae1922134ea962e14f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/dcc580eadfaa4e7f9d2cf9ae1922134ea962e14f", + "reference": "dcc580eadfaa4e7f9d2cf9ae1922134ea962e14f", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0", + "sebastian/diff": "^4.0", + "sebastian/exporter": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "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" + ], + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T12:05:46+00:00" + }, + { + "name": "sebastian/complexity", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "33fcd6a26656c6546f70871244ecba4b4dced097" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/33fcd6a26656c6546f70871244ecba4b4dced097", + "reference": "33fcd6a26656c6546f70871244ecba4b4dced097", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.7", + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.2" + }, + "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", + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-07-25T14:01:34+00:00" + }, + { + "name": "sebastian/diff", + "version": "4.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "1e90b4cf905a7d06c420b1d2e9d11a4dc8a13113" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/1e90b4cf905a7d06c420b1d2e9d11a4dc8a13113", + "reference": "1e90b4cf905a7d06c420b1d2e9d11a4dc8a13113", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0", + "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" + ], + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-30T04:46:02+00:00" + }, + { + "name": "sebastian/environment", + "version": "5.1.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "0a757cab9d5b7ef49a619f1143e6c9c1bc0fe9d2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/0a757cab9d5b7ef49a619f1143e6c9c1bc0fe9d2", + "reference": "0a757cab9d5b7ef49a619f1143e6c9c1bc0fe9d2", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "suggest": { + "ext-posix": "*" + }, + "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": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T12:07:24+00:00" + }, + { + "name": "sebastian/exporter", + "version": "4.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "571d721db4aec847a0e59690b954af33ebf9f023" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/571d721db4aec847a0e59690b954af33ebf9f023", + "reference": "571d721db4aec847a0e59690b954af33ebf9f023", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^9.2" + }, + "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": "http://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T12:08:55+00:00" + }, + { + "name": "sebastian/global-state", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "22ae663c951bdc39da96603edc3239ed3a299097" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/22ae663c951bdc39da96603edc3239ed3a299097", + "reference": "22ae663c951bdc39da96603edc3239ed3a299097", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0", + "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" + ], + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-08-07T04:09:03+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "e02bf626f404b5daec382a7b8a6a4456e49017e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e02bf626f404b5daec382a7b8a6a4456e49017e5", + "reference": "e02bf626f404b5daec382a7b8a6a4456e49017e5", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.6", + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.2" + }, + "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", + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-07-22T18:33:42+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "4.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "074fed2d0a6d08e1677dd8ce9d32aecb384917b8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/074fed2d0a6d08e1677dd8ce9d32aecb384917b8", + "reference": "074fed2d0a6d08e1677dd8ce9d32aecb384917b8", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "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/", + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T12:11:32+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "127a46f6b057441b201253526f81d5406d6c7840" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/127a46f6b057441b201253526f81d5406d6c7840", + "reference": "127a46f6b057441b201253526f81d5406d6c7840", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "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/", + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T12:12:55+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "4.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "062231bf61d2b9448c4fa5a7643b5e1829c11d63" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/062231bf61d2b9448c4fa5a7643b5e1829c11d63", + "reference": "062231bf61d2b9448c4fa5a7643b5e1829c11d63", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "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", + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T12:14:17+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "0653718a5a629b065e91f774595267f8dc32e213" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0653718a5a629b065e91f774595267f8dc32e213", + "reference": "0653718a5a629b065e91f774595267f8dc32e213", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "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", + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T12:16:22+00:00" + }, + { + "name": "sebastian/type", + "version": "2.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "86991e2b33446cd96e648c18bcdb1e95afb2c05a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/86991e2b33446cd96e648c18bcdb1e95afb2c05a", + "reference": "86991e2b33446cd96e648c18bcdb1e95afb2c05a", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.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": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-07-05T08:31:53+00:00" + }, + { + "name": "sebastian/version", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "626586115d0ed31cb71483be55beb759b5af5a3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/626586115d0ed31cb71483be55beb759b5af5a3c", + "reference": "626586115d0ed31cb71483be55beb759b5af5a3c", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.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", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T12:18:43+00:00" + }, + { + "name": "symfony/phpunit-bridge", + "version": "v5.1.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/phpunit-bridge.git", + "reference": "e7d37c91486a0f9eed58a8c23822e1870ea36db5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/e7d37c91486a0f9eed58a8c23822e1870ea36db5", + "reference": "e7d37c91486a0f9eed58a8c23822e1870ea36db5", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "conflict": { + "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0|<6.4,>=6.0|9.1.2" + }, + "suggest": { + "symfony/error-handler": "For tracking deprecated interfaces usages at runtime with DebugClassLoader" + }, + "bin": [ + "bin/simple-phpunit" + ], + "type": "symfony-bridge", + "extra": { + "branch-alias": { + "dev-master": "5.1-dev" + }, + "thanks": { + "name": "phpunit/phpunit", + "url": "https://github.com/sebastianbergmann/phpunit" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Bridge\\PhpUnit\\": "" + }, + "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": "Symfony PHPUnit Bridge", + "homepage": "https://symfony.com", + "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": "2020-09-01T13:16:17+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "75a63c33a8577608444246075ea0af0d052e452a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/75a63c33a8577608444246075ea0af0d052e452a", + "reference": "75a63c33a8577608444246075ea0af0d052e452a", + "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", + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2020-07-12T23:59:07+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.9.1", + "source": { + "type": "git", + "url": "https://github.com/webmozart/assert.git", + "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozart/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389", + "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0 || ^8.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "phpstan/phpstan": "<0.12.20", + "vimeo/psalm": "<3.9.1" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.36 || ^7.5.13" + }, + "type": "library", + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "time": "2020-07-08T17:02:28+00:00" + } + ], "aliases": [], "minimum-stability": "stable", "stability-flags": [], "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": ">=7.2.5", + "php": ">=7.4", "ext-ctype": "*", - "ext-iconv": "*" + "ext-iconv": "*", + "ext-pdo": "*" }, "platform-dev": [], "plugin-api-version": "1.1.0" diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..214665a --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,27 @@ + + + + + + + + + + + + + tests + + + + + + src + + + diff --git a/src/Manager/TableArchiver.php b/src/Manager/TableArchiver.php new file mode 100644 index 0000000..457ea12 --- /dev/null +++ b/src/Manager/TableArchiver.php @@ -0,0 +1,62 @@ +batchSize = $batchSize; + } + + public function archive( + PDO $pdo, + string $tableName, + int $archiveMode, + string $stampColumnName, + ?DateTimeImmutable $maxStamp + ): void { + $pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false); + + $offset = 0; + $pdo->exec('') + + foreach ( + $pdo->query( + $this->buildQuery($tableName, $stampColumnName, $offset, $this->batchSize, $maxStamp) + ) as $row + ) { + } + + while ($result->) + } + + private function buildQuery( + string $tableName, + string $stampColumnName, + ?int $offset, + ?int $limit, + ?DateTimeImmutable $maxStamp + ): string { + $query = 'SELECT * FROM `%s`'; + $params = [$tableName]; + + if ($maxStamp) { + $query .= ' WHERE `%s` < \'%s\''; + $params = [...$params, $stampColumnName, $maxStamp]; + } + + return sprintf($query, $params); + } +} diff --git a/symfony.lock b/symfony.lock index 69ee4bd..fa0b5a3 100644 --- a/symfony.lock +++ b/symfony.lock @@ -1,7 +1,63 @@ { + "doctrine/instantiator": { + "version": "1.3.1" + }, + "myclabs/deep-copy": { + "version": "1.10.1" + }, + "nikic/php-parser": { + "version": "v4.9.1" + }, + "phar-io/manifest": { + "version": "2.0.1" + }, + "phar-io/version": { + "version": "3.0.2" + }, "php": { "version": "7.4" }, + "phpdocumentor/reflection-common": { + "version": "2.2.0" + }, + "phpdocumentor/reflection-docblock": { + "version": "5.2.1" + }, + "phpdocumentor/type-resolver": { + "version": "1.3.0" + }, + "phpspec/prophecy": { + "version": "1.11.1" + }, + "phpunit/php-code-coverage": { + "version": "9.1.7" + }, + "phpunit/php-file-iterator": { + "version": "3.0.4" + }, + "phpunit/php-invoker": { + "version": "3.1.0" + }, + "phpunit/php-text-template": { + "version": "2.0.2" + }, + "phpunit/php-timer": { + "version": "5.0.1" + }, + "phpunit/phpunit": { + "version": "4.7", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "master", + "version": "4.7", + "ref": "477e1387616f39505ba79715f43f124836020d71" + }, + "files": [ + ".env.test", + "phpunit.xml.dist", + "tests/bootstrap.php" + ] + }, "psr/cache": { "version": "1.0.1" }, @@ -14,6 +70,54 @@ "psr/log": { "version": "1.1.3" }, + "sebastian/cli-parser": { + "version": "1.0.0" + }, + "sebastian/code-unit": { + "version": "1.0.5" + }, + "sebastian/code-unit-reverse-lookup": { + "version": "2.0.2" + }, + "sebastian/comparator": { + "version": "4.0.3" + }, + "sebastian/complexity": { + "version": "2.0.0" + }, + "sebastian/diff": { + "version": "4.0.2" + }, + "sebastian/environment": { + "version": "5.1.2" + }, + "sebastian/exporter": { + "version": "4.0.2" + }, + "sebastian/global-state": { + "version": "5.0.0" + }, + "sebastian/lines-of-code": { + "version": "1.0.0" + }, + "sebastian/object-enumerator": { + "version": "4.0.2" + }, + "sebastian/object-reflector": { + "version": "2.0.2" + }, + "sebastian/recursion-context": { + "version": "4.0.2" + }, + "sebastian/resource-operations": { + "version": "3.0.2" + }, + "sebastian/type": { + "version": "2.2.1" + }, + "sebastian/version": { + "version": "3.0.1" + }, "symfony/cache": { "version": "v5.1.4" }, @@ -96,6 +200,21 @@ "symfony/http-kernel": { "version": "v5.1.4" }, + "symfony/phpunit-bridge": { + "version": "4.3", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "master", + "version": "4.3", + "ref": "6d0e35f749d5f4bfe1f011762875275cd3f9874f" + }, + "files": [ + ".env.test", + "bin/phpunit", + "phpunit.xml.dist", + "tests/bootstrap.php" + ] + }, "symfony/polyfill-intl-grapheme": { "version": "v1.18.1" }, @@ -139,5 +258,11 @@ }, "symfony/yaml": { "version": "v5.1.4" + }, + "theseer/tokenizer": { + "version": "1.2.0" + }, + "webmozart/assert": { + "version": "1.9.1" } } diff --git a/tests/Manager/TableArchiverTest.php b/tests/Manager/TableArchiverTest.php new file mode 100644 index 0000000..fe34393 --- /dev/null +++ b/tests/Manager/TableArchiverTest.php @@ -0,0 +1,37 @@ +pdo = $this->setUpDb(); + $this->manager = new TableArchiverManager(2); + } + + public function test(): void + { + $this->manager->archive( + $this->pdo, + $this->getTableName(), + TableArchiverManager::YEAR, + $this->getTimestampName(), + null + ); + $this->assertTrue(true); + } +} diff --git a/tests/TestHelpers/DbSetupAwareTrait.php b/tests/TestHelpers/DbSetupAwareTrait.php new file mode 100644 index 0000000..f82ac5a --- /dev/null +++ b/tests/TestHelpers/DbSetupAwareTrait.php @@ -0,0 +1,71 @@ +exec( + sprintf( + 'CREATE table %s( + id INT( 11 ) AUTO_INCREMENT PRIMARY KEY, + test VARCHAR( 255 ) NOT NULL, + `%s` TIMESTAMP NOT NULL;', + $this->getTableName(), + $this->getTimestampName() + ) + ); + + $values = implode( + ',', + array_map( + function (array $dbRow): string { + /** @var DateTime $dateTime */ + [$test, $dateTime] = $dbRow; + return sprintf('(\'%s\', \'%s\')', $test, $dateTime->format('Y-m-d H:i:s')); + }, + $this->getDbData() + ) + ); + + $pdo->exec( + sprintf( + 'INSERT INTO %s (test, `%s`) VALUES %s;', + $this->getTableName(), + $this->getTimestampName(), + $values + ) + ); + + return $pdo; + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100755 index 0000000..39548eb --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,5 @@ + Date: Fri, 11 Sep 2020 03:19:50 +0200 Subject: [PATCH 02/13] Parallel POC --- Dockerfile | 2 + composer.json | 3 +- config/services.yaml | 4 ++ src/Factory/ArchiverWorkerFactory.php | 17 +++++++ src/Factory/QueryFactory.php | 51 +++++++++++++++++++ src/Manager/TableArchiver.php | 71 ++++++++++++++++----------- src/Services/ArchiverWorker.php | 15 ++++++ src/Services/Supervisor.php | 50 +++++++++++++++++++ tests/Manager/TableArchiverTest.php | 19 +++++-- 9 files changed, 197 insertions(+), 35 deletions(-) create mode 100644 src/Factory/ArchiverWorkerFactory.php create mode 100644 src/Factory/QueryFactory.php create mode 100644 src/Services/ArchiverWorker.php create mode 100644 src/Services/Supervisor.php diff --git a/Dockerfile b/Dockerfile index 5483829..21672e9 100755 --- a/Dockerfile +++ b/Dockerfile @@ -20,6 +20,8 @@ RUN curl -sSL https://github.com/krakjoe/parallel/archive/develop.zip -o /tmp/pa && make install \ && rm -rf /tmp/parallel* +RUN docker-php-ext-enable parallel + RUN composer global require hirak/prestissimo WORKDIR /app diff --git a/composer.json b/composer.json index 9d52874..9ca9ce3 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,8 @@ "symfony/flex": "^1.3.1", "symfony/framework-bundle": "5.1.*", "symfony/yaml": "5.1.*", - "ext-pdo": "*" + "ext-pdo": "*", + "ext-parallel": "*" }, "require-dev": { "phpunit/phpunit": "^9", diff --git a/config/services.yaml b/config/services.yaml index 1ca8bf8..0b5788d 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -20,3 +20,7 @@ services: - '../src/Entity/' - '../src/Kernel.php' - '../src/Tests/' + + Linkorb\TableArchiver\Services\Supervisor: + arguments: + $workerFactory: !service {factory: [ 'Linkorb\TableArchiver\Factory\ArchiverWorkerFactory', 'createFactoryMethod' ]} diff --git a/src/Factory/ArchiverWorkerFactory.php b/src/Factory/ArchiverWorkerFactory.php new file mode 100644 index 0000000..1bd20ed --- /dev/null +++ b/src/Factory/ArchiverWorkerFactory.php @@ -0,0 +1,17 @@ +queryFactory = $queryFactory; + $this->supervisor = $supervisor; $this->batchSize = $batchSize; } @@ -27,36 +35,41 @@ public function archive( string $stampColumnName, ?DateTimeImmutable $maxStamp ): void { - $pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false); - - $offset = 0; - $pdo->exec('') + $count = $pdo->query( + $this->queryFactory->buildCountQuery($tableName, $stampColumnName, $maxStamp), + PDO::FETCH_COLUMN + ); - foreach ( - $pdo->query( - $this->buildQuery($tableName, $stampColumnName, $offset, $this->batchSize, $maxStamp) - ) as $row - ) { + for ($offset = 0; $offset < $count; $offset += $this->batchSize) { + $this->supervisor->spawn( + [ + $pdo, + $this->queryFactory->buildFetchQuery( + $tableName, + $stampColumnName, + $offset, + $this->batchSize, + $maxStamp + ), + $archiveMode + ] + ); } - while ($result->) - } - - private function buildQuery( - string $tableName, - string $stampColumnName, - ?int $offset, - ?int $limit, - ?DateTimeImmutable $maxStamp - ): string { - $query = 'SELECT * FROM `%s`'; - $params = [$tableName]; - - if ($maxStamp) { - $query .= ' WHERE `%s` < \'%s\''; - $params = [...$params, $stampColumnName, $maxStamp]; - } + $this->supervisor->spawn( + [ + $pdo, + $this->queryFactory->buildFetchQuery( + $tableName, + $stampColumnName, + $offset, + null, + $maxStamp + ), + $archiveMode + ] + ); - return sprintf($query, $params); + $this->supervisor->waitForFinish(); } } diff --git a/src/Services/ArchiverWorker.php b/src/Services/ArchiverWorker.php new file mode 100644 index 0000000..065b271 --- /dev/null +++ b/src/Services/ArchiverWorker.php @@ -0,0 +1,15 @@ +setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false); + } +} diff --git a/src/Services/Supervisor.php b/src/Services/Supervisor.php new file mode 100644 index 0000000..627ab50 --- /dev/null +++ b/src/Services/Supervisor.php @@ -0,0 +1,50 @@ +workerFactory = Closure::fromCallable($workerFactory); + } + + public function spawn(array $args): void + { + $this->futures[] = $this->runWorker($args); + } + + public function waitForFinish(): void + { + while (count($this->futures) > 0) { + foreach ($this->futures as $key => $future) { + if ($this->futures->done()) { + unset($this->futures[$key]); + } + } + } + } + + private function runWorker(array $args): Future + { + $future = new Runtime(__DIR__ . '/../../vendor/autoload.php'); + + return $future->run( + $this->workerFactory->call($this), + $args + ); + } +} diff --git a/tests/Manager/TableArchiverTest.php b/tests/Manager/TableArchiverTest.php index fe34393..74a6226 100644 --- a/tests/Manager/TableArchiverTest.php +++ b/tests/Manager/TableArchiverTest.php @@ -4,23 +4,32 @@ namespace Linkorb\TableArchiver\Tests\Manager; -use Linkorb\TableArchiver\Manager\TableArchiverManager; +use Linkorb\TableArchiver\Factory\QueryFactory; +use Linkorb\TableArchiver\Manager\TableArchiver; +use Linkorb\TableArchiver\Services\Supervisor; use Linkorb\TableArchiver\Tests\TestHelpers\DbSetupAwareTrait; use PDO; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -class TableArchiverManagerTest extends TestCase +class TableArchiverTest extends TestCase { use DbSetupAwareTrait; - private TableArchiverManager $manager; + private TableArchiver $manager; private PDO $pdo; + /** + * @var MockObject|Supervisor + */ + private MockObject $supervisor; + public function setUp(): void { $this->pdo = $this->setUpDb(); - $this->manager = new TableArchiverManager(2); + $this->supervisor = $this->createMock(Supervisor::class); + $this->manager = new TableArchiver(new QueryFactory(), $this->supervisor, 2); } public function test(): void @@ -28,7 +37,7 @@ public function test(): void $this->manager->archive( $this->pdo, $this->getTableName(), - TableArchiverManager::YEAR, + TableArchiver::YEAR, $this->getTimestampName(), null ); From a9c1f284e67053bbd83f705df020a1521c75a819 Mon Sep 17 00:00:00 2001 From: amsprost Date: Mon, 14 Sep 2020 03:39:56 +0200 Subject: [PATCH 03/13] Workers implementation, Dto added, Writers logic, Tests --- .gitignore | 4 ++ Dockerfile | 5 +- composer.json | 3 +- config/services.yaml | 4 ++ output/.gitkeep | 0 src/Dto/ArchiveDto.php | 16 ++++++ src/Factory/QueryFactory.php | 28 ++++++---- src/Manager/TableArchiver.php | 68 +++++++++++++++++-------- src/Services/ArchiverWorker.php | 24 ++++++++- src/Services/OutputWriter.php | 65 +++++++++++++++++++++++ src/Services/Supervisor.php | 2 +- tests/Manager/TableArchiverTest.php | 39 ++++++++++---- tests/TestHelpers/DbSetupAwareTrait.php | 5 +- 13 files changed, 218 insertions(+), 45 deletions(-) create mode 100644 output/.gitkeep create mode 100644 src/Dto/ArchiveDto.php create mode 100644 src/Services/OutputWriter.php diff --git a/.gitignore b/.gitignore index 5fbbc72..40d6291 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,7 @@ .phpunit.result.cache /phpunit.xml ###< symfony/phpunit-bridge ### + +output/* + +!.gitkeep diff --git a/Dockerfile b/Dockerfile index 21672e9..c3fcbf4 100755 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ FROM php:7.4-zts USER root RUN apt-get update && \ - apt-get install -y --no-install-recommends git zip unzip libzip4 libzip-dev ssh libxml2-dev && \ + apt-get install -y --no-install-recommends git zip unzip libzip4 libzip-dev ssh libxml2-dev sqlite3 libsqlite3-dev && \ curl -sSL https://getcomposer.org/composer.phar -o /usr/bin/composer && \ chmod +x /usr/bin/composer && \ composer selfupdate && \ @@ -22,6 +22,9 @@ RUN curl -sSL https://github.com/krakjoe/parallel/archive/develop.zip -o /tmp/pa RUN docker-php-ext-enable parallel +RUN mkdir /db +RUN /usr/bin/sqlite3 /db/test.db + RUN composer global require hirak/prestissimo WORKDIR /app diff --git a/composer.json b/composer.json index 9ca9ce3..0761ac5 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,8 @@ "symfony/framework-bundle": "5.1.*", "symfony/yaml": "5.1.*", "ext-pdo": "*", - "ext-parallel": "*" + "ext-parallel": "*", + "ext-json": "*" }, "require-dev": { "phpunit/phpunit": "^9", diff --git a/config/services.yaml b/config/services.yaml index 0b5788d..503ff2b 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -24,3 +24,7 @@ services: Linkorb\TableArchiver\Services\Supervisor: arguments: $workerFactory: !service {factory: [ 'Linkorb\TableArchiver\Factory\ArchiverWorkerFactory', 'createFactoryMethod' ]} + + Linkorb\TableArchiver\Services\OutputWriter: + arguments: + $basePath: '%kernel.project_dir%/output' diff --git a/output/.gitkeep b/output/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/Dto/ArchiveDto.php b/src/Dto/ArchiveDto.php new file mode 100644 index 0000000..2240d70 --- /dev/null +++ b/src/Dto/ArchiveDto.php @@ -0,0 +1,16 @@ +getTimestamp() : $maxStamp]; } - if ($limit) { + if (!is_null($limit)) { $query .= ' LIMIT %d'; $params = [...$params, $limit]; } - if ($offset) { + if (!is_null($offset)) { $query .= ' OFFSET %d'; $params = [...$params, $offset]; } - return sprintf($query, $params); + return sprintf($query, ...$params); } - public function buildCountQuery(string $tableName, string $stampColumnName, ?DateTimeImmutable $maxStamp): string - { + public function buildCountQuery( + string $tableName, + string $stampColumnName, + ?DateTimeImmutable $maxStamp, + bool $isTimestamp + ): string { $query = 'SELECT COUNT(*) FROM `%s`'; $params = [$tableName]; if ($maxStamp) { $query .= ' WHERE `%s` < \'%s\''; - $params = [...$params, $stampColumnName, $maxStamp]; + $params = [...$params, $stampColumnName, $isTimestamp ? $maxStamp->getTimestamp() : $maxStamp]; } - return sprintf($query, $params); + return sprintf($query, ...$params); + } + + public function buildTestQuery(string $tableName, string $stampColumnName): string + { + return sprintf('SELECT `%s` FROM `%s` WHERE `%s` IS NOT NULL', $stampColumnName, $tableName, $stampColumnName); } } diff --git a/src/Manager/TableArchiver.php b/src/Manager/TableArchiver.php index 6072fa5..7708e09 100644 --- a/src/Manager/TableArchiver.php +++ b/src/Manager/TableArchiver.php @@ -4,7 +4,8 @@ namespace Linkorb\TableArchiver\Manager; -use DateTimeImmutable; +use BadFunctionCallException; +use Linkorb\TableArchiver\Dto\ArchiveDto; use Linkorb\TableArchiver\Factory\QueryFactory; use Linkorb\TableArchiver\Services\Supervisor; use PDO; @@ -28,30 +29,45 @@ public function __construct(QueryFactory $queryFactory, Supervisor $supervisor, $this->batchSize = $batchSize; } - public function archive( - PDO $pdo, - string $tableName, - int $archiveMode, - string $stampColumnName, - ?DateTimeImmutable $maxStamp - ): void { + public function archive(PDO $pdo, ArchiveDto $dto): void + { + if (false === $pdo->query('SELECT 1')->fetch()) { + throw new BadFunctionCallException('Something is wrong with your PDO connection'); + } + + $this->detectColumnType($pdo, $dto); + $count = $pdo->query( - $this->queryFactory->buildCountQuery($tableName, $stampColumnName, $maxStamp), - PDO::FETCH_COLUMN - ); + $this->queryFactory->buildCountQuery( + $dto->tableName, + $dto->stampColumnName, + $dto->maxStamp, + $dto->isTimestamp + ), + PDO::FETCH_COLUMN, + 0 + )->fetch(); + + $this->spawnWorkers($pdo, $dto, (int) $count); + + $this->supervisor->waitForFinish(); + } - for ($offset = 0; $offset < $count; $offset += $this->batchSize) { + private function spawnWorkers(PDO $pdo, ArchiveDto $dto, int $count): void + { + for ($offset = 0; $offset < $count - $this->batchSize; $offset += $this->batchSize) { $this->supervisor->spawn( [ $pdo, $this->queryFactory->buildFetchQuery( - $tableName, - $stampColumnName, + $dto->tableName, + $dto->stampColumnName, $offset, $this->batchSize, - $maxStamp + $dto->maxStamp, + $dto->isTimestamp ), - $archiveMode + $dto ] ); } @@ -60,16 +76,26 @@ public function archive( [ $pdo, $this->queryFactory->buildFetchQuery( - $tableName, - $stampColumnName, + $dto->tableName, + $dto->stampColumnName, $offset, null, - $maxStamp + $dto->maxStamp, + $dto->isTimestamp ), - $archiveMode + $dto ] ); + } - $this->supervisor->waitForFinish(); + private function detectColumnType(PDO $pdo, ArchiveDto $dto): void + { + $testStampColumnValue = $pdo->query( + $this->queryFactory->buildTestQuery($dto->tableName, $dto->stampColumnName), + PDO::FETCH_COLUMN, + 0 + )->fetch(); + + $dto->isTimestamp = is_numeric($testStampColumnValue); } } diff --git a/src/Services/ArchiverWorker.php b/src/Services/ArchiverWorker.php index 065b271..1d2d93b 100644 --- a/src/Services/ArchiverWorker.php +++ b/src/Services/ArchiverWorker.php @@ -4,12 +4,34 @@ namespace Linkorb\TableArchiver\Services; +use DateTimeImmutable; +use DateTimeInterface; +use Linkorb\TableArchiver\Dto\ArchiveDto; use PDO; class ArchiverWorker { - public function __call(PDO $pdo, string $query, int $archiveMode): void + private OutputWriter $writer; + + public function __call(PDO $pdo, string $query, ArchiveDto $dto): void { $pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false); + + $this->writer->setArchiveMode($dto->archiveMode); + + foreach ($pdo->query($query)->fetch() as $row) { + $this->writer->write($row, $this->fetchDateTime($row, $dto)); + } + } + + private function fetchDateTime(array $row, ArchiveDto $dto): DateTimeInterface + { + $dateValue = $row[$dto->stampColumnName]; + + if ($dto->isTimestamp) { + return (new DateTimeImmutable())->setTimestamp($dateValue); + } + + return DateTimeImmutable::createFromFormat('Y-m-d h:i:s', $dateValue); } } diff --git a/src/Services/OutputWriter.php b/src/Services/OutputWriter.php new file mode 100644 index 0000000..63e20d6 --- /dev/null +++ b/src/Services/OutputWriter.php @@ -0,0 +1,65 @@ +basePath = $basePath; + } + + public function __destruct() + { + foreach ($this->fileResources as $fileResource) { + fclose($fileResource); + } + } + + public function write(array $row, DateTimeInterface $dateTime): void + { + $name = $this->getFileName($dateTime); + + if (!isset($this->fileResources[$name])) { + $fp = fopen($this->basePath . DIRECTORY_SEPARATOR . $name, 'a'); + $this->fileResources[$name] = $fp; + } + + fwrite($this->fileResources[$name], json_encode($row)); + } + + public function setArchiveMode(int $archiveMode): void + { + $this->archiveMode = $archiveMode; + } + + private function getFileName(DateTimeInterface $dateTime): string + { + switch ($this->archiveMode) { + case TableArchiver::YEAR: + return sprintf('%04d.ndjson', $dateTime->format('Y')); + case TableArchiver::YEAR_MONTH: + return sprintf('%04d%02d.ndjson', $dateTime->format('Y'), $dateTime->format('m')); + case TableArchiver::YEAR_MONTH_DAY: + return sprintf( + '%04d%02d%02d.ndjson', + $dateTime->format('Y'), + $dateTime->format('m'), + $dateTime->format('d') + ); + default: + throw new Exception('Invalid archive mode'); + } + } +} diff --git a/src/Services/Supervisor.php b/src/Services/Supervisor.php index 627ab50..5be4b20 100644 --- a/src/Services/Supervisor.php +++ b/src/Services/Supervisor.php @@ -31,7 +31,7 @@ public function waitForFinish(): void { while (count($this->futures) > 0) { foreach ($this->futures as $key => $future) { - if ($this->futures->done()) { + if ($future->done()) { unset($this->futures[$key]); } } diff --git a/tests/Manager/TableArchiverTest.php b/tests/Manager/TableArchiverTest.php index 74a6226..f6ef162 100644 --- a/tests/Manager/TableArchiverTest.php +++ b/tests/Manager/TableArchiverTest.php @@ -4,11 +4,13 @@ namespace Linkorb\TableArchiver\Tests\Manager; +use Linkorb\TableArchiver\Dto\ArchiveDto; use Linkorb\TableArchiver\Factory\QueryFactory; use Linkorb\TableArchiver\Manager\TableArchiver; use Linkorb\TableArchiver\Services\Supervisor; use Linkorb\TableArchiver\Tests\TestHelpers\DbSetupAwareTrait; use PDO; +use PDOStatement; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -32,15 +34,34 @@ public function setUp(): void $this->manager = new TableArchiver(new QueryFactory(), $this->supervisor, 2); } - public function test(): void + public function testIncorrectDbConnection() { - $this->manager->archive( - $this->pdo, - $this->getTableName(), - TableArchiver::YEAR, - $this->getTimestampName(), - null - ); - $this->assertTrue(true); + $statement = $this->createConfiguredMock(PDOStatement::class, ['fetch' => false]); + $pdo = $this->createConfiguredMock(PDO::class, ['query' => $statement]); + $dto = new ArchiveDto(); + + $this->expectExceptionMessage('Something is wrong with your PDO connection'); + + $this->manager->archive($pdo, $dto); + } + + public function testAcrchive(): void + { + $dto = new ArchiveDto(); + $dto->archiveMode = TableArchiver::YEAR; + $dto->stampColumnName = $this->getTimestampName(); + $dto->tableName = $this->getTableName(); + + $this->supervisor + ->expects($this->exactly(4)) + ->method('spawn') + ->withConsecutive( + [[$this->pdo, 'SELECT * FROM `' . $this->getTableName() . '` LIMIT 2 OFFSET 0', $dto]], + [[$this->pdo, 'SELECT * FROM `' . $this->getTableName() . '` LIMIT 2 OFFSET 2', $dto]], + [[$this->pdo, 'SELECT * FROM `' . $this->getTableName() . '` LIMIT 2 OFFSET 4', $dto]], + [[$this->pdo, 'SELECT * FROM `' . $this->getTableName() . '` OFFSET 6', $dto]], + ); + + $this->manager->archive($this->pdo, $dto); } } diff --git a/tests/TestHelpers/DbSetupAwareTrait.php b/tests/TestHelpers/DbSetupAwareTrait.php index f82ac5a..a54e680 100644 --- a/tests/TestHelpers/DbSetupAwareTrait.php +++ b/tests/TestHelpers/DbSetupAwareTrait.php @@ -18,6 +18,7 @@ protected function getDbData(): array ['d', new DateTime('1999-01-01 00:13:56')], ['e', new DateTime('2020-08-02 11:13:37')], ['f', new DateTime('2020-08-03 09:57:34')], + ['g', new DateTime('2019-08-03 09:57:34')], ]; } @@ -37,9 +38,9 @@ private function setUpDb(): PDO $pdo->exec( sprintf( 'CREATE table %s( - id INT( 11 ) AUTO_INCREMENT PRIMARY KEY, + id INTEGER PRIMARY KEY AUTOINCREMENT, test VARCHAR( 255 ) NOT NULL, - `%s` TIMESTAMP NOT NULL;', + `%s` TIMESTAMP NOT NULL)', $this->getTableName(), $this->getTimestampName() ) From f63969a67b5b9ca23b2d3eab7e91f0ab9b9d8d41 Mon Sep 17 00:00:00 2001 From: amsprost Date: Fri, 18 Sep 2020 04:19:27 +0200 Subject: [PATCH 04/13] Tests, bug fixes --- config/services.yaml | 5 ++- src/Dto/ArchiveDto.php | 3 ++ src/Factory/ArchiverWorkerFactory.php | 7 ++-- src/Manager/TableArchiver.php | 11 ++++-- src/Services/ArchiverWorker.php | 20 ++++++++-- src/Services/OutputWriter.php | 9 ++++- src/Services/Supervisor.php | 5 +-- tests/Manager/TableArchiverTest.php | 50 ++++++++++++++++++------ tests/Services/ArchiverWorkerTest.php | 51 +++++++++++++++++++++++++ tests/TestHelpers/DbSetupAwareTrait.php | 21 ++++++++-- 10 files changed, 150 insertions(+), 32 deletions(-) create mode 100644 tests/Services/ArchiverWorkerTest.php diff --git a/config/services.yaml b/config/services.yaml index 503ff2b..803dbd9 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -23,7 +23,10 @@ services: Linkorb\TableArchiver\Services\Supervisor: arguments: - $workerFactory: !service {factory: [ 'Linkorb\TableArchiver\Factory\ArchiverWorkerFactory', 'createFactoryMethod' ]} + $workerFactory: !service + factory: [ 'Linkorb\TableArchiver\Factory\ArchiverWorkerFactory', 'createFactoryMethod' ] + arguments: + - '@Linkorb\TableArchiver\Services\OutputWriter' Linkorb\TableArchiver\Services\OutputWriter: arguments: diff --git a/src/Dto/ArchiveDto.php b/src/Dto/ArchiveDto.php index 2240d70..0272d70 100644 --- a/src/Dto/ArchiveDto.php +++ b/src/Dto/ArchiveDto.php @@ -8,6 +8,9 @@ class ArchiveDto { + public string $pdoDsn; + public string $pdoUsername; + public string $pdoPassword; public string $tableName; public int $archiveMode; public string $stampColumnName; diff --git a/src/Factory/ArchiverWorkerFactory.php b/src/Factory/ArchiverWorkerFactory.php index 1bd20ed..37b660f 100644 --- a/src/Factory/ArchiverWorkerFactory.php +++ b/src/Factory/ArchiverWorkerFactory.php @@ -5,13 +5,14 @@ namespace Linkorb\TableArchiver\Factory; use Linkorb\TableArchiver\Services\ArchiverWorker; +use Linkorb\TableArchiver\Services\OutputWriter; class ArchiverWorkerFactory { - public function createFactoryMethod(): callable + public function createFactoryMethod(OutputWriter $writer): callable { - return function () { - return new ArchiverWorker(); + return function () use ($writer) { + return new ArchiverWorker($writer); }; } } diff --git a/src/Manager/TableArchiver.php b/src/Manager/TableArchiver.php index 7708e09..033f1bd 100644 --- a/src/Manager/TableArchiver.php +++ b/src/Manager/TableArchiver.php @@ -29,8 +29,10 @@ public function __construct(QueryFactory $queryFactory, Supervisor $supervisor, $this->batchSize = $batchSize; } - public function archive(PDO $pdo, ArchiveDto $dto): void + public function archive(ArchiveDto $dto): void { + $pdo = $this->createPDO($dto); + if (false === $pdo->query('SELECT 1')->fetch()) { throw new BadFunctionCallException('Something is wrong with your PDO connection'); } @@ -53,12 +55,16 @@ public function archive(PDO $pdo, ArchiveDto $dto): void $this->supervisor->waitForFinish(); } + protected function createPDO(ArchiveDto $dto): PDO + { + return new PDO($dto->pdoDsn, $dto->pdoUsername, $dto->pdoPassword); + } + private function spawnWorkers(PDO $pdo, ArchiveDto $dto, int $count): void { for ($offset = 0; $offset < $count - $this->batchSize; $offset += $this->batchSize) { $this->supervisor->spawn( [ - $pdo, $this->queryFactory->buildFetchQuery( $dto->tableName, $dto->stampColumnName, @@ -74,7 +80,6 @@ private function spawnWorkers(PDO $pdo, ArchiveDto $dto, int $count): void $this->supervisor->spawn( [ - $pdo, $this->queryFactory->buildFetchQuery( $dto->tableName, $dto->stampColumnName, diff --git a/src/Services/ArchiverWorker.php b/src/Services/ArchiverWorker.php index 1d2d93b..068e9ff 100644 --- a/src/Services/ArchiverWorker.php +++ b/src/Services/ArchiverWorker.php @@ -13,17 +13,31 @@ class ArchiverWorker { private OutputWriter $writer; - public function __call(PDO $pdo, string $query, ArchiveDto $dto): void + public function __construct(OutputWriter $writer) { + $this->writer = $writer; + } + + public function __invoke(string $query, ArchiveDto $dto): void + { + $pdo = $this->createPDO($dto); + $pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false); $this->writer->setArchiveMode($dto->archiveMode); - foreach ($pdo->query($query)->fetch() as $row) { + $pdoStatement = $pdo->query($query); + + while ($row = $pdoStatement->fetch(PDO::FETCH_ASSOC)) { $this->writer->write($row, $this->fetchDateTime($row, $dto)); } } + protected function createPDO(ArchiveDto $dto): PDO + { + return new PDO($dto->pdoDsn, $dto->pdoUsername, $dto->pdoPassword); + } + private function fetchDateTime(array $row, ArchiveDto $dto): DateTimeInterface { $dateValue = $row[$dto->stampColumnName]; @@ -32,6 +46,6 @@ private function fetchDateTime(array $row, ArchiveDto $dto): DateTimeInterface return (new DateTimeImmutable())->setTimestamp($dateValue); } - return DateTimeImmutable::createFromFormat('Y-m-d h:i:s', $dateValue); + return DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $dateValue); } } diff --git a/src/Services/OutputWriter.php b/src/Services/OutputWriter.php index 63e20d6..a743b8e 100644 --- a/src/Services/OutputWriter.php +++ b/src/Services/OutputWriter.php @@ -32,11 +32,11 @@ public function write(array $row, DateTimeInterface $dateTime): void $name = $this->getFileName($dateTime); if (!isset($this->fileResources[$name])) { - $fp = fopen($this->basePath . DIRECTORY_SEPARATOR . $name, 'a'); + $fp = fopen($this->outputPath($name), 'a'); $this->fileResources[$name] = $fp; } - fwrite($this->fileResources[$name], json_encode($row)); + fwrite($this->fileResources[$name], json_encode($row) . "\n"); } public function setArchiveMode(int $archiveMode): void @@ -62,4 +62,9 @@ private function getFileName(DateTimeInterface $dateTime): string throw new Exception('Invalid archive mode'); } } + + protected function outputPath(string $name): string + { + return $this->basePath . DIRECTORY_SEPARATOR . $name; + } } diff --git a/src/Services/Supervisor.php b/src/Services/Supervisor.php index 5be4b20..aeb1f98 100644 --- a/src/Services/Supervisor.php +++ b/src/Services/Supervisor.php @@ -42,9 +42,6 @@ private function runWorker(array $args): Future { $future = new Runtime(__DIR__ . '/../../vendor/autoload.php'); - return $future->run( - $this->workerFactory->call($this), - $args - ); + return $future->run(Closure::fromCallable($this->workerFactory->call($this)), $args); } } diff --git a/tests/Manager/TableArchiverTest.php b/tests/Manager/TableArchiverTest.php index f6ef162..da43e5a 100644 --- a/tests/Manager/TableArchiverTest.php +++ b/tests/Manager/TableArchiverTest.php @@ -19,34 +19,36 @@ class TableArchiverTest extends TestCase use DbSetupAwareTrait; private TableArchiver $manager; - private PDO $pdo; - /** - * @var MockObject|Supervisor - */ + /** @var MockObject|Supervisor */ private MockObject $supervisor; public function setUp(): void { $this->pdo = $this->setUpDb(); $this->supervisor = $this->createMock(Supervisor::class); - $this->manager = new TableArchiver(new QueryFactory(), $this->supervisor, 2); + $this->manager = $this->createPartialMock(TableArchiver::class, ['createPDO']); + + $this->manager->__construct(new QueryFactory(), $this->supervisor, 2); } public function testIncorrectDbConnection() { $statement = $this->createConfiguredMock(PDOStatement::class, ['fetch' => false]); $pdo = $this->createConfiguredMock(PDO::class, ['query' => $statement]); + $this->manager->method('createPDO')->willReturn($pdo); $dto = new ArchiveDto(); $this->expectExceptionMessage('Something is wrong with your PDO connection'); - $this->manager->archive($pdo, $dto); + $this->manager->archive($dto); } - public function testAcrchive(): void + public function testArchiveTimestamp(): void { + $this->manager->method('createPDO')->willReturn($this->pdo); + $dto = new ArchiveDto(); $dto->archiveMode = TableArchiver::YEAR; $dto->stampColumnName = $this->getTimestampName(); @@ -56,12 +58,36 @@ public function testAcrchive(): void ->expects($this->exactly(4)) ->method('spawn') ->withConsecutive( - [[$this->pdo, 'SELECT * FROM `' . $this->getTableName() . '` LIMIT 2 OFFSET 0', $dto]], - [[$this->pdo, 'SELECT * FROM `' . $this->getTableName() . '` LIMIT 2 OFFSET 2', $dto]], - [[$this->pdo, 'SELECT * FROM `' . $this->getTableName() . '` LIMIT 2 OFFSET 4', $dto]], - [[$this->pdo, 'SELECT * FROM `' . $this->getTableName() . '` OFFSET 6', $dto]], + [['SELECT * FROM `' . $this->getTableName() . '` LIMIT 2 OFFSET 0', $dto]], + [['SELECT * FROM `' . $this->getTableName() . '` LIMIT 2 OFFSET 2', $dto]], + [['SELECT * FROM `' . $this->getTableName() . '` LIMIT 2 OFFSET 4', $dto]], + [['SELECT * FROM `' . $this->getTableName() . '` OFFSET 6', $dto]], + ); + + $this->manager->archive($dto); + $this->assertTrue($dto->isTimestamp); + } + + public function testArchiveDateTime(): void + { + $this->manager->method('createPDO')->willReturn($this->pdo); + + $dto = new ArchiveDto(); + $dto->archiveMode = TableArchiver::YEAR_MONTH_DAY; + $dto->stampColumnName = $this->getDateTimeName(); + $dto->tableName = $this->getTableName(); + + $this->supervisor + ->expects($this->exactly(4)) + ->method('spawn') + ->withConsecutive( + [['SELECT * FROM `' . $this->getTableName() . '` LIMIT 2 OFFSET 0', $dto]], + [['SELECT * FROM `' . $this->getTableName() . '` LIMIT 2 OFFSET 2', $dto]], + [['SELECT * FROM `' . $this->getTableName() . '` LIMIT 2 OFFSET 4', $dto]], + [['SELECT * FROM `' . $this->getTableName() . '` OFFSET 6', $dto]], ); - $this->manager->archive($this->pdo, $dto); + $this->manager->archive($dto); + $this->assertFalse($dto->isTimestamp); } } diff --git a/tests/Services/ArchiverWorkerTest.php b/tests/Services/ArchiverWorkerTest.php new file mode 100644 index 0000000..0b10427 --- /dev/null +++ b/tests/Services/ArchiverWorkerTest.php @@ -0,0 +1,51 @@ +pdo = $this->setUpDb(); + $this->writer = $this->createPartialMock(OutputWriter::class, ['outputPath']); + $this->writer->method('outputPath')->willReturn('php://memory'); + + $this->worker = $this->createPartialMock(ArchiverWorker::class, ['createPDO']); + $this->worker->__construct($this->writer); + $this->worker->method('createPDO')->willReturn($this->pdo); + } + + public function testInvokeDateTimeYearMonthDay() + { + $dto = new ArchiveDto(); + $dto->isTimestamp = false; + $dto->tableName = $this->getTableName(); + $dto->stampColumnName = $this->getDateTimeName(); + $dto->archiveMode = TableArchiver::YEAR_MONTH_DAY; + + $this->writer + ->expects($this->exactly(2)) + ->method('outputPath') + ->withConsecutive(['20200801.ndjson'], ['20001110.ndjson']); + + ($this->worker)('SELECT * FROM `' . $this->getTableName() . '` LIMIT 2 OFFSET 0', $dto); + } +} diff --git a/tests/TestHelpers/DbSetupAwareTrait.php b/tests/TestHelpers/DbSetupAwareTrait.php index a54e680..7a9cbea 100644 --- a/tests/TestHelpers/DbSetupAwareTrait.php +++ b/tests/TestHelpers/DbSetupAwareTrait.php @@ -32,6 +32,11 @@ protected function getTimestampName(): string return 'timestamp'; } + protected function getDateTimeName(): string + { + return 'datetime'; + } + private function setUpDb(): PDO { $pdo = new PDO('sqlite::memory:'); @@ -39,9 +44,11 @@ private function setUpDb(): PDO sprintf( 'CREATE table %s( id INTEGER PRIMARY KEY AUTOINCREMENT, - test VARCHAR( 255 ) NOT NULL, - `%s` TIMESTAMP NOT NULL)', + test VARCHAR( 255 ) NOT NULL, + `%s` DATETIME NOT NULL, + `%s` BIGINT NOT NULL)', $this->getTableName(), + $this->getDateTimeName(), $this->getTimestampName() ) ); @@ -52,7 +59,12 @@ private function setUpDb(): PDO function (array $dbRow): string { /** @var DateTime $dateTime */ [$test, $dateTime] = $dbRow; - return sprintf('(\'%s\', \'%s\')', $test, $dateTime->format('Y-m-d H:i:s')); + return sprintf( + '(\'%s\', \'%s\', \'%s\')', + $test, + $dateTime->format('Y-m-d H:i:s'), + $dateTime->getTimestamp() + ); }, $this->getDbData() ) @@ -60,8 +72,9 @@ function (array $dbRow): string { $pdo->exec( sprintf( - 'INSERT INTO %s (test, `%s`) VALUES %s;', + 'INSERT INTO %s (test, `%s`, `%s`) VALUES %s;', $this->getTableName(), + $this->getDateTimeName(), $this->getTimestampName(), $values ) From 7299c8dae684bd4ae2e5f3ea638ad6dc6b845076 Mon Sep 17 00:00:00 2001 From: amsprost Date: Fri, 25 Sep 2020 03:54:41 +0200 Subject: [PATCH 05/13] Implement compressing, finalized services, command implementation started --- .env | 2 ++ composer.json | 9 ++--- composer.lock | 48 ++++++++++++++++++++++++- config/services.yaml | 8 +++++ src/Command/TableArchiveCommand.php | 35 ++++++++++++++++++ src/Manager/TableArchiver.php | 24 ++++++++++--- src/Services/ArchiverWorker.php | 7 +++- src/Services/OutputArchiver.php | 55 +++++++++++++++++++++++++++++ src/Services/Supervisor.php | 15 ++++++-- symfony.lock | 3 ++ 10 files changed, 194 insertions(+), 12 deletions(-) create mode 100644 src/Command/TableArchiveCommand.php create mode 100644 src/Services/OutputArchiver.php diff --git a/.env b/.env index ed754c2..c15e5c4 100644 --- a/.env +++ b/.env @@ -19,3 +19,5 @@ APP_SECRET=c7bb60cab041aecda02f06effa4c969b #TRUSTED_PROXIES=127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 #TRUSTED_HOSTS='^(localhost|example\.com)$' ###< symfony/framework-bundle ### + +APP_THREAD_BATCH_SIZE=4 diff --git a/composer.json b/composer.json index 0761ac5..0386d78 100644 --- a/composer.json +++ b/composer.json @@ -5,14 +5,15 @@ "php": ">=7.4", "ext-ctype": "*", "ext-iconv": "*", + "ext-json": "*", + "ext-parallel": "*", + "ext-pdo": "*", + "linkorb/connector": "^1.2", "symfony/console": "5.1.*", "symfony/dotenv": "5.1.*", "symfony/flex": "^1.3.1", "symfony/framework-bundle": "5.1.*", - "symfony/yaml": "5.1.*", - "ext-pdo": "*", - "ext-parallel": "*", - "ext-json": "*" + "symfony/yaml": "5.1.*" }, "require-dev": { "phpunit/phpunit": "^9", diff --git a/composer.lock b/composer.lock index 8eb1567..84adc4d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,52 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "421321aba53467b6b48bd29dde233a4d", + "content-hash": "4bf9c447d3b8f0b7d7fc44ed8731a9a4", "packages": [ + { + "name": "linkorb/connector", + "version": "v1.2.2", + "source": { + "type": "git", + "url": "https://github.com/linkorb/connector.git", + "reference": "a6872994a45c6f2bf5c70ce3b00f5f4efe7f78f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/linkorb/connector/zipball/a6872994a45c6f2bf5c70ce3b00f5f4efe7f78f9", + "reference": "a6872994a45c6f2bf5c70ce3b00f5f4efe7f78f9", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Connector\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Joost Faassen", + "email": "j.faassen@linkorb.com", + "role": "Development" + } + ], + "description": "Database connection config resolver", + "homepage": "http://www.github.com/linkorb/connector", + "keywords": [ + "connector", + "linkorb", + "pdo", + "php" + ], + "time": "2019-01-08T18:46:45+00:00" + }, { "name": "psr/cache", "version": "1.0.1", @@ -4378,6 +4422,8 @@ "php": ">=7.4", "ext-ctype": "*", "ext-iconv": "*", + "ext-json": "*", + "ext-parallel": "*", "ext-pdo": "*" }, "platform-dev": [], diff --git a/config/services.yaml b/config/services.yaml index 803dbd9..bb4b82a 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -31,3 +31,11 @@ services: Linkorb\TableArchiver\Services\OutputWriter: arguments: $basePath: '%kernel.project_dir%/output' + + Linkorb\TableArchiver\Services\OutputArchiver: + arguments: + $basePath: '%kernel.project_dir%/output' + + Linkorb\TableArchiver\Manager\TableArchiver: + arguments: + $batchSize: '%env(APP_THREAD_BATCH_SIZE)%' diff --git a/src/Command/TableArchiveCommand.php b/src/Command/TableArchiveCommand.php new file mode 100644 index 0000000..ece9aec --- /dev/null +++ b/src/Command/TableArchiveCommand.php @@ -0,0 +1,35 @@ +archiver = $archiver; + $this->connector = $connector; + + parent::__construct(); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $config = $this->connector->getConfig($input->getArgument('dsn')); + $pdo = $this->connector->getPdo($config); + } +} diff --git a/src/Manager/TableArchiver.php b/src/Manager/TableArchiver.php index 033f1bd..b6239f6 100644 --- a/src/Manager/TableArchiver.php +++ b/src/Manager/TableArchiver.php @@ -7,7 +7,9 @@ use BadFunctionCallException; use Linkorb\TableArchiver\Dto\ArchiveDto; use Linkorb\TableArchiver\Factory\QueryFactory; +use Linkorb\TableArchiver\Services\OutputArchiver; use Linkorb\TableArchiver\Services\Supervisor; +use LogicException; use PDO; class TableArchiver @@ -20,12 +22,19 @@ class TableArchiver private Supervisor $supervisor; + private OutputArchiver $outputArchiver; + private int $batchSize; - public function __construct(QueryFactory $queryFactory, Supervisor $supervisor, int $batchSize) - { + public function __construct( + QueryFactory $queryFactory, + Supervisor $supervisor, + OutputArchiver $outputArchiver, + int $batchSize + ) { $this->queryFactory = $queryFactory; $this->supervisor = $supervisor; + $this->outputArchiver = $outputArchiver; $this->batchSize = $batchSize; } @@ -50,9 +59,16 @@ public function archive(ArchiveDto $dto): void 0 )->fetch(); - $this->spawnWorkers($pdo, $dto, (int) $count); + $this->spawnWorkers($pdo, $dto, (int)$count); + + if ($count !== $this->supervisor->waitForFinish()) { + throw new LogicException('Number of found and processed rows isn\'t match'); + } + } - $this->supervisor->waitForFinish(); + public function finalize(): void + { + $this->outputArchiver->archive(); } protected function createPDO(ArchiveDto $dto): PDO diff --git a/src/Services/ArchiverWorker.php b/src/Services/ArchiverWorker.php index 068e9ff..bd8dc24 100644 --- a/src/Services/ArchiverWorker.php +++ b/src/Services/ArchiverWorker.php @@ -7,6 +7,7 @@ use DateTimeImmutable; use DateTimeInterface; use Linkorb\TableArchiver\Dto\ArchiveDto; +use parallel\Channel; use PDO; class ArchiverWorker @@ -18,7 +19,7 @@ public function __construct(OutputWriter $writer) $this->writer = $writer; } - public function __invoke(string $query, ArchiveDto $dto): void + public function __invoke(string $query, ArchiveDto $dto, Channel $channel): void { $pdo = $this->createPDO($dto); @@ -28,9 +29,13 @@ public function __invoke(string $query, ArchiveDto $dto): void $pdoStatement = $pdo->query($query); + $rowsCount = 0; while ($row = $pdoStatement->fetch(PDO::FETCH_ASSOC)) { + ++$rowsCount; $this->writer->write($row, $this->fetchDateTime($row, $dto)); } + + $channel->send($rowsCount); } protected function createPDO(ArchiveDto $dto): PDO diff --git a/src/Services/OutputArchiver.php b/src/Services/OutputArchiver.php new file mode 100644 index 0000000..d915260 --- /dev/null +++ b/src/Services/OutputArchiver.php @@ -0,0 +1,55 @@ +basePath = $basePath; + } + + public function archive(): void + { + if (!is_dir($this->basePath) || !($dh = opendir($this->basePath))) { + throw new Exception('Base path isn\'t a dir'); + } + + while (($file = readdir($dh)) !== false) { + $filePath = $this->basePath . DIRECTORY_SEPARATOR . $file; + + if (!is_file($filePath) || pathinfo($filePath, PATHINFO_EXTENSION) !== 'ndjson') { + continue; + } + + $this->gzCompressFile($filePath); + } + + closedir($dh); + } + + // TODO: consider moving compression to threads + private function gzCompressFile(string $source, $level = 9): void + { + $dest = $source . '.gz'; + $mode = 'wb' . $level; + if (!$fpOut = gzopen($dest, $mode)) { + throw new Exception('Unable to open archive'); + } + + if (!$fpIn = fopen($source, 'rb')) { + throw new Exception('Base path hasn\'t been accessible for read'); + } + + while (!feof($fpIn)) { + gzwrite($fpOut, fread($fpIn, 1024 * 512)); + } + fclose($fpIn); + + gzclose($fpOut); + } +} diff --git a/src/Services/Supervisor.php b/src/Services/Supervisor.php index aeb1f98..e6fc042 100644 --- a/src/Services/Supervisor.php +++ b/src/Services/Supervisor.php @@ -5,6 +5,7 @@ namespace Linkorb\TableArchiver\Services; use Closure; +use parallel\Channel; use parallel\Future; use parallel\Runtime; @@ -17,9 +18,12 @@ class Supervisor private Closure $workerFactory; + private Channel $channel; + public function __construct(callable $workerFactory) { $this->workerFactory = Closure::fromCallable($workerFactory); + $this->channel = new Channel(); } public function spawn(array $args): void @@ -27,21 +31,28 @@ public function spawn(array $args): void $this->futures[] = $this->runWorker($args); } - public function waitForFinish(): void + public function waitForFinish(): int { + $totalRows = 0; + while (count($this->futures) > 0) { foreach ($this->futures as $key => $future) { if ($future->done()) { + $totalRows += $this->channel->recv(); unset($this->futures[$key]); } } } + + $this->channel->close(); + + return $totalRows; } private function runWorker(array $args): Future { $future = new Runtime(__DIR__ . '/../../vendor/autoload.php'); - return $future->run(Closure::fromCallable($this->workerFactory->call($this)), $args); + return $future->run(Closure::fromCallable($this->workerFactory->call($this)), [...$args, $this->channel]); } } diff --git a/symfony.lock b/symfony.lock index fa0b5a3..5bb36dc 100644 --- a/symfony.lock +++ b/symfony.lock @@ -2,6 +2,9 @@ "doctrine/instantiator": { "version": "1.3.1" }, + "linkorb/connector": { + "version": "v1.2.2" + }, "myclabs/deep-copy": { "version": "1.10.1" }, From f702d311e80f384899b4ab9acea0ea20fe3debfe Mon Sep 17 00:00:00 2001 From: amsprost Date: Wed, 30 Sep 2020 04:05:13 +0200 Subject: [PATCH 06/13] Finish functional implementation --- .env | 2 +- config/services.yaml | 5 +- src/Command/TableArchiveCommand.php | 69 +++++++++++++++++++++++++-- src/Dto/ArchiveDto.php | 6 ++- src/Factory/ArchiverWorkerFactory.php | 7 +-- src/Factory/QueryFactory.php | 17 +++++++ src/Manager/TableArchiver.php | 47 +++++++++++++----- src/Services/ArchiverWorker.php | 12 ++--- src/Services/OutputWriter.php | 8 ++-- src/Services/Supervisor.php | 10 ++-- tests/Manager/TableArchiverTest.php | 4 +- tests/Services/ArchiverWorkerTest.php | 3 +- 12 files changed, 148 insertions(+), 42 deletions(-) diff --git a/.env b/.env index c15e5c4..5db3684 100644 --- a/.env +++ b/.env @@ -20,4 +20,4 @@ APP_SECRET=c7bb60cab041aecda02f06effa4c969b #TRUSTED_HOSTS='^(localhost|example\.com)$' ###< symfony/framework-bundle ### -APP_THREAD_BATCH_SIZE=4 +APP_THREAD_BATCH_SIZE=2 diff --git a/config/services.yaml b/config/services.yaml index bb4b82a..293bcb8 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -24,9 +24,10 @@ services: Linkorb\TableArchiver\Services\Supervisor: arguments: $workerFactory: !service - factory: [ 'Linkorb\TableArchiver\Factory\ArchiverWorkerFactory', 'createFactoryMethod' ] + factory: [ '@Linkorb\TableArchiver\Factory\ArchiverWorkerFactory', 'createFactoryMethod' ] arguments: - '@Linkorb\TableArchiver\Services\OutputWriter' + - '@Connector\Connector' Linkorb\TableArchiver\Services\OutputWriter: arguments: @@ -39,3 +40,5 @@ services: Linkorb\TableArchiver\Manager\TableArchiver: arguments: $batchSize: '%env(APP_THREAD_BATCH_SIZE)%' + + Connector\Connector: diff --git a/src/Command/TableArchiveCommand.php b/src/Command/TableArchiveCommand.php index ece9aec..d7452ad 100644 --- a/src/Command/TableArchiveCommand.php +++ b/src/Command/TableArchiveCommand.php @@ -5,15 +5,19 @@ namespace Linkorb\TableArchiver\Command; use Connector\Connector; +use DateTime; +use DateTimeInterface; +use Linkorb\TableArchiver\Dto\ArchiveDto; use Linkorb\TableArchiver\Manager\TableArchiver; -use PDO; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ConfirmationQuestion; final class TableArchiveCommand extends Command { - protected static $defaultName = 'linkorb:table:arcive'; + protected static $defaultName = 'linkorb:table:archive'; private TableArchiver $archiver; @@ -27,9 +31,66 @@ public function __construct(TableArchiver $archiver, Connector $connector) parent::__construct(); } + protected function configure() + { + $this + ->addArgument('dsn', InputArgument::REQUIRED, 'PDO dsn for DB') + ->addArgument('tableName', InputArgument::REQUIRED, 'Table name to archive') + ->addArgument( + 'mode', + InputArgument::REQUIRED, + 'Date range for which archived cluster will be created. Allowed values: YEAR, YEAR_MONTH, YEAR_MONTH_DAY' + ) + ->addArgument('columnName', InputArgument::REQUIRED, 'Column which contains date information') + ->addArgument('maxStamp', InputArgument::OPTIONAL, 'Archive records which older than specified date'); + } + protected function execute(InputInterface $input, OutputInterface $output) { - $config = $this->connector->getConfig($input->getArgument('dsn')); - $pdo = $this->connector->getPdo($config); + $dto = $this->createDto($input, $output); + + $pdo = $this->connector->getPdo($this->connector->getConfig($dto->pdoDsn)); + + $count = $this->archiver->archive($pdo, $dto); + $this->archiver->archiveExportedFiles(); + + $helper = $this->getHelper('question'); + $question = new ConfirmationQuestion('All records archived. Do you want to proceed with removal?', false); + + $output->write(sprintf('%d records have been processed', $count)); + if (!$helper->ask($input, $output, $question)) { + $this->archiver->flushArchived($pdo, $dto, $count); + } + } + + private function createDto(InputInterface $input, OutputInterface $output): ArchiveDto + { + $archiveModeMap = [ + 'YEAR' => ArchiveDto::YEAR, + 'YEAR_MONTH' => ArchiveDto::YEAR_MONTH, + 'YEAR_MONTH_DAY' => ArchiveDto::YEAR_MONTH_DAY, + ]; + + $dto = new ArchiveDto(); + $dto->pdoDsn = $input->getArgument('dsn'); + $dto->tableName = $input->getArgument('tableName'); + + if (!isset($archiveModeMap[$input->getArgument('mode')])) { + $output->write('This archive mode is not allowed'); + return -1; + } + + $dto->archiveMode = $archiveModeMap[$input->getArgument('mode')]; + $dto->stampColumnName = $input->getArgument('columnName'); + + $datetime = $input->getArgument('maxStamp') ? + DateTime::createFromFormat('Ymd', $input->getArgument('maxStamp')) : null; + if ($datetime === false) { + $output->write('Incorrect max stamp passed'); + return -1; + } + $dto->maxStamp = $datetime instanceof DateTimeInterface ? $datetime->setTime(0, 0) : $datetime; + + return $dto; } } diff --git a/src/Dto/ArchiveDto.php b/src/Dto/ArchiveDto.php index 0272d70..ad0be18 100644 --- a/src/Dto/ArchiveDto.php +++ b/src/Dto/ArchiveDto.php @@ -8,9 +8,11 @@ class ArchiveDto { + public const YEAR = 0; + public const YEAR_MONTH = 1; + public const YEAR_MONTH_DAY = 2; + public string $pdoDsn; - public string $pdoUsername; - public string $pdoPassword; public string $tableName; public int $archiveMode; public string $stampColumnName; diff --git a/src/Factory/ArchiverWorkerFactory.php b/src/Factory/ArchiverWorkerFactory.php index 37b660f..710530e 100644 --- a/src/Factory/ArchiverWorkerFactory.php +++ b/src/Factory/ArchiverWorkerFactory.php @@ -4,15 +4,16 @@ namespace Linkorb\TableArchiver\Factory; +use Connector\Connector; use Linkorb\TableArchiver\Services\ArchiverWorker; use Linkorb\TableArchiver\Services\OutputWriter; class ArchiverWorkerFactory { - public function createFactoryMethod(OutputWriter $writer): callable + public function createFactoryMethod(OutputWriter $writer, Connector $connector): callable { - return function () use ($writer) { - return new ArchiverWorker($writer); + return function () use ($writer, $connector) { + return new ArchiverWorker($writer, $connector); }; } } diff --git a/src/Factory/QueryFactory.php b/src/Factory/QueryFactory.php index 5e065a3..5b2b184 100644 --- a/src/Factory/QueryFactory.php +++ b/src/Factory/QueryFactory.php @@ -54,6 +54,23 @@ public function buildCountQuery( return sprintf($query, ...$params); } + public function buildDeleteQuery( + string $tableName, + string $stampColumnName, + ?DateTimeImmutable $maxStamp, + bool $isTimestamp + ): string { + $query = 'DELETE FROM `%s`'; + $params = [$tableName]; + + if ($maxStamp) { + $query .= ' WHERE `%s` < \'%s\''; + $params = [...$params, $stampColumnName, $isTimestamp ? $maxStamp->getTimestamp() : $maxStamp]; + } + + return sprintf($query, ...$params); + } + public function buildTestQuery(string $tableName, string $stampColumnName): string { return sprintf('SELECT `%s` FROM `%s` WHERE `%s` IS NOT NULL', $stampColumnName, $tableName, $stampColumnName); diff --git a/src/Manager/TableArchiver.php b/src/Manager/TableArchiver.php index b6239f6..4fc6e28 100644 --- a/src/Manager/TableArchiver.php +++ b/src/Manager/TableArchiver.php @@ -5,6 +5,7 @@ namespace Linkorb\TableArchiver\Manager; use BadFunctionCallException; +use InvalidArgumentException; use Linkorb\TableArchiver\Dto\ArchiveDto; use Linkorb\TableArchiver\Factory\QueryFactory; use Linkorb\TableArchiver\Services\OutputArchiver; @@ -14,10 +15,6 @@ class TableArchiver { - public const YEAR = 0; - public const YEAR_MONTH = 1; - public const YEAR_MONTH_DAY = 2; - private QueryFactory $queryFactory; private Supervisor $supervisor; @@ -38,10 +35,8 @@ public function __construct( $this->batchSize = $batchSize; } - public function archive(ArchiveDto $dto): void + public function archive(PDO $pdo, ArchiveDto $dto): int { - $pdo = $this->createPDO($dto); - if (false === $pdo->query('SELECT 1')->fetch()) { throw new BadFunctionCallException('Something is wrong with your PDO connection'); } @@ -59,24 +54,52 @@ public function archive(ArchiveDto $dto): void 0 )->fetch(); - $this->spawnWorkers($pdo, $dto, (int)$count); + $this->spawnWorkers($dto, (int)$count); if ($count !== $this->supervisor->waitForFinish()) { throw new LogicException('Number of found and processed rows isn\'t match'); } + + return $count; } - public function finalize(): void + public function archiveExportedFiles(): void { $this->outputArchiver->archive(); } - protected function createPDO(ArchiveDto $dto): PDO + public function flushArchived(PDO $pdo, ArchiveDto $dto, int $rowsArchived): void { - return new PDO($dto->pdoDsn, $dto->pdoUsername, $dto->pdoPassword); + $pdo->beginTransaction(); + + $count = $pdo->query( + $this->queryFactory->buildCountQuery( + $dto->tableName, + $dto->stampColumnName, + $dto->maxStamp, + $dto->isTimestamp + ), + PDO::FETCH_COLUMN, + 0 + )->fetch(); + + if ($count !== $rowsArchived) { + throw new InvalidArgumentException('Number of archived rows and marked for deletion mismatch'); + } + + $pdo->exec( + $this->queryFactory->buildDeleteQuery( + $dto->tableName, + $dto->stampColumnName, + $dto->maxStamp, + $dto->isTimestamp + ) + ); + + $pdo->commit(); } - private function spawnWorkers(PDO $pdo, ArchiveDto $dto, int $count): void + private function spawnWorkers(ArchiveDto $dto, int $count): void { for ($offset = 0; $offset < $count - $this->batchSize; $offset += $this->batchSize) { $this->supervisor->spawn( diff --git a/src/Services/ArchiverWorker.php b/src/Services/ArchiverWorker.php index bd8dc24..23e64ca 100644 --- a/src/Services/ArchiverWorker.php +++ b/src/Services/ArchiverWorker.php @@ -4,6 +4,7 @@ namespace Linkorb\TableArchiver\Services; +use Connector\Connector; use DateTimeImmutable; use DateTimeInterface; use Linkorb\TableArchiver\Dto\ArchiveDto; @@ -13,15 +14,17 @@ class ArchiverWorker { private OutputWriter $writer; + private Connector $connector; - public function __construct(OutputWriter $writer) + public function __construct(OutputWriter $writer, Connector $connector) { $this->writer = $writer; + $this->connector = $connector; } public function __invoke(string $query, ArchiveDto $dto, Channel $channel): void { - $pdo = $this->createPDO($dto); + $pdo = $this->connector->getPdo($this->connector->getConfig($dto->pdoDsn)); $pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false); @@ -38,11 +41,6 @@ public function __invoke(string $query, ArchiveDto $dto, Channel $channel): void $channel->send($rowsCount); } - protected function createPDO(ArchiveDto $dto): PDO - { - return new PDO($dto->pdoDsn, $dto->pdoUsername, $dto->pdoPassword); - } - private function fetchDateTime(array $row, ArchiveDto $dto): DateTimeInterface { $dateValue = $row[$dto->stampColumnName]; diff --git a/src/Services/OutputWriter.php b/src/Services/OutputWriter.php index a743b8e..8b537fc 100644 --- a/src/Services/OutputWriter.php +++ b/src/Services/OutputWriter.php @@ -6,7 +6,7 @@ use DateTimeInterface; use Exception; -use Linkorb\TableArchiver\Manager\TableArchiver; +use Linkorb\TableArchiver\Dto\ArchiveDto; class OutputWriter { @@ -47,11 +47,11 @@ public function setArchiveMode(int $archiveMode): void private function getFileName(DateTimeInterface $dateTime): string { switch ($this->archiveMode) { - case TableArchiver::YEAR: + case ArchiveDto::YEAR: return sprintf('%04d.ndjson', $dateTime->format('Y')); - case TableArchiver::YEAR_MONTH: + case ArchiveDto::YEAR_MONTH: return sprintf('%04d%02d.ndjson', $dateTime->format('Y'), $dateTime->format('m')); - case TableArchiver::YEAR_MONTH_DAY: + case ArchiveDto::YEAR_MONTH_DAY: return sprintf( '%04d%02d%02d.ndjson', $dateTime->format('Y'), diff --git a/src/Services/Supervisor.php b/src/Services/Supervisor.php index e6fc042..cc59d6b 100644 --- a/src/Services/Supervisor.php +++ b/src/Services/Supervisor.php @@ -20,10 +20,13 @@ class Supervisor private Channel $channel; + private Runtime $runtime; + public function __construct(callable $workerFactory) { $this->workerFactory = Closure::fromCallable($workerFactory); $this->channel = new Channel(); + $this->runtime = new Runtime(__DIR__ . '/../../vendor/autoload.php'); } public function spawn(array $args): void @@ -37,7 +40,7 @@ public function waitForFinish(): int while (count($this->futures) > 0) { foreach ($this->futures as $key => $future) { - if ($future->done()) { + if ($future->value()) { $totalRows += $this->channel->recv(); unset($this->futures[$key]); } @@ -51,8 +54,7 @@ public function waitForFinish(): int private function runWorker(array $args): Future { - $future = new Runtime(__DIR__ . '/../../vendor/autoload.php'); - - return $future->run(Closure::fromCallable($this->workerFactory->call($this)), [...$args, $this->channel]); + return $this->runtime + ->run(Closure::fromCallable($this->workerFactory->call($this)), [...$args, $this->channel]); } } diff --git a/tests/Manager/TableArchiverTest.php b/tests/Manager/TableArchiverTest.php index da43e5a..f6bbcf3 100644 --- a/tests/Manager/TableArchiverTest.php +++ b/tests/Manager/TableArchiverTest.php @@ -50,7 +50,7 @@ public function testArchiveTimestamp(): void $this->manager->method('createPDO')->willReturn($this->pdo); $dto = new ArchiveDto(); - $dto->archiveMode = TableArchiver::YEAR; + $dto->archiveMode = ArchiveDto::YEAR; $dto->stampColumnName = $this->getTimestampName(); $dto->tableName = $this->getTableName(); @@ -73,7 +73,7 @@ public function testArchiveDateTime(): void $this->manager->method('createPDO')->willReturn($this->pdo); $dto = new ArchiveDto(); - $dto->archiveMode = TableArchiver::YEAR_MONTH_DAY; + $dto->archiveMode = ArchiveDto::YEAR_MONTH_DAY; $dto->stampColumnName = $this->getDateTimeName(); $dto->tableName = $this->getTableName(); diff --git a/tests/Services/ArchiverWorkerTest.php b/tests/Services/ArchiverWorkerTest.php index 0b10427..a3874d8 100644 --- a/tests/Services/ArchiverWorkerTest.php +++ b/tests/Services/ArchiverWorkerTest.php @@ -5,7 +5,6 @@ namespace Linkorb\TableArchiver\Tests\Services; use Linkorb\TableArchiver\Dto\ArchiveDto; -use Linkorb\TableArchiver\Manager\TableArchiver; use Linkorb\TableArchiver\Services\ArchiverWorker; use Linkorb\TableArchiver\Services\OutputWriter; use Linkorb\TableArchiver\Tests\TestHelpers\DbSetupAwareTrait; @@ -39,7 +38,7 @@ public function testInvokeDateTimeYearMonthDay() $dto->isTimestamp = false; $dto->tableName = $this->getTableName(); $dto->stampColumnName = $this->getDateTimeName(); - $dto->archiveMode = TableArchiver::YEAR_MONTH_DAY; + $dto->archiveMode = ArchiveDto::YEAR_MONTH_DAY; $this->writer ->expects($this->exactly(2)) From 401c1ed1a180c0ba3c8321072ea76c8305620dbf Mon Sep 17 00:00:00 2001 From: amsprost Date: Fri, 2 Oct 2020 04:05:23 +0200 Subject: [PATCH 07/13] Finalize functionality --- src/Command/TableArchiveCommand.php | 16 +++++++++------ src/Dto/ArchiveDto.php | 9 ++++++++- src/Factory/QueryFactory.php | 31 +++++++++++++++++++++-------- src/Manager/TableArchiver.php | 16 +++++++-------- src/Services/OutputArchiver.php | 3 +++ src/Services/Supervisor.php | 23 ++++++++++++--------- 6 files changed, 66 insertions(+), 32 deletions(-) diff --git a/src/Command/TableArchiveCommand.php b/src/Command/TableArchiveCommand.php index d7452ad..3ed36ae 100644 --- a/src/Command/TableArchiveCommand.php +++ b/src/Command/TableArchiveCommand.php @@ -5,7 +5,7 @@ namespace Linkorb\TableArchiver\Command; use Connector\Connector; -use DateTime; +use DateTimeImmutable; use DateTimeInterface; use Linkorb\TableArchiver\Dto\ArchiveDto; use Linkorb\TableArchiver\Manager\TableArchiver; @@ -55,12 +55,14 @@ protected function execute(InputInterface $input, OutputInterface $output) $this->archiver->archiveExportedFiles(); $helper = $this->getHelper('question'); - $question = new ConfirmationQuestion('All records archived. Do you want to proceed with removal?', false); + $question = new ConfirmationQuestion('All records archived. Do you want to proceed with removal? (y/N)', false); - $output->write(sprintf('%d records have been processed', $count)); - if (!$helper->ask($input, $output, $question)) { + $output->writeln(sprintf('%d records have been processed', $count)); + if ($helper->ask($input, $output, $question)) { $this->archiver->flushArchived($pdo, $dto, $count); } + + return 0; } private function createDto(InputInterface $input, OutputInterface $output): ArchiveDto @@ -84,12 +86,14 @@ private function createDto(InputInterface $input, OutputInterface $output): Arch $dto->stampColumnName = $input->getArgument('columnName'); $datetime = $input->getArgument('maxStamp') ? - DateTime::createFromFormat('Ymd', $input->getArgument('maxStamp')) : null; + DateTimeImmutable::createFromFormat('Ymd', $input->getArgument('maxStamp')) : null; if ($datetime === false) { $output->write('Incorrect max stamp passed'); return -1; } - $dto->maxStamp = $datetime instanceof DateTimeInterface ? $datetime->setTime(0, 0) : $datetime; + $dto->maxStamp = $datetime instanceof DateTimeInterface ? + $datetime->setTime(23, 59, 59, 999999)->format('Y-m-d H:i:s') : + $datetime; return $dto; } diff --git a/src/Dto/ArchiveDto.php b/src/Dto/ArchiveDto.php index ad0be18..d7feeab 100644 --- a/src/Dto/ArchiveDto.php +++ b/src/Dto/ArchiveDto.php @@ -17,5 +17,12 @@ class ArchiveDto public int $archiveMode; public string $stampColumnName; public bool $isTimestamp = false; - public ?DateTimeImmutable $maxStamp = null; + public ?string $maxStamp = null; + + public function getStampDateTime(): ?DateTimeImmutable + { + return $this->maxStamp !== null ? + DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $this->maxStamp) : + $this->maxStamp; + } } diff --git a/src/Factory/QueryFactory.php b/src/Factory/QueryFactory.php index 5b2b184..c721389 100644 --- a/src/Factory/QueryFactory.php +++ b/src/Factory/QueryFactory.php @@ -21,15 +21,22 @@ public function buildFetchQuery( if ($maxStamp) { $query .= ' WHERE `%s` < \'%s\''; - $params = [...$params, $stampColumnName, $isTimestamp ? $maxStamp->getTimestamp() : $maxStamp]; - } - - if (!is_null($limit)) { - $query .= ' LIMIT %d'; - $params = [...$params, $limit]; + $params = [ + ...$params, + $stampColumnName, + $isTimestamp ? $maxStamp->getTimestamp() : $maxStamp->format('Y-m-d H:i:s'), + ]; } if (!is_null($offset)) { + if (!is_null($limit)) { + $query .= ' LIMIT %d'; + $params = [...$params, $limit]; + } else { + $query .= ' LIMIT %d'; + $params = [...$params, 12340283492834]; + } + $query .= ' OFFSET %d'; $params = [...$params, $offset]; } @@ -48,7 +55,11 @@ public function buildCountQuery( if ($maxStamp) { $query .= ' WHERE `%s` < \'%s\''; - $params = [...$params, $stampColumnName, $isTimestamp ? $maxStamp->getTimestamp() : $maxStamp]; + $params = [ + ...$params, + $stampColumnName, + $isTimestamp ? $maxStamp->getTimestamp() : $maxStamp->format('Y-m-d H:i:s'), + ]; } return sprintf($query, ...$params); @@ -65,7 +76,11 @@ public function buildDeleteQuery( if ($maxStamp) { $query .= ' WHERE `%s` < \'%s\''; - $params = [...$params, $stampColumnName, $isTimestamp ? $maxStamp->getTimestamp() : $maxStamp]; + $params = [ + ...$params, + $stampColumnName, + $isTimestamp ? $maxStamp->getTimestamp() : $maxStamp->format('Y-m-d H:i:s'), + ]; } return sprintf($query, ...$params); diff --git a/src/Manager/TableArchiver.php b/src/Manager/TableArchiver.php index 4fc6e28..953aa41 100644 --- a/src/Manager/TableArchiver.php +++ b/src/Manager/TableArchiver.php @@ -43,18 +43,18 @@ public function archive(PDO $pdo, ArchiveDto $dto): int $this->detectColumnType($pdo, $dto); - $count = $pdo->query( + $count = (int)$pdo->query( $this->queryFactory->buildCountQuery( $dto->tableName, $dto->stampColumnName, - $dto->maxStamp, + $dto->getStampDateTime(), $dto->isTimestamp ), PDO::FETCH_COLUMN, 0 )->fetch(); - $this->spawnWorkers($dto, (int)$count); + $this->spawnWorkers($dto, $count); if ($count !== $this->supervisor->waitForFinish()) { throw new LogicException('Number of found and processed rows isn\'t match'); @@ -72,11 +72,11 @@ public function flushArchived(PDO $pdo, ArchiveDto $dto, int $rowsArchived): voi { $pdo->beginTransaction(); - $count = $pdo->query( + $count = (int)$pdo->query( $this->queryFactory->buildCountQuery( $dto->tableName, $dto->stampColumnName, - $dto->maxStamp, + $dto->getStampDateTime(), $dto->isTimestamp ), PDO::FETCH_COLUMN, @@ -91,7 +91,7 @@ public function flushArchived(PDO $pdo, ArchiveDto $dto, int $rowsArchived): voi $this->queryFactory->buildDeleteQuery( $dto->tableName, $dto->stampColumnName, - $dto->maxStamp, + $dto->getStampDateTime(), $dto->isTimestamp ) ); @@ -109,7 +109,7 @@ private function spawnWorkers(ArchiveDto $dto, int $count): void $dto->stampColumnName, $offset, $this->batchSize, - $dto->maxStamp, + $dto->getStampDateTime(), $dto->isTimestamp ), $dto @@ -124,7 +124,7 @@ private function spawnWorkers(ArchiveDto $dto, int $count): void $dto->stampColumnName, $offset, null, - $dto->maxStamp, + $dto->getStampDateTime(), $dto->isTimestamp ), $dto diff --git a/src/Services/OutputArchiver.php b/src/Services/OutputArchiver.php index d915260..71ce916 100644 --- a/src/Services/OutputArchiver.php +++ b/src/Services/OutputArchiver.php @@ -4,6 +4,8 @@ namespace Linkorb\TableArchiver\Services; +use Exception; + class OutputArchiver { private string $basePath; @@ -27,6 +29,7 @@ public function archive(): void } $this->gzCompressFile($filePath); + unlink($filePath); } closedir($dh); diff --git a/src/Services/Supervisor.php b/src/Services/Supervisor.php index cc59d6b..4272c58 100644 --- a/src/Services/Supervisor.php +++ b/src/Services/Supervisor.php @@ -5,6 +5,8 @@ namespace Linkorb\TableArchiver\Services; use Closure; +use Linkorb\TableArchiver\Dto\ArchiveDto; +use Linkorb\TableArchiver\Factory\ArchiverWorkerFactory; use parallel\Channel; use parallel\Future; use parallel\Runtime; @@ -38,23 +40,26 @@ public function waitForFinish(): int { $totalRows = 0; - while (count($this->futures) > 0) { - foreach ($this->futures as $key => $future) { - if ($future->value()) { - $totalRows += $this->channel->recv(); - unset($this->futures[$key]); - } - } + foreach ($this->futures as $future) { + $totalRows += $this->channel->recv(); + $future->value(); } $this->channel->close(); + $this->futures = []; return $totalRows; } private function runWorker(array $args): Future { - return $this->runtime - ->run(Closure::fromCallable($this->workerFactory->call($this)), [...$args, $this->channel]); + $workerFactory = $this->workerFactory; + + return $this->runtime->run( + function (string $query, ArchiveDto $dto, Channel $channel) use ($workerFactory) { + return ($workerFactory->call(new ArchiverWorkerFactory()))($query, $dto, $channel); + }, + [...$args, $this->channel] + ); } } From 0c57cfc202e246dfd3b790a8630f78d6532680da Mon Sep 17 00:00:00 2001 From: amsprost Date: Sun, 11 Oct 2020 04:05:30 +0200 Subject: [PATCH 08/13] docker-compose, README, old-new write implementation started --- README.md | 25 ++++++++ composer.json | 3 +- docker-compose.yaml | 9 +++ src/Command/TableArchiveCommand.php | 12 +++- src/Services/ArchiverWorker.php | 15 +---- src/Services/DateTimeHelper.php | 23 ++++++++ src/Services/FileLineBisectionHelper.php | 72 ++++++++++++++++++++++++ src/Services/OutputWriter.php | 20 +++++-- 8 files changed, 156 insertions(+), 23 deletions(-) create mode 100644 README.md create mode 100644 docker-compose.yaml create mode 100644 src/Services/DateTimeHelper.php create mode 100644 src/Services/FileLineBisectionHelper.php diff --git a/README.md b/README.md new file mode 100644 index 0000000..f4ed8fa --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +Table Archiver +============== +This is a CLI tool to export old data from any SQL database table into ranged ndjson files +that you can then wherever you like. + +## Installation + + git clone git@github.com:linkorb/table-archiver.git + cd table-archiver + docker-compose up -d + docker-compose exec app composer install # install PHP dependencies + +## Usage +Command looks like: + + ./bin/console linkorb:table:archive {db_dsn} {table_name} {mode} {date_column} [max_stamp]\ +See `./bin/console linkorb:table:archive --help` for more info + +(To run it from docker container you need to prepend it with `docker-compose exec app `...) + +All gzipped [ndjson](http://ndjson.org/) files can be found under `./output` directory + +### Example: + + ./bin/console linkorb:table:archive mysql://root:root@127.0.0.1:3306/test target_table YEAR_MONTH_DAY timestamp 20200101 diff --git a/composer.json b/composer.json index 0386d78..204ecea 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,8 @@ "symfony/dotenv": "5.1.*", "symfony/flex": "^1.3.1", "symfony/framework-bundle": "5.1.*", - "symfony/yaml": "5.1.*" + "symfony/yaml": "5.1.*", + "ext-zlib": "*" }, "require-dev": { "phpunit/phpunit": "^9", diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..ec0e9b6 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,9 @@ +version: '3' +services: + app: + build: + context: . + network_mode: "host" + volumes: + - ./:/app + command: tail -F /dev/null diff --git a/src/Command/TableArchiveCommand.php b/src/Command/TableArchiveCommand.php index 3ed36ae..6e11b1b 100644 --- a/src/Command/TableArchiveCommand.php +++ b/src/Command/TableArchiveCommand.php @@ -41,8 +41,16 @@ protected function configure() InputArgument::REQUIRED, 'Date range for which archived cluster will be created. Allowed values: YEAR, YEAR_MONTH, YEAR_MONTH_DAY' ) - ->addArgument('columnName', InputArgument::REQUIRED, 'Column which contains date information') - ->addArgument('maxStamp', InputArgument::OPTIONAL, 'Archive records which older than specified date'); + ->addArgument( + 'columnName', + InputArgument::REQUIRED, + 'Column which contains date information. It may be a date, datetime or int (unix timestamp) column, this is auto-detected' + ) + ->addArgument( + 'maxStamp', + InputArgument::OPTIONAL, + 'Archive records which older than specified date. Data newer than this date is not archived, and kept in the database' + ); } protected function execute(InputInterface $input, OutputInterface $output) diff --git a/src/Services/ArchiverWorker.php b/src/Services/ArchiverWorker.php index 23e64ca..2b6e1ab 100644 --- a/src/Services/ArchiverWorker.php +++ b/src/Services/ArchiverWorker.php @@ -5,8 +5,6 @@ namespace Linkorb\TableArchiver\Services; use Connector\Connector; -use DateTimeImmutable; -use DateTimeInterface; use Linkorb\TableArchiver\Dto\ArchiveDto; use parallel\Channel; use PDO; @@ -35,20 +33,9 @@ public function __invoke(string $query, ArchiveDto $dto, Channel $channel): void $rowsCount = 0; while ($row = $pdoStatement->fetch(PDO::FETCH_ASSOC)) { ++$rowsCount; - $this->writer->write($row, $this->fetchDateTime($row, $dto)); + $this->writer->write($row, $dto); } $channel->send($rowsCount); } - - private function fetchDateTime(array $row, ArchiveDto $dto): DateTimeInterface - { - $dateValue = $row[$dto->stampColumnName]; - - if ($dto->isTimestamp) { - return (new DateTimeImmutable())->setTimestamp($dateValue); - } - - return DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $dateValue); - } } diff --git a/src/Services/DateTimeHelper.php b/src/Services/DateTimeHelper.php new file mode 100644 index 0000000..721d7ba --- /dev/null +++ b/src/Services/DateTimeHelper.php @@ -0,0 +1,23 @@ +stampColumnName]; + + if ($dto->isTimestamp) { + return (new DateTimeImmutable())->setTimestamp($dateValue); + } + + return DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $dateValue); + } +} diff --git a/src/Services/FileLineBisectionHelper.php b/src/Services/FileLineBisectionHelper.php new file mode 100644 index 0000000..3b9a634 --- /dev/null +++ b/src/Services/FileLineBisectionHelper.php @@ -0,0 +1,72 @@ +fseek($fileObject->getSize()); + $sectorLength = $fileObject->key(); + + if (!is_null($pos = static::tryOptimistic($fileObject, $dateTime, $dto))) { + return $pos; + } + } + + $sectorLength = (int) ceil($sectorLength / 2); + $positionCandidate = $offset + ($direction <=> 0) * $sectorLength; + + try { + $fileObject->seek($positionCandidate - 1); + + $prevVal = static::strToDate($fileObject->fgets()?: null, $dto); + } catch (LogicException $e) { + $prevVal = null; + $fileObject->seek(0); + } + + $nextVal = !$fileObject->eof() ? static::strToDate($fileObject->fgets()?: null, $dto) : null; + + switch (true) { + case $prevVal && $nextVal && $nextVal < $dateTime: + return static::getLine($fileObject, $dateTime, $dto, $sectorLength, $positionCandidate, 1); + case $prevVal && $nextVal && $prevVal > $dateTime: + return static::getLine($fileObject, $dateTime, $dto, $sectorLength, $positionCandidate, -1); + case !$prevVal && !$nextVal: + return 0; + default: + return $positionCandidate; + } + } + + private static function tryOptimistic(SplFileObject $file, DateTimeInterface $dateTime, ArchiveDto $dto): ?int + { + $endValue = static::strToDate($file->current()?: null, $dto); + if ($dateTime > $endValue) { + return $file->key(); + } + + return null; + } + + private static function strToDate(?string $str, ArchiveDto $dto): ?DateTimeInterface + { + return $str ? DateTimeHelper::fetchDateTime(json_decode($str), $dto) : null; + } +} diff --git a/src/Services/OutputWriter.php b/src/Services/OutputWriter.php index 8b537fc..ff88719 100644 --- a/src/Services/OutputWriter.php +++ b/src/Services/OutputWriter.php @@ -7,12 +7,13 @@ use DateTimeInterface; use Exception; use Linkorb\TableArchiver\Dto\ArchiveDto; +use SplFileObject; class OutputWriter { private string $basePath; private int $archiveMode; - /** @var resource[] */ + /** @var SplFileObject[] */ private array $fileResources = []; public function __construct(string $basePath) @@ -22,21 +23,22 @@ public function __construct(string $basePath) public function __destruct() { - foreach ($this->fileResources as $fileResource) { - fclose($fileResource); + foreach ($this->fileResources as $key => $fileResource) { + unset($this->fileResources[$key]); } } - public function write(array $row, DateTimeInterface $dateTime): void + public function write(array $row, ArchiveDto $dto): void { + $dateTime = DateTimeHelper::fetchDateTime($row, $dto); $name = $this->getFileName($dateTime); if (!isset($this->fileResources[$name])) { - $fp = fopen($this->outputPath($name), 'a'); + $fp = new SplFileObject($this->outputPath($name), 'r+'); $this->fileResources[$name] = $fp; } - fwrite($this->fileResources[$name], json_encode($row) . "\n"); + $this->fileResources[$name]->fwrite(json_encode($row) . "\n"); } public function setArchiveMode(int $archiveMode): void @@ -67,4 +69,10 @@ protected function outputPath(string $name): string { return $this->basePath . DIRECTORY_SEPARATOR . $name; } + + private function getLinePosition(SplFileObject $fileObject, DateTimeInterface $dateTime): int + { + $length = count($fileObject); + + } } From 2862c50e7fbb9cc6031bef0d4e7c5fe7f477168e Mon Sep 17 00:00:00 2001 From: amsprost Date: Mon, 19 Oct 2020 03:54:33 +0200 Subject: [PATCH 09/13] Refactor closure in class, Implement sorting by timestap in resulting set --- .env | 2 +- README.md | 2 + config/services.yaml | 6 +- src/Command/TableArchiveCommand.php | 4 +- src/Factory/ArchiverWorkerFactory.php | 7 +- src/Factory/QueryFactory.php | 16 +---- src/Manager/TableArchiver.php | 82 +++++++----------------- src/Services/ArchiverWorker.php | 29 ++++----- src/Services/FileLineBisectionHelper.php | 72 --------------------- src/Services/OutputWriter.php | 8 +-- src/Services/Supervisor.php | 46 ++++++------- 11 files changed, 74 insertions(+), 200 deletions(-) delete mode 100644 src/Services/FileLineBisectionHelper.php diff --git a/.env b/.env index 5db3684..2b78338 100644 --- a/.env +++ b/.env @@ -20,4 +20,4 @@ APP_SECRET=c7bb60cab041aecda02f06effa4c969b #TRUSTED_HOSTS='^(localhost|example\.com)$' ###< symfony/framework-bundle ### -APP_THREAD_BATCH_SIZE=2 +APP_THREADS_NUMBER=5 diff --git a/README.md b/README.md index f4ed8fa..e25bce3 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,8 @@ See `./bin/console linkorb:table:archive --help` for more info All gzipped [ndjson](http://ndjson.org/) files can be found under `./output` directory +If you want to change threads number you can do that easily by changing `APP_THREADS_NUMBER` in `.env` + ### Example: ./bin/console linkorb:table:archive mysql://root:root@127.0.0.1:3306/test target_table YEAR_MONTH_DAY timestamp 20200101 diff --git a/config/services.yaml b/config/services.yaml index 293bcb8..a24316b 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -23,11 +23,11 @@ services: Linkorb\TableArchiver\Services\Supervisor: arguments: - $workerFactory: !service + $processingWorkerFactory: !service factory: [ '@Linkorb\TableArchiver\Factory\ArchiverWorkerFactory', 'createFactoryMethod' ] arguments: - '@Linkorb\TableArchiver\Services\OutputWriter' - - '@Connector\Connector' + $threadsNumber: '%env(APP_THREADS_NUMBER)%' Linkorb\TableArchiver\Services\OutputWriter: arguments: @@ -39,6 +39,6 @@ services: Linkorb\TableArchiver\Manager\TableArchiver: arguments: - $batchSize: '%env(APP_THREAD_BATCH_SIZE)%' + $processingThreadsNumber: '%env(APP_THREADS_NUMBER)%' Connector\Connector: diff --git a/src/Command/TableArchiveCommand.php b/src/Command/TableArchiveCommand.php index 6e11b1b..df22624 100644 --- a/src/Command/TableArchiveCommand.php +++ b/src/Command/TableArchiveCommand.php @@ -59,6 +59,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $pdo = $this->connector->getPdo($this->connector->getConfig($dto->pdoDsn)); + $pdo->beginTransaction(); $count = $this->archiver->archive($pdo, $dto); $this->archiver->archiveExportedFiles(); @@ -67,8 +68,9 @@ protected function execute(InputInterface $input, OutputInterface $output) $output->writeln(sprintf('%d records have been processed', $count)); if ($helper->ask($input, $output, $question)) { - $this->archiver->flushArchived($pdo, $dto, $count); + $this->archiver->flushArchived($pdo, $dto); } + $pdo->commit(); return 0; } diff --git a/src/Factory/ArchiverWorkerFactory.php b/src/Factory/ArchiverWorkerFactory.php index 710530e..37b660f 100644 --- a/src/Factory/ArchiverWorkerFactory.php +++ b/src/Factory/ArchiverWorkerFactory.php @@ -4,16 +4,15 @@ namespace Linkorb\TableArchiver\Factory; -use Connector\Connector; use Linkorb\TableArchiver\Services\ArchiverWorker; use Linkorb\TableArchiver\Services\OutputWriter; class ArchiverWorkerFactory { - public function createFactoryMethod(OutputWriter $writer, Connector $connector): callable + public function createFactoryMethod(OutputWriter $writer): callable { - return function () use ($writer, $connector) { - return new ArchiverWorker($writer, $connector); + return function () use ($writer) { + return new ArchiverWorker($writer); }; } } diff --git a/src/Factory/QueryFactory.php b/src/Factory/QueryFactory.php index c721389..335a7c5 100644 --- a/src/Factory/QueryFactory.php +++ b/src/Factory/QueryFactory.php @@ -11,8 +11,6 @@ class QueryFactory public function buildFetchQuery( string $tableName, string $stampColumnName, - ?int $offset, - ?int $limit, ?DateTimeImmutable $maxStamp, bool $isTimestamp ): string { @@ -28,18 +26,8 @@ public function buildFetchQuery( ]; } - if (!is_null($offset)) { - if (!is_null($limit)) { - $query .= ' LIMIT %d'; - $params = [...$params, $limit]; - } else { - $query .= ' LIMIT %d'; - $params = [...$params, 12340283492834]; - } - - $query .= ' OFFSET %d'; - $params = [...$params, $offset]; - } + $query .= ' ORDER BY `%s` ASC'; + $params = [...$params, $stampColumnName]; return sprintf($query, ...$params); } diff --git a/src/Manager/TableArchiver.php b/src/Manager/TableArchiver.php index 953aa41..a1c9ac2 100644 --- a/src/Manager/TableArchiver.php +++ b/src/Manager/TableArchiver.php @@ -4,8 +4,6 @@ namespace Linkorb\TableArchiver\Manager; -use BadFunctionCallException; -use InvalidArgumentException; use Linkorb\TableArchiver\Dto\ArchiveDto; use Linkorb\TableArchiver\Factory\QueryFactory; use Linkorb\TableArchiver\Services\OutputArchiver; @@ -21,25 +19,23 @@ class TableArchiver private OutputArchiver $outputArchiver; - private int $batchSize; + private int $processingThreadsNumber; public function __construct( QueryFactory $queryFactory, Supervisor $supervisor, OutputArchiver $outputArchiver, - int $batchSize + int $processingThreadsNumber ) { $this->queryFactory = $queryFactory; $this->supervisor = $supervisor; $this->outputArchiver = $outputArchiver; - $this->batchSize = $batchSize; + $this->processingThreadsNumber = $processingThreadsNumber; } public function archive(PDO $pdo, ArchiveDto $dto): int { - if (false === $pdo->query('SELECT 1')->fetch()) { - throw new BadFunctionCallException('Something is wrong with your PDO connection'); - } + $pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false); $this->detectColumnType($pdo, $dto); @@ -54,10 +50,21 @@ public function archive(PDO $pdo, ArchiveDto $dto): int 0 )->fetch(); - $this->spawnWorkers($dto, $count); + $this->spawnWorkers($dto); + + $pdoStatement = $pdo->query($this->queryFactory->buildFetchQuery( + $dto->tableName, + $dto->stampColumnName, + $dto->getStampDateTime(), + $dto->isTimestamp + )); + + while ($row = $pdoStatement->fetch(PDO::FETCH_ASSOC)) { + $this->supervisor->delegate($row); + } - if ($count !== $this->supervisor->waitForFinish()) { - throw new LogicException('Number of found and processed rows isn\'t match'); + if ($count !== $this->supervisor->terminateThreads()) { + throw new LogicException('Number of found and processed rows aren\'t match'); } return $count; @@ -68,25 +75,8 @@ public function archiveExportedFiles(): void $this->outputArchiver->archive(); } - public function flushArchived(PDO $pdo, ArchiveDto $dto, int $rowsArchived): void + public function flushArchived(PDO $pdo, ArchiveDto $dto): void { - $pdo->beginTransaction(); - - $count = (int)$pdo->query( - $this->queryFactory->buildCountQuery( - $dto->tableName, - $dto->stampColumnName, - $dto->getStampDateTime(), - $dto->isTimestamp - ), - PDO::FETCH_COLUMN, - 0 - )->fetch(); - - if ($count !== $rowsArchived) { - throw new InvalidArgumentException('Number of archived rows and marked for deletion mismatch'); - } - $pdo->exec( $this->queryFactory->buildDeleteQuery( $dto->tableName, @@ -95,41 +85,13 @@ public function flushArchived(PDO $pdo, ArchiveDto $dto, int $rowsArchived): voi $dto->isTimestamp ) ); - - $pdo->commit(); } - private function spawnWorkers(ArchiveDto $dto, int $count): void + private function spawnWorkers(ArchiveDto $dto): void { - for ($offset = 0; $offset < $count - $this->batchSize; $offset += $this->batchSize) { - $this->supervisor->spawn( - [ - $this->queryFactory->buildFetchQuery( - $dto->tableName, - $dto->stampColumnName, - $offset, - $this->batchSize, - $dto->getStampDateTime(), - $dto->isTimestamp - ), - $dto - ] - ); + for ($i = 0; $i < $this->processingThreadsNumber; $i++) { + $this->supervisor->spawnProcessing([$dto]); } - - $this->supervisor->spawn( - [ - $this->queryFactory->buildFetchQuery( - $dto->tableName, - $dto->stampColumnName, - $offset, - null, - $dto->getStampDateTime(), - $dto->isTimestamp - ), - $dto - ] - ); } private function detectColumnType(PDO $pdo, ArchiveDto $dto): void diff --git a/src/Services/ArchiverWorker.php b/src/Services/ArchiverWorker.php index 2b6e1ab..92cb9dd 100644 --- a/src/Services/ArchiverWorker.php +++ b/src/Services/ArchiverWorker.php @@ -4,38 +4,35 @@ namespace Linkorb\TableArchiver\Services; -use Connector\Connector; use Linkorb\TableArchiver\Dto\ArchiveDto; use parallel\Channel; -use PDO; +use parallel\Channel\Error\Closed; class ArchiverWorker { private OutputWriter $writer; - private Connector $connector; - public function __construct(OutputWriter $writer, Connector $connector) + public function __construct(OutputWriter $writer) { $this->writer = $writer; - $this->connector = $connector; } - public function __invoke(string $query, ArchiveDto $dto, Channel $channel): void + public function __invoke(ArchiveDto $dto, Channel $channel): int { - $pdo = $this->connector->getPdo($this->connector->getConfig($dto->pdoDsn)); - - $pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false); + $rowsProcessed = 0; $this->writer->setArchiveMode($dto->archiveMode); - $pdoStatement = $pdo->query($query); - - $rowsCount = 0; - while ($row = $pdoStatement->fetch(PDO::FETCH_ASSOC)) { - ++$rowsCount; - $this->writer->write($row, $dto); + try { + while ($row = $channel->recv()) { + $this->writer->write($row, $dto); + $rowsProcessed++; + } + } catch (Closed $e) { + // channel is closed worker should be stopped + return $rowsProcessed; } - $channel->send($rowsCount); + return $rowsProcessed; } } diff --git a/src/Services/FileLineBisectionHelper.php b/src/Services/FileLineBisectionHelper.php deleted file mode 100644 index 3b9a634..0000000 --- a/src/Services/FileLineBisectionHelper.php +++ /dev/null @@ -1,72 +0,0 @@ -fseek($fileObject->getSize()); - $sectorLength = $fileObject->key(); - - if (!is_null($pos = static::tryOptimistic($fileObject, $dateTime, $dto))) { - return $pos; - } - } - - $sectorLength = (int) ceil($sectorLength / 2); - $positionCandidate = $offset + ($direction <=> 0) * $sectorLength; - - try { - $fileObject->seek($positionCandidate - 1); - - $prevVal = static::strToDate($fileObject->fgets()?: null, $dto); - } catch (LogicException $e) { - $prevVal = null; - $fileObject->seek(0); - } - - $nextVal = !$fileObject->eof() ? static::strToDate($fileObject->fgets()?: null, $dto) : null; - - switch (true) { - case $prevVal && $nextVal && $nextVal < $dateTime: - return static::getLine($fileObject, $dateTime, $dto, $sectorLength, $positionCandidate, 1); - case $prevVal && $nextVal && $prevVal > $dateTime: - return static::getLine($fileObject, $dateTime, $dto, $sectorLength, $positionCandidate, -1); - case !$prevVal && !$nextVal: - return 0; - default: - return $positionCandidate; - } - } - - private static function tryOptimistic(SplFileObject $file, DateTimeInterface $dateTime, ArchiveDto $dto): ?int - { - $endValue = static::strToDate($file->current()?: null, $dto); - if ($dateTime > $endValue) { - return $file->key(); - } - - return null; - } - - private static function strToDate(?string $str, ArchiveDto $dto): ?DateTimeInterface - { - return $str ? DateTimeHelper::fetchDateTime(json_decode($str), $dto) : null; - } -} diff --git a/src/Services/OutputWriter.php b/src/Services/OutputWriter.php index ff88719..579d84f 100644 --- a/src/Services/OutputWriter.php +++ b/src/Services/OutputWriter.php @@ -34,7 +34,7 @@ public function write(array $row, ArchiveDto $dto): void $name = $this->getFileName($dateTime); if (!isset($this->fileResources[$name])) { - $fp = new SplFileObject($this->outputPath($name), 'r+'); + $fp = new SplFileObject($this->outputPath($name), 'a'); $this->fileResources[$name] = $fp; } @@ -69,10 +69,4 @@ protected function outputPath(string $name): string { return $this->basePath . DIRECTORY_SEPARATOR . $name; } - - private function getLinePosition(SplFileObject $fileObject, DateTimeInterface $dateTime): int - { - $length = count($fileObject); - - } } diff --git a/src/Services/Supervisor.php b/src/Services/Supervisor.php index 4272c58..10a6db5 100644 --- a/src/Services/Supervisor.php +++ b/src/Services/Supervisor.php @@ -6,7 +6,6 @@ use Closure; use Linkorb\TableArchiver\Dto\ArchiveDto; -use Linkorb\TableArchiver\Factory\ArchiverWorkerFactory; use parallel\Channel; use parallel\Future; use parallel\Runtime; @@ -16,48 +15,51 @@ class Supervisor /** * @var Future[] */ - private array $futures = []; + private array $processingFutures = []; - private Closure $workerFactory; + private Closure $processingWorkerFactory; private Channel $channel; private Runtime $runtime; - public function __construct(callable $workerFactory) + public function __construct(callable $processingWorkerFactory, int $threadsNumber) { - $this->workerFactory = Closure::fromCallable($workerFactory); - $this->channel = new Channel(); - $this->runtime = new Runtime(__DIR__ . '/../../vendor/autoload.php'); + $this->processingWorkerFactory = Closure::fromCallable($processingWorkerFactory); + $this->channel = Channel::make('data_transfer', $threadsNumber); + $this->runtime = new Runtime(__DIR__ . '/../../vendor/autoload.php'); } - public function spawn(array $args): void + public function spawnProcessing(array $args): void { - $this->futures[] = $this->runWorker($args); + $this->processingFutures[] = $this->runProcessingWorker($args); } - public function waitForFinish(): int + public function delegate(array $row): void { - $totalRows = 0; - - foreach ($this->futures as $future) { - $totalRows += $this->channel->recv(); - $future->value(); - } + $this->channel->send($row); + } + public function terminateThreads(): int + { $this->channel->close(); - $this->futures = []; - return $totalRows; + $processedRows = 0; + + foreach ($this->processingFutures as $future) { + $processedRows += $future->value(); + } + + return $processedRows; } - private function runWorker(array $args): Future + private function runProcessingWorker(array $args): Future { - $workerFactory = $this->workerFactory; + $workerFactory = $this->processingWorkerFactory; return $this->runtime->run( - function (string $query, ArchiveDto $dto, Channel $channel) use ($workerFactory) { - return ($workerFactory->call(new ArchiverWorkerFactory()))($query, $dto, $channel); + function (ArchiveDto $dto, Channel $channel) use ($workerFactory) { + return ($workerFactory())($dto, $channel); }, [...$args, $this->channel] ); From 5246fab0491d6f9ff213956f69a7630f22633732 Mon Sep 17 00:00:00 2001 From: amsprost Date: Sun, 25 Oct 2020 02:43:16 +0200 Subject: [PATCH 10/13] Add --no-cache option --- README.md | 4 ++++ src/Command/TableArchiveCommand.php | 17 ++++++++++++++++- src/Services/OutputWriter.php | 23 +++++++++++++++++++---- 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index e25bce3..1db3405 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,10 @@ All gzipped [ndjson](http://ndjson.org/) files can be found under `./output` dir If you want to change threads number you can do that easily by changing `APP_THREADS_NUMBER` in `.env` +With big date ranges it's recommended to run command with disabled (or increased) memory limit +(prepended `php -d memory_limit=-1`). If that's not an option you can bypass caching for writers by passing `no-cache` +option. Be aware that this setting noticeable affects performance. + ### Example: ./bin/console linkorb:table:archive mysql://root:root@127.0.0.1:3306/test target_table YEAR_MONTH_DAY timestamp 20200101 diff --git a/src/Command/TableArchiveCommand.php b/src/Command/TableArchiveCommand.php index df22624..2e3a181 100644 --- a/src/Command/TableArchiveCommand.php +++ b/src/Command/TableArchiveCommand.php @@ -9,9 +9,11 @@ use DateTimeInterface; use Linkorb\TableArchiver\Dto\ArchiveDto; use Linkorb\TableArchiver\Manager\TableArchiver; +use Linkorb\TableArchiver\Services\OutputWriter; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ConfirmationQuestion; @@ -23,10 +25,13 @@ final class TableArchiveCommand extends Command private Connector $connector; - public function __construct(TableArchiver $archiver, Connector $connector) + private OutputWriter $writer; + + public function __construct(TableArchiver $archiver, Connector $connector, OutputWriter $writer) { $this->archiver = $archiver; $this->connector = $connector; + $this->writer = $writer; parent::__construct(); } @@ -50,6 +55,12 @@ protected function configure() 'maxStamp', InputArgument::OPTIONAL, 'Archive records which older than specified date. Data newer than this date is not archived, and kept in the database' + ) + ->addOption( + 'no-cache', + null, + InputOption::VALUE_NONE, + 'Disables caching of file resource descriptors. Could be used in case of memory limit exceeded / memory leakage. Affects performance' ); } @@ -59,6 +70,10 @@ protected function execute(InputInterface $input, OutputInterface $output) $pdo = $this->connector->getPdo($this->connector->getConfig($dto->pdoDsn)); + if ($input->getOption('no-cache')) { + $this->writer->disableCache(); + } + $pdo->beginTransaction(); $count = $this->archiver->archive($pdo, $dto); $this->archiver->archiveExportedFiles(); diff --git a/src/Services/OutputWriter.php b/src/Services/OutputWriter.php index 579d84f..9bbc00b 100644 --- a/src/Services/OutputWriter.php +++ b/src/Services/OutputWriter.php @@ -15,6 +15,7 @@ class OutputWriter private int $archiveMode; /** @var SplFileObject[] */ private array $fileResources = []; + private bool $disableCache = false; public function __construct(string $basePath) { @@ -33,12 +34,21 @@ public function write(array $row, ArchiveDto $dto): void $dateTime = DateTimeHelper::fetchDateTime($row, $dto); $name = $this->getFileName($dateTime); - if (!isset($this->fileResources[$name])) { - $fp = new SplFileObject($this->outputPath($name), 'a'); - $this->fileResources[$name] = $fp; + switch (true) { + case !$this->disableCache && !isset($this->fileResources[$name]): + $fp = new SplFileObject($this->outputPath($name), 'a'); + $this->fileResources[$name] = $fp; + break; + case !$this->disableCache && isset($this->fileResources[$name]): + $fp = $this->fileResources[$name]; + break; + case $this->disableCache: + default: + $fp = new SplFileObject($this->outputPath($name), 'a'); + break; } - $this->fileResources[$name]->fwrite(json_encode($row) . "\n"); + $fp->fwrite(json_encode($row) . "\n"); } public function setArchiveMode(int $archiveMode): void @@ -46,6 +56,11 @@ public function setArchiveMode(int $archiveMode): void $this->archiveMode = $archiveMode; } + public function disableCache(): void + { + $this->disableCache = true; + } + private function getFileName(DateTimeInterface $dateTime): string { switch ($this->archiveMode) { From d6fc71042d1222cc554b39c59b8ff80b6d0adce5 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 25 Oct 2020 01:26:10 +0000 Subject: [PATCH 11/13] feat: run multi-repo --- .editorconfig | 12 + .gitignore | 2 + .hooks/pre-push.sample | 1 + .versionrc | 36 ++ README.md | 4 + commitlint.config.js | 3 + composer.json | 24 +- composer.lock | 669 ++++++++++++++++++- package-lock.json | 1387 ++++++++++++++++++++++++++++++++++++++++ package.json | 16 + phpcs.xml.dist | 19 + 11 files changed, 2168 insertions(+), 5 deletions(-) create mode 100644 .editorconfig create mode 100644 .hooks/pre-push.sample create mode 100644 .versionrc create mode 100644 commitlint.config.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 phpcs.xml.dist diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6cbc618 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.php] +indent_size = 4 diff --git a/.gitignore b/.gitignore index 40d6291..b61a6a8 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,5 @@ output/* !.gitkeep + +node_modules diff --git a/.hooks/pre-push.sample b/.hooks/pre-push.sample new file mode 100644 index 0000000..6daa0e4 --- /dev/null +++ b/.hooks/pre-push.sample @@ -0,0 +1 @@ +composer run qa-checks diff --git a/.versionrc b/.versionrc new file mode 100644 index 0000000..343d176 --- /dev/null +++ b/.versionrc @@ -0,0 +1,36 @@ +{ + "types": [ + { + "type": "feat", + "section": "Features" + }, + { + "type": "fix", + "section": "Bug Fixes" + }, + { + "type": "docs", + "section": "Documentation" + }, + { + "type": "test", + "section": "Tests" + }, + { + "type": "build", + "section": "Build System" + }, + { + "type": "ci" + }, + { + "type": "chore", + "section": "Chores" + }, + { + "type": "deps", + "section": "Dependencies" + } + ], + "issueUrlFormat": "https://team.linkorb.com/cards/{{id}}" +} diff --git a/README.md b/README.md index 1db3405..48b1153 100644 --- a/README.md +++ b/README.md @@ -29,3 +29,7 @@ option. Be aware that this setting noticeable affects performance. ### Example: ./bin/console linkorb:table:archive mysql://root:root@127.0.0.1:3306/test target_table YEAR_MONTH_DAY timestamp 20200101 + +## Git hooks + +There are some git hooks under `.hooks` directory. Feel free to copy & adjust & use them diff --git a/commitlint.config.js b/commitlint.config.js new file mode 100644 index 0000000..a989bfc --- /dev/null +++ b/commitlint.config.js @@ -0,0 +1,3 @@ +module.exports = { + extends: ['@commitlint/config-conventional'] +}; diff --git a/composer.json b/composer.json index 204ecea..1f9a3b5 100644 --- a/composer.json +++ b/composer.json @@ -17,8 +17,14 @@ "ext-zlib": "*" }, "require-dev": { + "phpstan/phpstan": "^0.12.51", + "phpstan/phpstan-symfony": "^0.12.10", "phpunit/phpunit": "^9", - "symfony/phpunit-bridge": "^5.1" + "sebastian/phpcpd": "^6.0", + "sensiolabs/security-checker": "^6.0", + "squizlabs/php_codesniffer": "^3.5", + "symfony/phpunit-bridge": "^5.1", + "wapmorgan/php-code-fixer": "^2.0" }, "config": { "optimize-autoloader": true, @@ -55,7 +61,19 @@ ], "post-update-cmd": [ "@auto-scripts" - ] + ], + "qa-checks": [ + "@phpstan", + "@phpcs", + "@phpcpd", + "@phpcf", + "@security-check" + ], + "phpstan": "./vendor/bin/phpstan analyze --level=5 ./src/", + "phpcs": "./vendor/bin/phpcs ./src/", + "phpcpd": "./vendor/bin/phpcpd --fuzzy ./src/", + "phpcf": "./vendor/bin/phpcf --target 7.1 ./src/", + "security-check": "./vendor/bin/security-checker security:check ./composer.lock" }, "conflict": { "symfony/symfony": "*" @@ -66,4 +84,4 @@ "require": "5.1.*" } } -} +} \ No newline at end of file diff --git a/composer.lock b/composer.lock index 84adc4d..3e59373 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": "4bf9c447d3b8f0b7d7fc44ed8731a9a4", + "content-hash": "98e2ab4795240f7fb008fb0c877461ff", "packages": [ { "name": "linkorb/connector", @@ -2941,6 +2941,132 @@ ], "time": "2020-07-08T12:44:21+00:00" }, + { + "name": "phpstan/phpstan", + "version": "0.12.51", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "b430527cd7b956324bcd554fb85a5e2fd43177d6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/b430527cd7b956324bcd554fb85a5e2fd43177d6", + "reference": "b430527cd7b956324bcd554fb85a5e2fd43177d6", + "shasum": "" + }, + "require": { + "php": "^7.1|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.12-dev" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://www.patreon.com/phpstan", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", + "type": "tidelift" + } + ], + "time": "2020-10-23T11:58:04+00:00" + }, + { + "name": "phpstan/phpstan-symfony", + "version": "0.12.10", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-symfony.git", + "reference": "675703d820235cac1abe08e8bfb12e5509a3b169" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/675703d820235cac1abe08e8bfb12e5509a3b169", + "reference": "675703d820235cac1abe08e8bfb12e5509a3b169", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "php": "^7.1 || ^8.0", + "phpstan/phpstan": "^0.12.26" + }, + "conflict": { + "symfony/framework-bundle": "<3.0" + }, + "require-dev": { + "consistence/coding-standard": "^3.10", + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", + "ergebnis/composer-normalize": "^2.0.2", + "phing/phing": "^2.16.2", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-phpunit": "^0.12.8", + "phpstan/phpstan-strict-rules": "^0.12.2", + "phpunit/phpunit": "^7.5.20", + "slevomat/coding-standard": "^6.4", + "squizlabs/php_codesniffer": "^3.5.6", + "symfony/console": "^4.0", + "symfony/framework-bundle": "^4.0", + "symfony/http-foundation": "^4.0", + "symfony/messenger": "^4.2", + "symfony/serializer": "^4.0" + }, + "type": "phpstan-extension", + "extra": { + "branch-alias": { + "dev-master": "0.12-dev" + }, + "phpstan": { + "includes": [ + "extension.neon", + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lukáš Unger", + "email": "looky.msc@gmail.com", + "homepage": "https://lookyman.net" + } + ], + "description": "Symfony Framework extensions and rules for PHPStan", + "time": "2020-10-22T10:47:37+00:00" + }, { "name": "phpunit/php-code-coverage", "version": "9.1.7", @@ -4027,6 +4153,63 @@ ], "time": "2020-06-26T12:12:55+00:00" }, + { + "name": "sebastian/phpcpd", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpcpd.git", + "reference": "4f130523214c755c69d1d59297afdff206c7b029" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpcpd/zipball/4f130523214c755c69d1d59297afdff206c7b029", + "reference": "4f130523214c755c69d1d59297afdff206c7b029", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "php": "^7.3", + "phpunit/php-file-iterator": "^3.0", + "phpunit/php-timer": "^5.0", + "sebastian/cli-parser": "^1.0", + "sebastian/version": "^3.0" + }, + "bin": [ + "phpcpd" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.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": "Copy/Paste Detector (CPD) for PHP code.", + "homepage": "https://github.com/sebastianbergmann/phpcpd", + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-08-18T13:27:23+00:00" + }, { "name": "sebastian/recursion-context", "version": "4.0.2", @@ -4238,6 +4421,344 @@ ], "time": "2020-06-26T12:18:43+00:00" }, + { + "name": "sensiolabs/security-checker", + "version": "v6.0.3", + "source": { + "type": "git", + "url": "https://github.com/sensiolabs/security-checker.git", + "reference": "a576c01520d9761901f269c4934ba55448be4a54" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sensiolabs/security-checker/zipball/a576c01520d9761901f269c4934ba55448be4a54", + "reference": "a576c01520d9761901f269c4934ba55448be4a54", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "symfony/console": "^2.8|^3.4|^4.2|^5.0", + "symfony/http-client": "^4.3|^5.0", + "symfony/mime": "^4.3|^5.0", + "symfony/polyfill-ctype": "^1.11" + }, + "bin": [ + "security-checker" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.0-dev" + } + }, + "autoload": { + "psr-4": { + "SensioLabs\\Security\\": "SensioLabs/Security" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien.potencier@gmail.com" + } + ], + "description": "A security checker for your composer.lock", + "time": "2019-11-01T13:20:14+00:00" + }, + { + "name": "squizlabs/php_codesniffer", + "version": "3.5.8", + "source": { + "type": "git", + "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", + "reference": "9d583721a7157ee997f235f327de038e7ea6dac4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/9d583721a7157ee997f235f327de038e7ea6dac4", + "reference": "9d583721a7157ee997f235f327de038e7ea6dac4", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" + }, + "bin": [ + "bin/phpcs", + "bin/phpcbf" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Greg Sherwood", + "role": "lead" + } + ], + "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", + "keywords": [ + "phpcs", + "standards" + ], + "time": "2020-10-23T02:01:07+00:00" + }, + { + "name": "symfony/http-client", + "version": "v5.1.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client.git", + "reference": "df757997ee95101c0ca94c7ea2b76e16a758e0ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client/zipball/df757997ee95101c0ca94c7ea2b76e16a758e0ca", + "reference": "df757997ee95101c0ca94c7ea2b76e16a758e0ca", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "psr/log": "^1.0", + "symfony/http-client-contracts": "^2.2", + "symfony/polyfill-php73": "^1.11", + "symfony/polyfill-php80": "^1.15", + "symfony/service-contracts": "^1.0|^2" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "1.0", + "symfony/http-client-implementation": "1.1" + }, + "require-dev": { + "amphp/http-client": "^4.2.1", + "amphp/http-tunnel": "^1.0", + "amphp/socket": "^1.1", + "guzzlehttp/promises": "^1.3.1", + "nyholm/psr7": "^1.0", + "php-http/httplug": "^1.0|^2.0", + "psr/http-client": "^1.0", + "symfony/dependency-injection": "^4.4|^5.0", + "symfony/http-kernel": "^4.4.13|^5.1.5", + "symfony/process": "^4.4|^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpClient\\": "" + }, + "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": "Symfony HttpClient component", + "homepage": "https://symfony.com", + "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": "2020-10-02T14:24:03+00:00" + }, + { + "name": "symfony/http-client-contracts", + "version": "v2.3.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client-contracts.git", + "reference": "41db680a15018f9c1d4b23516059633ce280ca33" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/41db680a15018f9c1d4b23516059633ce280ca33", + "reference": "41db680a15018f9c1d4b23516059633ce280ca33", + "shasum": "" + }, + "require": { + "php": ">=7.2.5" + }, + "suggest": { + "symfony/http-client-implementation": "" + }, + "type": "library", + "extra": { + "branch-version": "2.3", + "branch-alias": { + "dev-main": "2.3-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\HttpClient\\": "" + } + }, + "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 HTTP clients", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "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": "2020-10-14T17:08:19+00:00" + }, + { + "name": "symfony/mime", + "version": "v5.1.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/mime.git", + "reference": "4404d6545125863561721514ad9388db2661eec5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mime/zipball/4404d6545125863561721514ad9388db2661eec5", + "reference": "4404d6545125863561721514ad9388db2661eec5", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0", + "symfony/polyfill-php80": "^1.15" + }, + "conflict": { + "symfony/mailer": "<4.4" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10", + "symfony/dependency-injection": "^4.4|^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Mime\\": "" + }, + "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": "A library to manipulate MIME messages", + "homepage": "https://symfony.com", + "keywords": [ + "mime", + "mime-type" + ], + "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": "2020-09-02T16:23:27+00:00" + }, { "name": "symfony/phpunit-bridge", "version": "v5.1.5", @@ -4317,6 +4838,91 @@ ], "time": "2020-09-01T13:16:17+00:00" }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.19.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "4ad5115c0f5d5172a9fe8147675ec6de266d8826" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/4ad5115c0f5d5172a9fe8147675ec6de266d8826", + "reference": "4ad5115c0f5d5172a9fe8147675ec6de266d8826", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "symfony/polyfill-intl-normalizer": "^1.10", + "symfony/polyfill-php70": "^1.10", + "symfony/polyfill-php72": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.19-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "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": "2020-10-21T09:57:48+00:00" + }, { "name": "theseer/tokenizer", "version": "1.2.0", @@ -4363,6 +4969,64 @@ ], "time": "2020-07-12T23:59:07+00:00" }, + { + "name": "wapmorgan/php-code-fixer", + "version": "2.0.24", + "source": { + "type": "git", + "url": "https://github.com/wapmorgan/PhpCodeFixer.git", + "reference": "d6c280079701dfaecc3b8697adf514a76813ea66" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/wapmorgan/PhpCodeFixer/zipball/d6c280079701dfaecc3b8697adf514a76813ea66", + "reference": "d6c280079701dfaecc3b8697adf514a76813ea66", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=5.4", + "symfony/console": "^3.4|^4.0|^5.0" + }, + "suggest": { + "ext-json": "Adds ability to store report in JSON format" + }, + "bin": [ + "bin/phpcf" + ], + "type": "package", + "extra": { + "phar-builder": { + "compression": "BZip2", + "name": "phpcf-dev.phar", + "output-dir": "./", + "entry-point": "bin/phpcf", + "include": [ + "bin", + "data" + ], + "events": { + "command.package.start": "git describe --tags > bin/version.txt", + "command.package.end": "cp phpcf-dev.phar phpcf-`cat bin/version.txt`.phar && chmod +x phpcf.phar && rm bin/version.txt" + } + } + }, + "autoload": { + "psr-4": { + "wapmorgan\\PhpCodeFixer\\": "src/" + }, + "files": [ + "src/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Analyzer of PHP code to search issues with deprecated functionality in newer interpreter versions.", + "time": "2020-06-12T23:49:26+00:00" + }, { "name": "webmozart/assert", "version": "1.9.1", @@ -4424,7 +5088,8 @@ "ext-iconv": "*", "ext-json": "*", "ext-parallel": "*", - "ext-pdo": "*" + "ext-pdo": "*", + "ext-zlib": "*" }, "platform-dev": [], "plugin-api-version": "1.1.0" diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..d386f11 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1387 @@ +{ + "requires": true, + "lockfileVersion": 1, + "dependencies": { + "@babel/code-frame": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "dev": true, + "requires": { + "@babel/highlight": "7.10.4" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, + "@babel/highlight": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "7.10.4", + "chalk": "2.4.2", + "js-tokens": "4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "1.9.3" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "3.2.1", + "escape-string-regexp": "1.0.5", + "supports-color": "5.5.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "3.0.0" + } + } + } + }, + "@babel/runtime": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.1.tgz", + "integrity": "sha512-J5AIf3vPj3UwXaAzb5j1xM4WAQDX3EMgemF8rjCP3SoW09LfRKAXQKt6CoVYl230P6iWdRcBbnLDDdnqWxZSCA==", + "dev": true, + "requires": { + "regenerator-runtime": "0.13.7" + } + }, + "@commitlint/cli": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-11.0.0.tgz", + "integrity": "sha512-YWZWg1DuqqO5Zjh7vUOeSX76vm0FFyz4y0cpGMFhrhvUi5unc4IVfCXZ6337R9zxuBtmveiRuuhQqnRRer+13g==", + "dev": true, + "requires": { + "@babel/runtime": "7.12.1", + "@commitlint/format": "11.0.0", + "@commitlint/lint": "11.0.0", + "@commitlint/load": "11.0.0", + "@commitlint/read": "11.0.0", + "chalk": "4.1.0", + "core-js": "3.6.5", + "get-stdin": "8.0.0", + "lodash": "4.17.20", + "resolve-from": "5.0.0", + "resolve-global": "1.0.0", + "yargs": "15.4.1" + } + }, + "@commitlint/config-conventional": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-11.0.0.tgz", + "integrity": "sha512-SNDRsb5gLuDd2PL83yCOQX6pE7gevC79UPFx+GLbLfw6jGnnbO9/tlL76MLD8MOViqGbo7ZicjChO9Gn+7tHhA==", + "dev": true, + "requires": { + "conventional-changelog-conventionalcommits": "4.4.0" + } + }, + "@commitlint/ensure": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-11.0.0.tgz", + "integrity": "sha512-/T4tjseSwlirKZdnx4AuICMNNlFvRyPQimbZIOYujp9DSO6XRtOy9NrmvWujwHsq9F5Wb80QWi4WMW6HMaENug==", + "dev": true, + "requires": { + "@commitlint/types": "11.0.0", + "lodash": "4.17.20" + } + }, + "@commitlint/execute-rule": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-11.0.0.tgz", + "integrity": "sha512-g01p1g4BmYlZ2+tdotCavrMunnPFPhTzG1ZiLKTCYrooHRbmvqo42ZZn4QMStUEIcn+jfLb6BRZX3JzIwA1ezQ==", + "dev": true + }, + "@commitlint/format": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-11.0.0.tgz", + "integrity": "sha512-bpBLWmG0wfZH/svzqD1hsGTpm79TKJWcf6EXZllh2J/LSSYKxGlv967lpw0hNojme0sZd4a/97R3qA2QHWWSLg==", + "dev": true, + "requires": { + "@commitlint/types": "11.0.0", + "chalk": "4.1.0" + } + }, + "@commitlint/is-ignored": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-11.0.0.tgz", + "integrity": "sha512-VLHOUBN+sOlkYC4tGuzE41yNPO2w09sQnOpfS+pSPnBFkNUUHawEuA44PLHtDvQgVuYrMAmSWFQpWabMoP5/Xg==", + "dev": true, + "requires": { + "@commitlint/types": "11.0.0", + "semver": "7.3.2" + } + }, + "@commitlint/lint": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-11.0.0.tgz", + "integrity": "sha512-Q8IIqGIHfwKr8ecVZyYh6NtXFmKw4YSEWEr2GJTB/fTZXgaOGtGFZDWOesCZllQ63f1s/oWJYtVv5RAEuwN8BQ==", + "dev": true, + "requires": { + "@commitlint/is-ignored": "11.0.0", + "@commitlint/parse": "11.0.0", + "@commitlint/rules": "11.0.0", + "@commitlint/types": "11.0.0" + } + }, + "@commitlint/load": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-11.0.0.tgz", + "integrity": "sha512-t5ZBrtgvgCwPfxmG811FCp39/o3SJ7L+SNsxFL92OR4WQxPcu6c8taD0CG2lzOHGuRyuMxZ7ps3EbngT2WpiCg==", + "dev": true, + "requires": { + "@commitlint/execute-rule": "11.0.0", + "@commitlint/resolve-extends": "11.0.0", + "@commitlint/types": "11.0.0", + "chalk": "4.1.0", + "cosmiconfig": "7.0.0", + "lodash": "4.17.20", + "resolve-from": "5.0.0" + } + }, + "@commitlint/message": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-11.0.0.tgz", + "integrity": "sha512-01ObK/18JL7PEIE3dBRtoMmU6S3ecPYDTQWWhcO+ErA3Ai0KDYqV5VWWEijdcVafNpdeUNrEMigRkxXHQLbyJA==", + "dev": true + }, + "@commitlint/parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-11.0.0.tgz", + "integrity": "sha512-DekKQAIYWAXIcyAZ6/PDBJylWJ1BROTfDIzr9PMVxZRxBPc1gW2TG8fLgjZfBP5mc0cuthPkVi91KQQKGri/7A==", + "dev": true, + "requires": { + "conventional-changelog-angular": "5.0.11", + "conventional-commits-parser": "3.1.0" + } + }, + "@commitlint/read": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-11.0.0.tgz", + "integrity": "sha512-37V0V91GSv0aDzMzJioKpCoZw6l0shk7+tRG8RkW1GfZzUIytdg3XqJmM+IaIYpaop0m6BbZtfq+idzUwJnw7g==", + "dev": true, + "requires": { + "@commitlint/top-level": "11.0.0", + "fs-extra": "9.0.1", + "git-raw-commits": "2.0.7" + } + }, + "@commitlint/resolve-extends": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-11.0.0.tgz", + "integrity": "sha512-WinU6Uv6L7HDGLqn/To13KM1CWvZ09VHZqryqxXa1OY+EvJkfU734CwnOEeNlSCK7FVLrB4kmodLJtL1dkEpXw==", + "dev": true, + "requires": { + "import-fresh": "3.2.1", + "lodash": "4.17.20", + "resolve-from": "5.0.0", + "resolve-global": "1.0.0" + } + }, + "@commitlint/rules": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-11.0.0.tgz", + "integrity": "sha512-2hD9y9Ep5ZfoNxDDPkQadd2jJeocrwC4vJ98I0g8pNYn/W8hS9+/FuNpolREHN8PhmexXbkjrwyQrWbuC0DVaA==", + "dev": true, + "requires": { + "@commitlint/ensure": "11.0.0", + "@commitlint/message": "11.0.0", + "@commitlint/to-lines": "11.0.0", + "@commitlint/types": "11.0.0" + } + }, + "@commitlint/to-lines": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/to-lines/-/to-lines-11.0.0.tgz", + "integrity": "sha512-TIDTB0Y23jlCNubDROUVokbJk6860idYB5cZkLWcRS9tlb6YSoeLn1NLafPlrhhkkkZzTYnlKYzCVrBNVes1iw==", + "dev": true + }, + "@commitlint/top-level": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-11.0.0.tgz", + "integrity": "sha512-O0nFU8o+Ws+py5pfMQIuyxOtfR/kwtr5ybqTvR+C2lUPer2x6lnQU+OnfD7hPM+A+COIUZWx10mYQvkR3MmtAA==", + "dev": true, + "requires": { + "find-up": "5.0.0" + }, + "dependencies": { + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "6.0.0", + "path-exists": "4.0.0" + } + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "5.0.0" + } + }, + "p-limit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.0.2.tgz", + "integrity": "sha512-iwqZSOoWIW+Ew4kAGUlN16J4M7OB3ysMLSZtnhmqx7njIHFPlxWBX8xo3lVTyFVq6mI/lL9qt2IsN1sHwaxJkg==", + "dev": true, + "requires": { + "p-try": "2.2.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "3.0.2" + } + } + } + }, + "@commitlint/types": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-11.0.0.tgz", + "integrity": "sha512-VoNqai1vR5anRF5Tuh/+SWDFk7xi7oMwHrHrbm1BprYXjB2RJsWLhUrStMssDxEl5lW/z3EUdg8RvH/IUBccSQ==", + "dev": true + }, + "@types/minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY=", + "dev": true + }, + "@types/normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==", + "dev": true + }, + "@types/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", + "dev": true + }, + "JSONStream": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dev": true, + "requires": { + "jsonparse": "1.3.1", + "through": "2.3.8" + } + }, + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "2.0.1" + } + }, + "array-ify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", + "integrity": "sha1-nlKHYrSpBmrRY6aWKjZEGOlibs4=", + "dev": true + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + }, + "at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "camelcase-keys": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", + "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", + "dev": true, + "requires": { + "camelcase": "5.3.1", + "map-obj": "4.1.0", + "quick-lru": "4.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "4.3.0", + "supports-color": "7.2.0" + } + }, + "ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "requires": { + "string-width": "4.2.0", + "strip-ansi": "6.0.0", + "wrap-ansi": "6.2.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "compare-func": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", + "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==", + "dev": true, + "requires": { + "array-ify": "1.0.0", + "dot-prop": "5.3.0" + } + }, + "compare-versions": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.6.0.tgz", + "integrity": "sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==", + "dev": true + }, + "conventional-changelog-angular": { + "version": "5.0.11", + "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-5.0.11.tgz", + "integrity": "sha512-nSLypht/1yEflhuTogC03i7DX7sOrXGsRn14g131Potqi6cbGbGEE9PSDEHKldabB6N76HiSyw9Ph+kLmC04Qw==", + "dev": true, + "requires": { + "compare-func": "2.0.0", + "q": "1.5.1" + } + }, + "conventional-changelog-conventionalcommits": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-4.4.0.tgz", + "integrity": "sha512-ybvx76jTh08tpaYrYn/yd0uJNLt5yMrb1BphDe4WBredMlvPisvMghfpnJb6RmRNcqXeuhR6LfGZGewbkRm9yA==", + "dev": true, + "requires": { + "compare-func": "2.0.0", + "lodash": "4.17.20", + "q": "1.5.1" + } + }, + "conventional-commits-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-3.1.0.tgz", + "integrity": "sha512-RSo5S0WIwXZiRxUGTPuYFbqvrR4vpJ1BDdTlthFgvHt5kEdnd1+pdvwWphWn57/oIl4V72NMmOocFqqJ8mFFhA==", + "dev": true, + "requires": { + "JSONStream": "1.3.5", + "is-text-path": "1.0.1", + "lodash": "4.17.20", + "meow": "7.1.1", + "split2": "2.2.0", + "through2": "3.0.2", + "trim-off-newlines": "1.0.1" + } + }, + "core-js": { + "version": "3.6.5", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.5.tgz", + "integrity": "sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "cosmiconfig": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.0.tgz", + "integrity": "sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA==", + "dev": true, + "requires": { + "@types/parse-json": "4.0.0", + "import-fresh": "3.2.1", + "parse-json": "5.1.0", + "path-type": "4.0.0", + "yaml": "1.10.0" + } + }, + "dargs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/dargs/-/dargs-7.0.0.tgz", + "integrity": "sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==", + "dev": true + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "decamelize-keys": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz", + "integrity": "sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=", + "dev": true, + "requires": { + "decamelize": "1.2.0", + "map-obj": "1.0.1" + }, + "dependencies": { + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", + "dev": true + } + } + }, + "dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dev": true, + "requires": { + "is-obj": "2.0.0" + } + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "0.2.1" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "5.0.0", + "path-exists": "4.0.0" + } + }, + "find-versions": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-3.2.0.tgz", + "integrity": "sha512-P8WRou2S+oe222TOCHitLy8zj+SIsVJh52VP4lvXkaFVnOFFdoWv1H1Jjvel1aI6NCFOAaeAVm8qrI0odiLcww==", + "dev": true, + "requires": { + "semver-regex": "2.0.0" + } + }, + "fs-extra": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz", + "integrity": "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==", + "dev": true, + "requires": { + "at-least-node": "1.0.0", + "graceful-fs": "4.2.4", + "jsonfile": "6.0.1", + "universalify": "1.0.0" + } + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-stdin": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz", + "integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==", + "dev": true + }, + "git-raw-commits": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-2.0.7.tgz", + "integrity": "sha512-SkwrTqrDxw8y0G1uGJ9Zw13F7qu3LF8V4BifyDeiJCxSnjRGZD9SaoMiMqUvvXMXh6S3sOQ1DsBN7L2fMUZW/g==", + "dev": true, + "requires": { + "dargs": "7.0.0", + "lodash.template": "4.5.0", + "meow": "7.1.1", + "split2": "2.2.0", + "through2": "3.0.2" + } + }, + "global-dirs": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", + "integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=", + "dev": true, + "requires": { + "ini": "1.3.5" + } + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + }, + "hard-rejection": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", + "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "1.1.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "hosted-git-info": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", + "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", + "dev": true + }, + "husky": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/husky/-/husky-4.3.0.tgz", + "integrity": "sha512-tTMeLCLqSBqnflBZnlVDhpaIMucSGaYyX6855jM4AguGeWCeSzNdb1mfyWduTZ3pe3SJVvVWGL0jO1iKZVPfTA==", + "dev": true, + "requires": { + "chalk": "4.1.0", + "ci-info": "2.0.0", + "compare-versions": "3.6.0", + "cosmiconfig": "7.0.0", + "find-versions": "3.2.0", + "opencollective-postinstall": "2.0.3", + "pkg-dir": "4.2.0", + "please-upgrade-node": "3.2.0", + "slash": "3.0.0", + "which-pm-runs": "1.0.0" + } + }, + "import-fresh": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", + "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", + "dev": true, + "requires": { + "parent-module": "1.0.1", + "resolve-from": "4.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + } + } + }, + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "dev": true + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-core-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.0.0.tgz", + "integrity": "sha512-jq1AH6C8MuteOoBPwkxHafmByhL9j5q4OaPGdbuD+ZtQJVzH+i6E3BJDQcBA09k57i2Hh2yQbEG8yObZ0jdlWw==", + "dev": true, + "requires": { + "has": "1.0.3" + } + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true + }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", + "dev": true + }, + "is-text-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-1.0.1.tgz", + "integrity": "sha1-Thqg+1G/vLPpJogAE5cgLBd1tm4=", + "dev": true, + "requires": { + "text-extensions": "1.9.0" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "jsonfile": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.0.1.tgz", + "integrity": "sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg==", + "dev": true, + "requires": { + "graceful-fs": "4.2.4", + "universalify": "1.0.0" + } + }, + "jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", + "dev": true + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + }, + "lines-and-columns": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", + "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", + "dev": true + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "4.1.0" + } + }, + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "dev": true + }, + "lodash._reinterpolate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", + "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=", + "dev": true + }, + "lodash.template": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", + "integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==", + "dev": true, + "requires": { + "lodash._reinterpolate": "3.0.0", + "lodash.templatesettings": "4.2.0" + } + }, + "lodash.templatesettings": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz", + "integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==", + "dev": true, + "requires": { + "lodash._reinterpolate": "3.0.0" + } + }, + "map-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.1.0.tgz", + "integrity": "sha512-glc9y00wgtwcDmp7GaE/0b0OnxpNJsVf3ael/An6Fe2Q51LLwN1er6sdomLRzz5h0+yMpiYLhWYF5R7HeqVd4g==", + "dev": true + }, + "meow": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/meow/-/meow-7.1.1.tgz", + "integrity": "sha512-GWHvA5QOcS412WCo8vwKDlTelGLsCGBVevQB5Kva961rmNfun0PCbv5+xta2kUMFJyR8/oWnn7ddeKdosbAPbA==", + "dev": true, + "requires": { + "@types/minimist": "1.2.0", + "camelcase-keys": "6.2.2", + "decamelize-keys": "1.1.0", + "hard-rejection": "2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "2.5.0", + "read-pkg-up": "7.0.1", + "redent": "3.0.0", + "trim-newlines": "3.0.0", + "type-fest": "0.13.1", + "yargs-parser": "18.1.3" + } + }, + "min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true + }, + "minimist-options": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", + "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", + "dev": true, + "requires": { + "arrify": "1.0.1", + "is-plain-obj": "1.1.0", + "kind-of": "6.0.3" + } + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "2.8.8", + "resolve": "1.18.1", + "semver": "5.7.1", + "validate-npm-package-license": "3.0.4" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "opencollective-postinstall": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz", + "integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==", + "dev": true + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "2.2.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "2.3.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "3.1.0" + } + }, + "parse-json": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.1.0.tgz", + "integrity": "sha512-+mi/lmVVNKFNVyLXV31ERiy2CY5E1/F6QtJFEzoChPRwwngMNXRDQ9GJ5WdE2Z2P4AujsOi0/+2qHID68KwfIQ==", + "dev": true, + "requires": { + "@babel/code-frame": "7.10.4", + "error-ex": "1.3.2", + "json-parse-even-better-errors": "2.3.1", + "lines-and-columns": "1.1.6" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "4.1.0" + } + }, + "please-upgrade-node": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", + "integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==", + "dev": true, + "requires": { + "semver-compare": "1.0.0" + } + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", + "dev": true + }, + "quick-lru": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", + "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", + "dev": true + }, + "read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "requires": { + "@types/normalize-package-data": "2.4.0", + "normalize-package-data": "2.5.0", + "parse-json": "5.1.0", + "type-fest": "0.6.0" + }, + "dependencies": { + "type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true + } + } + }, + "read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "requires": { + "find-up": "4.1.0", + "read-pkg": "5.2.0", + "type-fest": "0.8.1" + }, + "dependencies": { + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + } + } + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.4", + "isarray": "1.0.0", + "process-nextick-args": "2.0.1", + "safe-buffer": "5.1.2", + "string_decoder": "1.1.1", + "util-deprecate": "1.0.2" + } + }, + "redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "requires": { + "indent-string": "4.0.0", + "strip-indent": "3.0.0" + } + }, + "regenerator-runtime": { + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", + "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==", + "dev": true + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "resolve": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.18.1.tgz", + "integrity": "sha512-lDfCPaMKfOJXjy0dPayzPdF1phampNWr3qFCjAu+rw/qbQmr5jWH5xN2hwh9QKfw9E5v4hwV7A+jrCmL8yjjqA==", + "dev": true, + "requires": { + "is-core-module": "2.0.0", + "path-parse": "1.0.6" + } + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + }, + "resolve-global": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-global/-/resolve-global-1.0.0.tgz", + "integrity": "sha512-zFa12V4OLtT5XUX/Q4VLvTfBf+Ok0SPc1FNGM/z9ctUdiU618qwKpWnd0CHs3+RqROfyEg/DhuHbMWYqcgljEw==", + "dev": true, + "requires": { + "global-dirs": "0.1.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "dev": true + }, + "semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=", + "dev": true + }, + "semver-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-2.0.0.tgz", + "integrity": "sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw==", + "dev": true + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "spdx-correct": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "dev": true, + "requires": { + "spdx-expression-parse": "3.0.1", + "spdx-license-ids": "3.0.6" + } + }, + "spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "requires": { + "spdx-exceptions": "2.3.0", + "spdx-license-ids": "3.0.6" + } + }, + "spdx-license-ids": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.6.tgz", + "integrity": "sha512-+orQK83kyMva3WyPf59k1+Y525csj5JejicWut55zeTWANuN17qSiSLUXWtzHeNWORSvT7GLDJ/E/XiIWoXBTw==", + "dev": true + }, + "split2": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-2.2.0.tgz", + "integrity": "sha512-RAb22TG39LhI31MbreBgIuKiIKhVsawfTgEGqKHTK87aG+ul/PB8Sqoi3I7kVdRWiCfrKxK3uo4/YUkpNvhPbw==", + "dev": true, + "requires": { + "through2": "2.0.5" + }, + "dependencies": { + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "2.3.7", + "xtend": "4.0.2" + } + } + } + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "8.0.0", + "is-fullwidth-code-point": "3.0.0", + "strip-ansi": "6.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "5.0.0" + } + }, + "strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "requires": { + "min-indent": "1.0.1" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "4.0.0" + } + }, + "text-extensions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-1.9.0.tgz", + "integrity": "sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "through2": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", + "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", + "dev": true, + "requires": { + "inherits": "2.0.4", + "readable-stream": "2.3.7" + } + }, + "trim-newlines": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.0.tgz", + "integrity": "sha512-C4+gOpvmxaSMKuEf9Qc134F1ZuOHVXKRbtEflf4NTtuuJDEIJ9p5PXsalL8SkeRw+qit1Mo+yuvMPAKwWg/1hA==", + "dev": true + }, + "trim-off-newlines": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-off-newlines/-/trim-off-newlines-1.0.1.tgz", + "integrity": "sha1-n5up2e+odkw4dpi8v+sshI8RrbM=", + "dev": true + }, + "type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true + }, + "universalify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz", + "integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==", + "dev": true + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "3.1.1", + "spdx-expression-parse": "3.0.1" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "which-pm-runs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.0.0.tgz", + "integrity": "sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=", + "dev": true + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "4.3.0", + "string-width": "4.2.0", + "strip-ansi": "6.0.0" + } + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, + "yaml": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz", + "integrity": "sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==", + "dev": true + }, + "yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "requires": { + "cliui": "6.0.0", + "decamelize": "1.2.0", + "find-up": "4.1.0", + "get-caller-file": "2.0.5", + "require-directory": "2.1.1", + "require-main-filename": "2.0.0", + "set-blocking": "2.0.0", + "string-width": "4.2.0", + "which-module": "2.0.0", + "y18n": "4.0.0", + "yargs-parser": "18.1.3" + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "requires": { + "camelcase": "5.3.1", + "decamelize": "1.2.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..78b25b5 --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "devDependencies": { + "@commitlint/cli": "^11.0.0", + "@commitlint/config-conventional": "^11.0.0", + "husky": "^4.3.0" + }, + "license": "UNLICENSED", + "private": true, + "scripts": [], + "dependencies": [], + "husky": { + "hooks": { + "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" + } + } +} \ No newline at end of file diff --git a/phpcs.xml.dist b/phpcs.xml.dist new file mode 100644 index 0000000..a0ea605 --- /dev/null +++ b/phpcs.xml.dist @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + src/ + + From 353ce9bcd6d5ffe5706b581442efaad58d159a7e Mon Sep 17 00:00:00 2001 From: amsprost Date: Sun, 25 Oct 2020 02:36:47 +0100 Subject: [PATCH 12/13] Apply fixes from qa-checks --- composer.json | 5 +- composer.lock | 163 +++++++++++++------------- config/packages/security_checker.yaml | 8 ++ src/Command/TableArchiveCommand.php | 15 ++- src/Kernel.php | 14 +-- symfony.lock | 45 +++++++ 6 files changed, 155 insertions(+), 95 deletions(-) create mode 100644 config/packages/security_checker.yaml diff --git a/composer.json b/composer.json index 1f9a3b5..b19d691 100644 --- a/composer.json +++ b/composer.json @@ -54,7 +54,8 @@ }, "scripts": { "auto-scripts": { - "cache:clear": "symfony-cmd" + "cache:clear": "symfony-cmd", + "security-checker security:check": "script" }, "post-install-cmd": [ "@auto-scripts" @@ -84,4 +85,4 @@ "require": "5.1.*" } } -} \ No newline at end of file +} diff --git a/composer.lock b/composer.lock index 3e59373..ade7fe5 100644 --- a/composer.lock +++ b/composer.lock @@ -1376,6 +1376,82 @@ ], "time": "2020-08-30T09:59:07+00:00" }, + { + "name": "symfony/http-client-contracts", + "version": "v2.3.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client-contracts.git", + "reference": "41db680a15018f9c1d4b23516059633ce280ca33" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/41db680a15018f9c1d4b23516059633ce280ca33", + "reference": "41db680a15018f9c1d4b23516059633ce280ca33", + "shasum": "" + }, + "require": { + "php": ">=7.2.5" + }, + "suggest": { + "symfony/http-client-implementation": "" + }, + "type": "library", + "extra": { + "branch-version": "2.3", + "branch-alias": { + "dev-main": "2.3-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\HttpClient\\": "" + } + }, + "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 HTTP clients", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "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": "2020-10-14T17:08:19+00:00" + }, { "name": "symfony/http-foundation", "version": "v5.1.4", @@ -1453,16 +1529,16 @@ }, { "name": "symfony/http-kernel", - "version": "v5.1.4", + "version": "v5.1.7", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "f829c240113986b60fda425c2533142e88efd7c4" + "reference": "1764b87d2f10d5c9ce6e4850fe27934116d89708" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/f829c240113986b60fda425c2533142e88efd7c4", - "reference": "f829c240113986b60fda425c2533142e88efd7c4", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/1764b87d2f10d5c9ce6e4850fe27934116d89708", + "reference": "1764b87d2f10d5c9ce6e4850fe27934116d89708", "shasum": "" }, "require": { @@ -1471,6 +1547,7 @@ "symfony/deprecation-contracts": "^2.1", "symfony/error-handler": "^4.4|^5.0", "symfony/event-dispatcher": "^5.0", + "symfony/http-client-contracts": "^1.1|^2", "symfony/http-foundation": "^4.4|^5.0", "symfony/polyfill-ctype": "^1.8", "symfony/polyfill-php73": "^1.9", @@ -1562,7 +1639,7 @@ "type": "tidelift" } ], - "time": "2020-08-31T06:18:12+00:00" + "time": "2020-10-04T07:57:28+00:00" }, { "name": "symfony/polyfill-intl-grapheme", @@ -4606,82 +4683,6 @@ ], "time": "2020-10-02T14:24:03+00:00" }, - { - "name": "symfony/http-client-contracts", - "version": "v2.3.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/http-client-contracts.git", - "reference": "41db680a15018f9c1d4b23516059633ce280ca33" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/41db680a15018f9c1d4b23516059633ce280ca33", - "reference": "41db680a15018f9c1d4b23516059633ce280ca33", - "shasum": "" - }, - "require": { - "php": ">=7.2.5" - }, - "suggest": { - "symfony/http-client-implementation": "" - }, - "type": "library", - "extra": { - "branch-version": "2.3", - "branch-alias": { - "dev-main": "2.3-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Contracts\\HttpClient\\": "" - } - }, - "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 HTTP clients", - "homepage": "https://symfony.com", - "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" - ], - "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": "2020-10-14T17:08:19+00:00" - }, { "name": "symfony/mime", "version": "v5.1.7", diff --git a/config/packages/security_checker.yaml b/config/packages/security_checker.yaml new file mode 100644 index 0000000..2e905f7 --- /dev/null +++ b/config/packages/security_checker.yaml @@ -0,0 +1,8 @@ +services: + _defaults: + autowire: true + autoconfigure: true + + SensioLabs\Security\SecurityChecker: null + + SensioLabs\Security\Command\SecurityCheckerCommand: null diff --git a/src/Command/TableArchiveCommand.php b/src/Command/TableArchiveCommand.php index 2e3a181..0f49bf3 100644 --- a/src/Command/TableArchiveCommand.php +++ b/src/Command/TableArchiveCommand.php @@ -7,6 +7,7 @@ use Connector\Connector; use DateTimeImmutable; use DateTimeInterface; +use InvalidArgumentException; use Linkorb\TableArchiver\Dto\ArchiveDto; use Linkorb\TableArchiver\Manager\TableArchiver; use Linkorb\TableArchiver\Services\OutputWriter; @@ -16,6 +17,7 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ConfirmationQuestion; +use Throwable; final class TableArchiveCommand extends Command { @@ -66,7 +68,12 @@ protected function configure() protected function execute(InputInterface $input, OutputInterface $output) { - $dto = $this->createDto($input, $output); + try { + $dto = $this->createDto($input, $output); + } catch (Throwable $e) { + $output->write(sprintf('%s', $e->getMessage())); + return -1; + } $pdo = $this->connector->getPdo($this->connector->getConfig($dto->pdoDsn)); @@ -103,8 +110,7 @@ private function createDto(InputInterface $input, OutputInterface $output): Arch $dto->tableName = $input->getArgument('tableName'); if (!isset($archiveModeMap[$input->getArgument('mode')])) { - $output->write('This archive mode is not allowed'); - return -1; + throw new InvalidArgumentException('This archive mode is not allowed'); } $dto->archiveMode = $archiveModeMap[$input->getArgument('mode')]; @@ -113,8 +119,7 @@ private function createDto(InputInterface $input, OutputInterface $output): Arch $datetime = $input->getArgument('maxStamp') ? DateTimeImmutable::createFromFormat('Ymd', $input->getArgument('maxStamp')) : null; if ($datetime === false) { - $output->write('Incorrect max stamp passed'); - return -1; + throw new InvalidArgumentException('Incorrect max stamp passed'); } $dto->maxStamp = $datetime instanceof DateTimeInterface ? $datetime->setTime(23, 59, 59, 999999)->format('Y-m-d H:i:s') : diff --git a/src/Kernel.php b/src/Kernel.php index 7e315b0..f66b1cb 100644 --- a/src/Kernel.php +++ b/src/Kernel.php @@ -14,24 +14,24 @@ class Kernel extends BaseKernel protected function configureContainer(ContainerConfigurator $container): void { $container->import('../config/{packages}/*.yaml'); - $container->import('../config/{packages}/'.$this->environment.'/*.yaml'); + $container->import('../config/{packages}/' . $this->environment . '/*.yaml'); - if (is_file(\dirname(__DIR__).'/config/services.yaml')) { + if (is_file(\dirname(__DIR__) . '/config/services.yaml')) { $container->import('../config/{services}.yaml'); - $container->import('../config/{services}_'.$this->environment.'.yaml'); - } elseif (is_file($path = \dirname(__DIR__).'/config/services.php')) { + $container->import('../config/{services}_' . $this->environment . '.yaml'); + } elseif (is_file($path = \dirname(__DIR__) . '/config/services.php')) { (require $path)($container->withPath($path), $this); } } protected function configureRoutes(RoutingConfigurator $routes): void { - $routes->import('../config/{routes}/'.$this->environment.'/*.yaml'); + $routes->import('../config/{routes}/' . $this->environment . '/*.yaml'); $routes->import('../config/{routes}/*.yaml'); - if (is_file(\dirname(__DIR__).'/config/routes.yaml')) { + if (is_file(\dirname(__DIR__) . '/config/routes.yaml')) { $routes->import('../config/{routes}.yaml'); - } elseif (is_file($path = \dirname(__DIR__).'/config/routes.php')) { + } elseif (is_file($path = \dirname(__DIR__) . '/config/routes.php')) { (require $path)($routes->withPath($path), $this); } } diff --git a/symfony.lock b/symfony.lock index 5bb36dc..22540af 100644 --- a/symfony.lock +++ b/symfony.lock @@ -32,6 +32,12 @@ "phpspec/prophecy": { "version": "1.11.1" }, + "phpstan/phpstan": { + "version": "0.12.51" + }, + "phpstan/phpstan-symfony": { + "version": "0.12.10" + }, "phpunit/php-code-coverage": { "version": "9.1.7" }, @@ -109,6 +115,9 @@ "sebastian/object-reflector": { "version": "2.0.2" }, + "sebastian/phpcpd": { + "version": "6.0.2" + }, "sebastian/recursion-context": { "version": "4.0.2" }, @@ -121,6 +130,27 @@ "sebastian/version": { "version": "3.0.1" }, + "sensiolabs/security-checker": { + "version": "4.0", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "master", + "version": "4.0", + "ref": "160c9b600564faa1224e8f387d49ef13ceb8b793" + }, + "files": [ + "config/packages/security_checker.yaml" + ] + }, + "squizlabs/php_codesniffer": { + "version": "3.0", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "master", + "version": "3.0", + "ref": "0dc9cceda799fd3a08b96987e176a261028a3709" + } + }, "symfony/cache": { "version": "v5.1.4" }, @@ -197,12 +227,21 @@ "src/Kernel.php" ] }, + "symfony/http-client": { + "version": "v5.1.7" + }, + "symfony/http-client-contracts": { + "version": "v2.3.1" + }, "symfony/http-foundation": { "version": "v5.1.4" }, "symfony/http-kernel": { "version": "v5.1.4" }, + "symfony/mime": { + "version": "v5.1.7" + }, "symfony/phpunit-bridge": { "version": "4.3", "recipe": { @@ -221,6 +260,9 @@ "symfony/polyfill-intl-grapheme": { "version": "v1.18.1" }, + "symfony/polyfill-intl-idn": { + "version": "v1.19.0" + }, "symfony/polyfill-intl-normalizer": { "version": "v1.18.1" }, @@ -265,6 +307,9 @@ "theseer/tokenizer": { "version": "1.2.0" }, + "wapmorgan/php-code-fixer": { + "version": "2.0.24" + }, "webmozart/assert": { "version": "1.9.1" } From 26b2f5f2a0aa07b32876a68ed6db418922b1bbe2 Mon Sep 17 00:00:00 2001 From: amsprost Date: Sun, 1 Nov 2020 02:52:39 +0100 Subject: [PATCH 13/13] Add tests, phpunit to qa-checks --- composer.json | 4 +- phpunit.xml.dist | 40 ++++------ src/Command/TableArchiveCommand.php | 2 +- tests/Manager/TableArchiverTest.php | 98 ++++++++++++++----------- tests/Services/ArchiverWorkerTest.php | 96 +++++++++++++++++++++--- tests/TestHelpers/DbSetupAwareTrait.php | 5 ++ 6 files changed, 166 insertions(+), 79 deletions(-) diff --git a/composer.json b/composer.json index b19d691..5d7c404 100644 --- a/composer.json +++ b/composer.json @@ -68,12 +68,14 @@ "@phpcs", "@phpcpd", "@phpcf", - "@security-check" + "@security-check", + "@phpunit" ], "phpstan": "./vendor/bin/phpstan analyze --level=5 ./src/", "phpcs": "./vendor/bin/phpcs ./src/", "phpcpd": "./vendor/bin/phpcpd --fuzzy ./src/", "phpcf": "./vendor/bin/phpcf --target 7.1 ./src/", + "phpunit": "./vendor/bin/phpunit", "security-check": "./vendor/bin/security-checker security:check ./composer.lock" }, "conflict": { diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 214665a..dcf3209 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,27 +1,19 @@ - - - - - - - - - - - tests - - - - - - src - - + + + + src + + + + + + + + + + tests + + diff --git a/src/Command/TableArchiveCommand.php b/src/Command/TableArchiveCommand.php index 0f49bf3..a393058 100644 --- a/src/Command/TableArchiveCommand.php +++ b/src/Command/TableArchiveCommand.php @@ -71,7 +71,7 @@ protected function execute(InputInterface $input, OutputInterface $output) try { $dto = $this->createDto($input, $output); } catch (Throwable $e) { - $output->write(sprintf('%s', $e->getMessage())); + $output->writeln(sprintf('%s', $e->getMessage())); return -1; } diff --git a/tests/Manager/TableArchiverTest.php b/tests/Manager/TableArchiverTest.php index f6bbcf3..2daf11d 100644 --- a/tests/Manager/TableArchiverTest.php +++ b/tests/Manager/TableArchiverTest.php @@ -7,10 +7,10 @@ use Linkorb\TableArchiver\Dto\ArchiveDto; use Linkorb\TableArchiver\Factory\QueryFactory; use Linkorb\TableArchiver\Manager\TableArchiver; +use Linkorb\TableArchiver\Services\OutputArchiver; use Linkorb\TableArchiver\Services\Supervisor; use Linkorb\TableArchiver\Tests\TestHelpers\DbSetupAwareTrait; use PDO; -use PDOStatement; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -20,74 +20,90 @@ class TableArchiverTest extends TestCase private TableArchiver $manager; private PDO $pdo; + private int $threadsNumber = 5; + private QueryFactory $factory; /** @var MockObject|Supervisor */ private MockObject $supervisor; + /** @var MockObject|OutputArchiver */ + private MockObject $archiver; public function setUp(): void { $this->pdo = $this->setUpDb(); $this->supervisor = $this->createMock(Supervisor::class); - $this->manager = $this->createPartialMock(TableArchiver::class, ['createPDO']); - - $this->manager->__construct(new QueryFactory(), $this->supervisor, 2); - } - - public function testIncorrectDbConnection() - { - $statement = $this->createConfiguredMock(PDOStatement::class, ['fetch' => false]); - $pdo = $this->createConfiguredMock(PDO::class, ['query' => $statement]); - $this->manager->method('createPDO')->willReturn($pdo); - $dto = new ArchiveDto(); - - $this->expectExceptionMessage('Something is wrong with your PDO connection'); - - $this->manager->archive($dto); + $this->archiver = $this->createMock(OutputArchiver::class); + $this->factory = $this->createTestProxy(QueryFactory::class); + $this->manager = new TableArchiver($this->factory, $this->supervisor, $this->archiver, $this->threadsNumber); } public function testArchiveTimestamp(): void { - $this->manager->method('createPDO')->willReturn($this->pdo); - $dto = new ArchiveDto(); $dto->archiveMode = ArchiveDto::YEAR; $dto->stampColumnName = $this->getTimestampName(); $dto->tableName = $this->getTableName(); $this->supervisor - ->expects($this->exactly(4)) - ->method('spawn') - ->withConsecutive( - [['SELECT * FROM `' . $this->getTableName() . '` LIMIT 2 OFFSET 0', $dto]], - [['SELECT * FROM `' . $this->getTableName() . '` LIMIT 2 OFFSET 2', $dto]], - [['SELECT * FROM `' . $this->getTableName() . '` LIMIT 2 OFFSET 4', $dto]], - [['SELECT * FROM `' . $this->getTableName() . '` OFFSET 6', $dto]], - ); - - $this->manager->archive($dto); + ->expects($this->exactly($this->threadsNumber)) + ->method('spawnProcessing') + ->with([$dto]); + + $this->factory + ->expects($this->once()) + ->method('buildFetchQuery'); + + $this->factory + ->expects($this->once()) + ->method('buildCountQuery'); + + $this->supervisor->expects($this->exactly(count($this->getDbData())))->method('delegate'); + $this->supervisor->method('terminateThreads')->willReturn(count($this->getDbData())); + + $this->assertEquals(count($this->getDbData()), $this->manager->archive($this->pdo, $dto)); $this->assertTrue($dto->isTimestamp); } public function testArchiveDateTime(): void { - $this->manager->method('createPDO')->willReturn($this->pdo); - $dto = new ArchiveDto(); $dto->archiveMode = ArchiveDto::YEAR_MONTH_DAY; $dto->stampColumnName = $this->getDateTimeName(); $dto->tableName = $this->getTableName(); - $this->supervisor - ->expects($this->exactly(4)) - ->method('spawn') - ->withConsecutive( - [['SELECT * FROM `' . $this->getTableName() . '` LIMIT 2 OFFSET 0', $dto]], - [['SELECT * FROM `' . $this->getTableName() . '` LIMIT 2 OFFSET 2', $dto]], - [['SELECT * FROM `' . $this->getTableName() . '` LIMIT 2 OFFSET 4', $dto]], - [['SELECT * FROM `' . $this->getTableName() . '` OFFSET 6', $dto]], - ); - - $this->manager->archive($dto); + $this->supervisor->method('terminateThreads')->willReturn(count($this->getDbData())); + + $this->manager->archive($this->pdo, $dto); $this->assertFalse($dto->isTimestamp); } + + public function testArchiveMaxTimestamp(): void + { + $dto = new ArchiveDto(); + $dto->archiveMode = ArchiveDto::YEAR_MONTH_DAY; + $dto->stampColumnName = $this->getDateTimeName(); + $dto->tableName = $this->getTableName(); + list($dto->maxStamp, $count) = $this->getMaxTimestampData(); + + $this->supervisor->method('terminateThreads')->willReturn($count); + + $this->assertEquals($count, $this->manager->archive($this->pdo, $dto)); + } + + public function testFlushArchived() + { + $dto = new ArchiveDto(); + $dto->archiveMode = ArchiveDto::YEAR_MONTH_DAY; + $dto->stampColumnName = $this->getDateTimeName(); + $dto->tableName = $this->getTableName(); + + $this->factory->expects($this->once())->method('buildDeleteQuery'); + $this->assertNull($this->manager->flushArchived($this->pdo, $dto)); + } + + public function testArchiveExportedFiles() + { + $this->archiver->expects($this->once())->method('archive'); + $this->assertNull($this->manager->archiveExportedFiles()); + } } diff --git a/tests/Services/ArchiverWorkerTest.php b/tests/Services/ArchiverWorkerTest.php index a3874d8..d1ddefb 100644 --- a/tests/Services/ArchiverWorkerTest.php +++ b/tests/Services/ArchiverWorkerTest.php @@ -4,11 +4,12 @@ namespace Linkorb\TableArchiver\Tests\Services; +use DateTime; use Linkorb\TableArchiver\Dto\ArchiveDto; use Linkorb\TableArchiver\Services\ArchiverWorker; use Linkorb\TableArchiver\Services\OutputWriter; use Linkorb\TableArchiver\Tests\TestHelpers\DbSetupAwareTrait; -use PDO; +use parallel\Channel; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -18,33 +19,104 @@ class ArchiverWorkerTest extends TestCase /** @var OutputWriter|MockObject */ private OutputWriter $writer; - private PDO $pdo; private ArchiverWorker $worker; public function setUp(): void { - $this->pdo = $this->setUpDb(); $this->writer = $this->createPartialMock(OutputWriter::class, ['outputPath']); $this->writer->method('outputPath')->willReturn('php://memory'); - $this->worker = $this->createPartialMock(ArchiverWorker::class, ['createPDO']); - $this->worker->__construct($this->writer); - $this->worker->method('createPDO')->willReturn($this->pdo); + $this->worker = new ArchiverWorker($this->writer); } public function testInvokeDateTimeYearMonthDay() { - $dto = new ArchiveDto(); - $dto->isTimestamp = false; - $dto->tableName = $this->getTableName(); - $dto->stampColumnName = $this->getDateTimeName(); - $dto->archiveMode = ArchiveDto::YEAR_MONTH_DAY; + $channel = new Channel(10); + + $channel->send(['name' => 'a', $this->getDateTimeName() => '2020-08-01 15:15:34']); + $channel->send(['name' => 'b', $this->getDateTimeName() => '2000-11-10 12:34:56']); + $channel->close(); $this->writer ->expects($this->exactly(2)) ->method('outputPath') ->withConsecutive(['20200801.ndjson'], ['20001110.ndjson']); - ($this->worker)('SELECT * FROM `' . $this->getTableName() . '` LIMIT 2 OFFSET 0', $dto); + $this->assertEquals(2, ($this->worker)($this->getDto(ArchiveDto::YEAR_MONTH_DAY), $channel)); + } + + public function testInvokeDateTimeYearMonth() + { + $channel = new Channel(10); + + $channel->send(['name' => 'a', $this->getDateTimeName() => '2000-01-01 11:15:45']); + $channel->send(['name' => 'b', $this->getDateTimeName() => '2000-11-10 13:34:37']); + $channel->send(['name' => 'c', $this->getDateTimeName() => '2000-01-10 23:58:58']); + $channel->close(); + + $this->writer + ->expects($this->exactly(2)) + ->method('outputPath') + ->withConsecutive(['200001.ndjson'], ['200011.ndjson']); + + $this->assertEquals(3, ($this->worker)($this->getDto(ArchiveDto::YEAR_MONTH), $channel)); + } + + public function testInvokeDateTimeYear() + { + $channel = new Channel(10); + + $channel->send([$this->getDateTimeName() => '2000-01-01 00:00:00']); + $channel->close(); + + $this->writer->expects($this->exactly(1))->method('outputPath')->with('2000.ndjson'); + + $this->assertEquals(1, ($this->worker)($this->getDto(ArchiveDto::YEAR), $channel)); + } + + public function testInvokeCacheDisabled() + { + $channel = new Channel(10); + + $this->writer->disableCache(); + + $channel->send([$this->getDateTimeName() => '2020-08-01 15:15:34']); + $channel->send([$this->getDateTimeName() => '2020-08-01 15:15:35']); + $channel->close(); + + $this->writer + ->expects($this->exactly(2)) + ->method('outputPath') + ->withConsecutive(['20200801.ndjson'], ['20200801.ndjson']); + + $this->assertEquals(2, ($this->worker)($this->getDto(ArchiveDto::YEAR_MONTH_DAY), $channel)); + } + + public function testInvokeTimestamp() + { + $channel = new Channel(10); + + $channel->send([$this->getTimestampName() => (new DateTime('1999-11-13 00:01:22'))->getTimestamp()]); + $channel->send([$this->getTimestampName() => (new DateTime('1970-12-12 13:11:06'))->getTimestamp()]); + $channel->send([$this->getTimestampName() => (new DateTime('2002-07-08 17:09:00'))->getTimestamp()]); + $channel->close(); + + $this->writer + ->expects($this->exactly(3)) + ->method('outputPath') + ->withConsecutive(['19991113.ndjson'], ['19701212.ndjson'], ['20020708.ndjson']); + + $this->assertEquals(3, ($this->worker)($this->getDto(ArchiveDto::YEAR_MONTH_DAY, true), $channel)); + } + + private function getDto(int $mode, bool $timestamp = false): ArchiveDto + { + $dto = new ArchiveDto(); + $dto->isTimestamp = $timestamp; + $dto->tableName = $this->getTableName(); + $dto->stampColumnName = $timestamp ? $this->getTimestampName() : $this->getDateTimeName(); + $dto->archiveMode = $mode; + + return $dto; } } diff --git a/tests/TestHelpers/DbSetupAwareTrait.php b/tests/TestHelpers/DbSetupAwareTrait.php index 7a9cbea..c9a533f 100644 --- a/tests/TestHelpers/DbSetupAwareTrait.php +++ b/tests/TestHelpers/DbSetupAwareTrait.php @@ -37,6 +37,11 @@ protected function getDateTimeName(): string return 'datetime'; } + protected function getMaxTimestampData(): array + { + return ['2019-11-25 23:59:59', 4]; + } + private function setUpDb(): PDO { $pdo = new PDO('sqlite::memory:');