diff --git a/.coveralls.yml b/.coveralls.yml new file mode 100644 index 0000000000000000000000000000000000000000..a8172feb1d56d24d6a59e59e27ce75ccb5008be8 --- /dev/null +++ b/.coveralls.yml @@ -0,0 +1 @@ +json_path: coveralls-upload.json diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000000000000000000000000000000000000..2f34eb2402c7211066e2401b7e4cb11d3e5f4369 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,25 @@ +language: php + +dist: trusty + +matrix: + include: + - php: 7.2 + - php: 7.3 + - php: 7.4 + env: ANALYSIS='true' + - php: nightly + + allow_failures: + - php: nightly + +before_script: +- composer require php-coveralls/php-coveralls:^2.2.0 +- composer install -n + +script: +- if [[ "$ANALYSIS" == 'true' ]]; then vendor/bin/phpunit --coverage-clover clover.xml ; fi +- if [[ "$ANALYSIS" == 'true' ]]; then vendor/bin/phpstan analyse src ; fi + +after_success: +- if [[ "$ANALYSIS" == 'true' ]]; then vendor/bin/php-coveralls --coverage_clover=clover.xml -v ; fi diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000000000000000000000000000000000..1c7a3e3dd3d42b18299d733aff096175f016f60a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,14 @@ +# How to Contribute + +## Pull Requests + +1. Fork the Slim Skeleton repository +2. Create a new branch for each feature or improvement +3. Send a pull request from each feature branch to the **4.x** branch + +It is very important to separate new features or improvements into separate feature branches, and to send a +pull request for each branch. This allows us to review and pull in new features or improvements individually. + +## Style Guide + +All pull requests must adhere to the [PSR-2 standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md). diff --git a/README.md b/README.md index f535d3f98df67625ca4d5718c8818d065ab46c53..5d238f6fbab9f9b7df49f33ec38a2f7ba7bc418c 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,42 @@ -# project-covid +# Slim Framework 4 Skeleton Application +[](https://coveralls.io/github/slimphp/Slim-Skeleton?branch=master) + +Use this skeleton application to quickly setup and start working on a new Slim Framework 4 application. This application uses the latest Slim 4 with Slim PSR-7 implementation and PHP-DI container implementation. It also uses the Monolog logger. + +This skeleton application was built for Composer. This makes setting up a new Slim Framework application quick and easy. + +## Install the Application + +Run this command from the directory in which you want to install your new Slim Framework application. + +```bash +composer create-project slim/slim-skeleton [my-app-name] +``` + +Replace `[my-app-name]` with the desired directory name for your new application. You'll want to: + +* Point your virtual host document root to your new application's `public/` directory. +* Ensure `logs/` is web writable. + +To run the application in development, you can run these commands + +```bash +cd [my-app-name] +composer start +``` + +Or you can use `docker-compose` to run the app with `docker`, so you can run these commands: +```bash +cd [my-app-name] +docker-compose up -d +``` +After that, open `http://localhost:8080` in your browser. + +Run this command in the application directory to run the test suite + +```bash +composer test +``` + +That's it! Now go build something cool. diff --git a/app/dependencies.php b/app/dependencies.php new file mode 100644 index 0000000000000000000000000000000000000000..1ff8d29256dbd3f4ba3a41e3637a9910c2e8ab0a --- /dev/null +++ b/app/dependencies.php @@ -0,0 +1,28 @@ +<?php +declare(strict_types=1); + +use DI\ContainerBuilder; +use Monolog\Handler\StreamHandler; +use Monolog\Logger; +use Monolog\Processor\UidProcessor; +use Psr\Container\ContainerInterface; +use Psr\Log\LoggerInterface; + +return function (ContainerBuilder $containerBuilder) { + $containerBuilder->addDefinitions([ + LoggerInterface::class => function (ContainerInterface $c) { + $settings = $c->get('settings'); + + $loggerSettings = $settings['logger']; + $logger = new Logger($loggerSettings['name']); + + $processor = new UidProcessor(); + $logger->pushProcessor($processor); + + $handler = new StreamHandler($loggerSettings['path'], $loggerSettings['level']); + $logger->pushHandler($handler); + + return $logger; + }, + ]); +}; diff --git a/app/middleware.php b/app/middleware.php new file mode 100644 index 0000000000000000000000000000000000000000..20c89809f8412a569689c66361875f362b8c9692 --- /dev/null +++ b/app/middleware.php @@ -0,0 +1,9 @@ +<?php +declare(strict_types=1); + +use App\Application\Middleware\SessionMiddleware; +use Slim\App; + +return function (App $app) { + $app->add(SessionMiddleware::class); +}; diff --git a/app/repositories.php b/app/repositories.php new file mode 100644 index 0000000000000000000000000000000000000000..62a58f3e20c20457276b182eece699e46c548636 --- /dev/null +++ b/app/repositories.php @@ -0,0 +1,13 @@ +<?php +declare(strict_types=1); + +use App\Domain\User\UserRepository; +use App\Infrastructure\Persistence\User\InMemoryUserRepository; +use DI\ContainerBuilder; + +return function (ContainerBuilder $containerBuilder) { + // Here we map our UserRepository interface to its in memory implementation + $containerBuilder->addDefinitions([ + UserRepository::class => \DI\autowire(InMemoryUserRepository::class), + ]); +}; diff --git a/app/routes.php b/app/routes.php new file mode 100644 index 0000000000000000000000000000000000000000..d4a2fffdd54b37822de6e0d747bc37ba7fc19bca --- /dev/null +++ b/app/routes.php @@ -0,0 +1,26 @@ +<?php +declare(strict_types=1); + +use App\Application\Actions\User\ListUsersAction; +use App\Application\Actions\User\ViewUserAction; +use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; +use Slim\App; +use Slim\Interfaces\RouteCollectorProxyInterface as Group; + +return function (App $app) { + $app->options('/{routes:.*}', function (Request $request, Response $response) { + // CORS Pre-Flight OPTIONS Request Handler + return $response; + }); + + $app->get('/', function (Request $request, Response $response) { + $response->getBody()->write('Hello world!'); + return $response; + }); + + $app->group('/users', function (Group $group) { + $group->get('', ListUsersAction::class); + $group->get('/{id}', ViewUserAction::class); + }); +}; diff --git a/app/settings.php b/app/settings.php new file mode 100644 index 0000000000000000000000000000000000000000..9db7ad118c96eb7924a43bce9fe7b19c8dfc3966 --- /dev/null +++ b/app/settings.php @@ -0,0 +1,19 @@ +<?php +declare(strict_types=1); + +use DI\ContainerBuilder; +use Monolog\Logger; + +return function (ContainerBuilder $containerBuilder) { + // Global Settings Object + $containerBuilder->addDefinitions([ + 'settings' => [ + 'displayErrorDetails' => true, // Should be set to false in production + 'logger' => [ + 'name' => 'slim-app', + 'path' => isset($_ENV['docker']) ? 'php://stdout' : __DIR__ . '/../logs/app.log', + 'level' => Logger::DEBUG, + ], + ], + ]); +}; diff --git a/composer.json b/composer.json new file mode 100644 index 0000000000000000000000000000000000000000..00d20764b77391e20c38ea5c15cb89f29c7f6610 --- /dev/null +++ b/composer.json @@ -0,0 +1,56 @@ +{ + "name": "slim/slim-skeleton", + "description": "A Slim Framework skeleton application for rapid development", + "keywords": [ + "microframework", + "rest", + "router", + "psr7" + ], + "homepage": "http://github.com/slimphp/Slim-Skeleton", + "license": "MIT", + "authors": [ + { + "name": "Josh Lockhart", + "email": "info@joshlockhart.com", + "homepage": "http://www.joshlockhart.com/" + }, + { + "name": "Pierre Berube", + "email": "pierre@lgse.com", + "homepage": "http://www.lgse.com/" + } + ], + "require": { + "php": "^7.2", + "ext-json": "*", + "monolog/monolog": "^2.1", + "php-di/php-di": "^6.2", + "slim/psr7": "^1.1", + "slim/slim": "^4.5" + }, + "require-dev": { + "jangregor/phpstan-prophecy": "^0.8.0", + "phpstan/extension-installer": "^1.0.4", + "phpstan/phpstan": "^0.12.37", + "phpunit/phpunit": "^8.5" + }, + "config": { + "process-timeout": 0, + "sort-packages": true + }, + "autoload": { + "psr-4": { + "App\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Tests\\": "tests/" + } + }, + "scripts": { + "start": "php -S localhost:8080 -t public", + "test": "phpunit" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000000000000000000000000000000000000..ff6431a67beda497ace56b95b46184519a71e8d4 --- /dev/null +++ b/composer.lock @@ -0,0 +1,2686 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "2e2028ef90191ac3ecc1aa6e8436de9e", + "packages": [ + { + "name": "fig/http-message-util", + "version": "1.1.4", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message-util.git", + "reference": "3242caa9da7221a304b8f84eb9eaddae0a7cf422" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message-util/zipball/3242caa9da7221a304b8f84eb9eaddae0a7cf422", + "reference": "3242caa9da7221a304b8f84eb9eaddae0a7cf422", + "shasum": "" + }, + "require": { + "php": "^5.3 || ^7.0" + }, + "suggest": { + "psr/http-message": "The package containing the PSR-7 interfaces" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Fig\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Utility classes and constants for use with PSR-7 (psr/http-message)", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "time": "2020-02-05T20:36:27+00:00" + }, + { + "name": "monolog/monolog", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "f9eee5cec93dfb313a38b6b288741e84e53f02d5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/f9eee5cec93dfb313a38b6b288741e84e53f02d5", + "reference": "f9eee5cec93dfb313a38b6b288741e84e53f02d5", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "psr/log": "^1.0.1" + }, + "provide": { + "psr/log-implementation": "1.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^2.4.9 || ^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^6.0", + "graylog2/gelf-php": "^1.4.2", + "php-amqplib/php-amqplib": "~2.4", + "php-console/php-console": "^3.1.3", + "php-parallel-lint/php-parallel-lint": "^1.0", + "phpspec/prophecy": "^1.6.1", + "phpunit/phpunit": "^8.5", + "predis/predis": "^1.1", + "rollbar/rollbar": "^1.3", + "ruflin/elastica": ">=0.90 <3.0", + "swiftmailer/swiftmailer": "^5.3|^6.0" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "php-console/php-console": "Allow sending log messages to Google Chrome", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "http://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2020-07-23T08:41:23+00:00" + }, + { + "name": "nikic/fast-route", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/FastRoute.git", + "reference": "181d480e08d9476e61381e04a71b34dc0432e812" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/FastRoute/zipball/181d480e08d9476e61381e04a71b34dc0432e812", + "reference": "181d480e08d9476e61381e04a71b34dc0432e812", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35|~5.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "FastRoute\\": "src/" + }, + "files": [ + "src/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov", + "email": "nikic@php.net" + } + ], + "description": "Fast request router for PHP", + "keywords": [ + "router", + "routing" + ], + "time": "2018-02-13T20:26:39+00:00" + }, + { + "name": "opis/closure", + "version": "3.5.7", + "source": { + "type": "git", + "url": "https://github.com/opis/closure.git", + "reference": "4531e53afe2fc660403e76fb7644e95998bff7bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opis/closure/zipball/4531e53afe2fc660403e76fb7644e95998bff7bf", + "reference": "4531e53afe2fc660403e76fb7644e95998bff7bf", + "shasum": "" + }, + "require": { + "php": "^5.4 || ^7.0" + }, + "require-dev": { + "jeremeamia/superclosure": "^2.0", + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.5.x-dev" + } + }, + "autoload": { + "psr-4": { + "Opis\\Closure\\": "src/" + }, + "files": [ + "functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marius Sarca", + "email": "marius.sarca@gmail.com" + }, + { + "name": "Sorin Sarca", + "email": "sarca_sorin@hotmail.com" + } + ], + "description": "A library that can be used to serialize closures (anonymous functions) and arbitrary objects.", + "homepage": "https://opis.io/closure", + "keywords": [ + "anonymous functions", + "closure", + "function", + "serializable", + "serialization", + "serialize" + ], + "time": "2020-09-06T17:02:15+00:00" + }, + { + "name": "php-di/invoker", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/PHP-DI/Invoker.git", + "reference": "6a6f8f276d2680e77d06294b9fd67b4881b1f82d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-DI/Invoker/zipball/6a6f8f276d2680e77d06294b9fd67b4881b1f82d", + "reference": "6a6f8f276d2680e77d06294b9fd67b4881b1f82d", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "psr/container": "~1.0" + }, + "require-dev": { + "athletic/athletic": "~0.1.8", + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Invoker\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Generic and extensible callable invoker", + "homepage": "https://github.com/PHP-DI/Invoker", + "keywords": [ + "callable", + "dependency", + "dependency-injection", + "injection", + "invoke", + "invoker" + ], + "time": "2020-08-01T15:36:25+00:00" + }, + { + "name": "php-di/php-di", + "version": "6.2.2", + "source": { + "type": "git", + "url": "https://github.com/PHP-DI/PHP-DI.git", + "reference": "40140b5bca07c5fed6919a0f1029ff67617faccd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-DI/PHP-DI/zipball/40140b5bca07c5fed6919a0f1029ff67617faccd", + "reference": "40140b5bca07c5fed6919a0f1029ff67617faccd", + "shasum": "" + }, + "require": { + "opis/closure": "^3.5.5", + "php": ">=7.2.0", + "php-di/invoker": "^2.0", + "php-di/phpdoc-reader": "^2.0.1", + "psr/container": "^1.0" + }, + "provide": { + "psr/container-implementation": "^1.0" + }, + "require-dev": { + "doctrine/annotations": "~1.2", + "friendsofphp/php-cs-fixer": "^2.4", + "mnapoli/phpunit-easymock": "^1.2", + "ocramius/proxy-manager": "~2.0.2", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^8.5" + }, + "suggest": { + "doctrine/annotations": "Install it if you want to use annotations (version ~1.2)", + "ocramius/proxy-manager": "Install it if you want to use lazy injection (version ~2.0)" + }, + "type": "library", + "autoload": { + "psr-4": { + "DI\\": "src/" + }, + "files": [ + "src/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The dependency injection container for humans", + "homepage": "https://php-di.org/", + "keywords": [ + "PSR-11", + "container", + "container-interop", + "dependency injection", + "di", + "ioc", + "psr11" + ], + "funding": [ + { + "url": "https://github.com/mnapoli", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/php-di/php-di", + "type": "tidelift" + } + ], + "time": "2020-08-23T16:23:17+00:00" + }, + { + "name": "php-di/phpdoc-reader", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/PHP-DI/PhpDocReader.git", + "reference": "15678f7451c020226807f520efb867ad26fbbfcf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-DI/PhpDocReader/zipball/15678f7451c020226807f520efb867ad26fbbfcf", + "reference": "15678f7451c020226807f520efb867ad26fbbfcf", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.6" + }, + "type": "library", + "autoload": { + "psr-4": { + "PhpDocReader\\": "src/PhpDocReader" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PhpDocReader parses @var and @param values in PHP docblocks (supports namespaced class names with the same resolution rules as PHP)", + "keywords": [ + "phpdoc", + "reflection" + ], + "time": "2019-09-26T11:24:58+00:00" + }, + { + "name": "psr/container", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "time": "2017-02-14T16:28:37+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/12ac7fcd07e5b077433f5f2bee95b3a771bf61be", + "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be", + "shasum": "" + }, + "require": { + "php": ">=7.0.0", + "psr/http-message": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "time": "2019-04-30T12:38:16+00:00" + }, + { + "name": "psr/http-message", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "time": "2016-08-06T14:39:51+00:00" + }, + { + "name": "psr/http-server-handler", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-server-handler.git", + "reference": "aff2f80e33b7f026ec96bb42f63242dc50ffcae7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/aff2f80e33b7f026ec96bb42f63242dc50ffcae7", + "reference": "aff2f80e33b7f026ec96bb42f63242dc50ffcae7", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "psr/http-message": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP server-side request handler", + "keywords": [ + "handler", + "http", + "http-interop", + "psr", + "psr-15", + "psr-7", + "request", + "response", + "server" + ], + "time": "2018-10-30T16:46:14+00:00" + }, + { + "name": "psr/http-server-middleware", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-server-middleware.git", + "reference": "2296f45510945530b9dceb8bcedb5cb84d40c5f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-server-middleware/zipball/2296f45510945530b9dceb8bcedb5cb84d40c5f5", + "reference": "2296f45510945530b9dceb8bcedb5cb84d40c5f5", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "psr/http-message": "^1.0", + "psr/http-server-handler": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP server-side middleware", + "keywords": [ + "http", + "http-interop", + "middleware", + "psr", + "psr-15", + "psr-7", + "request", + "response" + ], + "time": "2018-10-30T17:12:04+00:00" + }, + { + "name": "psr/log", + "version": "1.1.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc", + "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "time": "2020-03-23T09:12:05+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "slim/psr7", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/slimphp/Slim-Psr7.git", + "reference": "832912cb3c2a807d472ef0ac392552e85703a667" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/slimphp/Slim-Psr7/zipball/832912cb3c2a807d472ef0ac392552e85703a667", + "reference": "832912cb3c2a807d472ef0ac392552e85703a667", + "shasum": "" + }, + "require": { + "fig/http-message-util": "^1.1.4", + "php": "^7.2", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0", + "ralouphie/getallheaders": "^3" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "adriansuter/php-autoload-override": "^1.2", + "ext-json": "*", + "http-interop/http-factory-tests": "^0.6.0", + "php-http/psr7-integration-tests": "dev-master", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^8.5", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Slim\\Psr7\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Josh Lockhart", + "email": "hello@joshlockhart.com", + "homepage": "http://joshlockhart.com" + }, + { + "name": "Andrew Smith", + "email": "a.smith@silentworks.co.uk", + "homepage": "http://silentworks.co.uk" + }, + { + "name": "Rob Allen", + "email": "rob@akrabat.com", + "homepage": "http://akrabat.com" + }, + { + "name": "Pierre Berube", + "email": "pierre@lgse.com", + "homepage": "http://www.lgse.com" + } + ], + "description": "Strict PSR-7 implementation", + "homepage": "https://www.slimframework.com", + "keywords": [ + "http", + "psr-7", + "psr7" + ], + "time": "2020-08-18T22:49:11+00:00" + }, + { + "name": "slim/slim", + "version": "4.5.0", + "source": { + "type": "git", + "url": "https://github.com/slimphp/Slim.git", + "reference": "5613cbb521081ed676d5d7eb3e44f2b80a818c24" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/slimphp/Slim/zipball/5613cbb521081ed676d5d7eb3e44f2b80a818c24", + "reference": "5613cbb521081ed676d5d7eb3e44f2b80a818c24", + "shasum": "" + }, + "require": { + "ext-json": "*", + "nikic/fast-route": "^1.3", + "php": "^7.2", + "psr/container": "^1.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0", + "psr/http-server-handler": "^1.0", + "psr/http-server-middleware": "^1.0", + "psr/log": "^1.1" + }, + "require-dev": { + "adriansuter/php-autoload-override": "^1.0", + "ext-simplexml": "*", + "guzzlehttp/psr7": "^1.5", + "http-interop/http-factory-guzzle": "^1.0", + "laminas/laminas-diactoros": "^2.1", + "nyholm/psr7": "^1.1", + "nyholm/psr7-server": "^0.3.0", + "phpspec/prophecy": "^1.10", + "phpstan/phpstan": "^0.11.5", + "phpunit/phpunit": "^8.5", + "slim/http": "^1.0", + "slim/psr7": "^1.0", + "squizlabs/php_codesniffer": "^3.5" + }, + "suggest": { + "ext-simplexml": "Needed to support XML format in BodyParsingMiddleware", + "ext-xml": "Needed to support XML format in BodyParsingMiddleware", + "php-di/php-di": "PHP-DI is the recommended container library to be used with Slim", + "slim/psr7": "Slim PSR-7 implementation. See https://www.slimframework.com/docs/v4/start/installation.html for more information." + }, + "type": "library", + "autoload": { + "psr-4": { + "Slim\\": "Slim" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Josh Lockhart", + "email": "hello@joshlockhart.com", + "homepage": "https://joshlockhart.com" + }, + { + "name": "Andrew Smith", + "email": "a.smith@silentworks.co.uk", + "homepage": "http://silentworks.co.uk" + }, + { + "name": "Rob Allen", + "email": "rob@akrabat.com", + "homepage": "http://akrabat.com" + }, + { + "name": "Pierre Berube", + "email": "pierre@lgse.com", + "homepage": "http://www.lgse.com" + }, + { + "name": "Gabriel Manricks", + "email": "gmanricks@me.com", + "homepage": "http://gabrielmanricks.com" + } + ], + "description": "Slim is a PHP micro framework that helps you quickly write simple yet powerful web applications and APIs", + "homepage": "https://www.slimframework.com", + "keywords": [ + "api", + "framework", + "micro", + "router" + ], + "funding": [ + { + "url": "https://opencollective.com/slimphp", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/slim/slim", + "type": "tidelift" + } + ], + "time": "2020-04-14T20:49:48+00:00" + } + ], + "packages-dev": [ + { + "name": "doctrine/instantiator", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "f350df0268e904597e3bd9c4685c53e0e333feea" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/f350df0268e904597e3bd9c4685c53e0e333feea", + "reference": "f350df0268e904597e3bd9c4685c53e0e333feea", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^6.0", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^0.13", + "phpstan/phpstan-phpunit": "^0.11", + "phpstan/phpstan-shim": "^0.11", + "phpunit/phpunit": "^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "http://ocramius.github.com/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2020-05-29T17:27:14+00:00" + }, + { + "name": "jangregor/phpstan-prophecy", + "version": "0.8.0", + "source": { + "type": "git", + "url": "https://github.com/Jan0707/phpstan-prophecy.git", + "reference": "d599ec96dce8087ba4f32d7d9f9cb4565239e3db" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Jan0707/phpstan-prophecy/zipball/d599ec96dce8087ba4f32d7d9f9cb4565239e3db", + "reference": "d599ec96dce8087ba4f32d7d9f9cb4565239e3db", + "shasum": "" + }, + "require": { + "php": "^7.1", + "phpstan/phpstan": "^0.12.6" + }, + "conflict": { + "phpspec/prophecy": "<1.7.0,>=2.0.0", + "phpunit/phpunit": "<6.0.0,>=10.0.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.1.1", + "ergebnis/license": "^1.0.0", + "ergebnis/php-cs-fixer-config": "~1.1.2", + "phpspec/prophecy": "^1.7.0", + "phpunit/phpunit": "^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "violinist": { + "allow_updates_beyond_constraint": 0, + "one_pull_request_per_package": 1, + "update_with_dependencies": 1 + } + }, + "autoload": { + "psr-4": { + "JanGregor\\Prophecy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Gregor Emge-Triebel", + "email": "jan@jangregor.me" + } + ], + "description": "Provides a phpstan/phpstan extension for phpspec/prophecy", + "funding": [ + { + "url": "https://www.buymeacoffee.com/localheinz", + "type": "custom" + }, + { + "url": "https://github.com/localheinz", + "type": "github" + } + ], + "time": "2020-05-12T08:04:04+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.10.1", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "969b211f9a51aa1f6c01d1d2aef56d3bd91598e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/969b211f9a51aa1f6c01d1d2aef56d3bd91598e5", + "reference": "969b211f9a51aa1f6c01d1d2aef56d3bd91598e5", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "replace": { + "myclabs/deep-copy": "self.version" + }, + "require-dev": { + "doctrine/collections": "^1.0", + "doctrine/common": "^2.6", + "phpunit/phpunit": "^7.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + }, + "files": [ + "src/DeepCopy/deep_copy.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2020-06-29T13:22:24+00:00" + }, + { + "name": "phar-io/manifest", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", + "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-phar": "*", + "phar-io/version": "^2.0", + "php": "^5.6 || ^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.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": "2018-07-08T19:23:20+00:00" + }, + { + "name": "phar-io/version", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/45a2ec53a73c70ce41d55cedef9063630abaf1b6", + "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.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": "2018-07-08T19:19:57+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": "phpstan/extension-installer", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/phpstan/extension-installer.git", + "reference": "5c2da3846819f951385cb6a25d3277051481c48a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/extension-installer/zipball/5c2da3846819f951385cb6a25d3277051481c48a", + "reference": "5c2da3846819f951385cb6a25d3277051481c48a", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.1 || ^2.0", + "php": "^7.1 || ^8.0", + "phpstan/phpstan": ">=0.11.6" + }, + "require-dev": { + "composer/composer": "^1.8", + "consistence/coding-standard": "^3.8", + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", + "ergebnis/composer-normalize": "^2.0.2", + "phing/phing": "^2.16", + "php-parallel-lint/php-parallel-lint": "^1.2.0", + "phpstan/phpstan-strict-rules": "^0.11", + "slevomat/coding-standard": "^5.0.4" + }, + "type": "composer-plugin", + "extra": { + "class": "PHPStan\\ExtensionInstaller\\Plugin" + }, + "autoload": { + "psr-4": { + "PHPStan\\ExtensionInstaller\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Composer plugin for automatic installation of PHPStan extensions", + "time": "2020-08-30T12:06:42+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "0.12.42", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "7c43b7c2d5ca6554f6231e82e342a710163ac5f4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/7c43b7c2d5ca6554f6231e82e342a710163ac5f4", + "reference": "7c43b7c2d5ca6554f6231e82e342a710163ac5f4", + "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-09-02T13:14:53+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "7.0.10", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "f1884187926fbb755a9aaf0b3836ad3165b478bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/f1884187926fbb755a9aaf0b3836ad3165b478bf", + "reference": "f1884187926fbb755a9aaf0b3836ad3165b478bf", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xmlwriter": "*", + "php": "^7.2", + "phpunit/php-file-iterator": "^2.0.2", + "phpunit/php-text-template": "^1.2.1", + "phpunit/php-token-stream": "^3.1.1", + "sebastian/code-unit-reverse-lookup": "^1.0.1", + "sebastian/environment": "^4.2.2", + "sebastian/version": "^2.0.1", + "theseer/tokenizer": "^1.1.3" + }, + "require-dev": { + "phpunit/phpunit": "^8.2.2" + }, + "suggest": { + "ext-xdebug": "^2.7.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.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 provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "time": "2019-11-20T13:55:58+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "050bedf145a257b1ff02746c31894800e5122946" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/050bedf145a257b1ff02746c31894800e5122946", + "reference": "050bedf145a257b1ff02746c31894800e5122946", + "shasum": "" + }, + "require": { + "php": "^7.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.1" + }, + "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": "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" + ], + "time": "2018-09-13T20:33:42+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "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" + ], + "time": "2015-06-21T13:50:34+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "2.1.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "1038454804406b0b5f5f520358e78c1c2f71501e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/1038454804406b0b5f5f520358e78c1c2f71501e", + "reference": "1038454804406b0b5f5f520358e78c1c2f71501e", + "shasum": "" + }, + "require": { + "php": "^7.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.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": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "time": "2019-06-07T04:22:29+00:00" + }, + { + "name": "phpunit/php-token-stream", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-token-stream.git", + "reference": "995192df77f63a59e47f025390d2d1fdf8f425ff" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/995192df77f63a59e47f025390d2d1fdf8f425ff", + "reference": "995192df77f63a59e47f025390d2d1fdf8f425ff", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.0" + }, + "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" + } + ], + "description": "Wrapper around PHP's tokenizer extension.", + "homepage": "https://github.com/sebastianbergmann/php-token-stream/", + "keywords": [ + "tokenizer" + ], + "abandoned": true, + "time": "2019-09-17T06:23:10+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "8.5.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "34c18baa6a44f1d1fbf0338907139e9dce95b997" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/34c18baa6a44f1d1fbf0338907139e9dce95b997", + "reference": "34c18baa6a44f1d1fbf0338907139e9dce95b997", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.2.0", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.9.1", + "phar-io/manifest": "^1.0.3", + "phar-io/version": "^2.0.1", + "php": "^7.2", + "phpspec/prophecy": "^1.8.1", + "phpunit/php-code-coverage": "^7.0.7", + "phpunit/php-file-iterator": "^2.0.2", + "phpunit/php-text-template": "^1.2.1", + "phpunit/php-timer": "^2.1.2", + "sebastian/comparator": "^3.0.2", + "sebastian/diff": "^3.0.2", + "sebastian/environment": "^4.2.2", + "sebastian/exporter": "^3.1.1", + "sebastian/global-state": "^3.0.0", + "sebastian/object-enumerator": "^3.0.3", + "sebastian/resource-operations": "^2.0.1", + "sebastian/type": "^1.1.3", + "sebastian/version": "^2.0.1" + }, + "require-dev": { + "ext-pdo": "*" + }, + "suggest": { + "ext-soap": "*", + "ext-xdebug": "*", + "phpunit/php-invoker": "^2.0.0" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "8.5-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": "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-06-22T07:06:58+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", + "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.7 || ^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-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/", + "time": "2017-03-04T06:30:41+00:00" + }, + { + "name": "sebastian/comparator", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/5de4fc177adf9bce8df98d8d141a7559d7ccf6da", + "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da", + "shasum": "" + }, + "require": { + "php": "^7.1", + "sebastian/diff": "^3.0", + "sebastian/exporter": "^3.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.1" + }, + "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": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "time": "2018-07-12T15:12:46+00:00" + }, + { + "name": "sebastian/diff", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/720fcc7e9b5cf384ea68d9d930d480907a0c1a29", + "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29", + "shasum": "" + }, + "require": { + "php": "^7.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.5 || ^8.0", + "symfony/process": "^2 || ^3.3 || ^4" + }, + "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": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "time": "2019-02-04T06:01:07+00:00" + }, + { + "name": "sebastian/environment", + "version": "4.2.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "464c90d7bdf5ad4e8a6aea15c091fec0603d4368" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/464c90d7bdf5ad4e8a6aea15c091fec0603d4368", + "reference": "464c90d7bdf5ad4e8a6aea15c091fec0603d4368", + "shasum": "" + }, + "require": { + "php": "^7.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.5" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.2-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" + ], + "time": "2019-11-20T08:46:58+00:00" + }, + { + "name": "sebastian/exporter", + "version": "3.1.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/68609e1261d215ea5b21b7987539cbfbe156ec3e", + "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e", + "shasum": "" + }, + "require": { + "php": "^7.0", + "sebastian/recursion-context": "^3.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1.x-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" + ], + "time": "2019-09-14T09:02:43+00:00" + }, + { + "name": "sebastian/global-state", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4", + "reference": "edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4", + "shasum": "" + }, + "require": { + "php": "^7.2", + "sebastian/object-reflector": "^1.1.1", + "sebastian/recursion-context": "^3.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^8.0" + }, + "suggest": { + "ext-uopz": "*" + }, + "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": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "time": "2019-02-01T05:30:01+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/7cfd9e65d11ffb5af41198476395774d4c8a84c5", + "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5", + "shasum": "" + }, + "require": { + "php": "^7.0", + "sebastian/object-reflector": "^1.1.1", + "sebastian/recursion-context": "^3.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-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/", + "time": "2017-08-03T12:35:26+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "773f97c67f28de00d397be301821b06708fca0be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/773f97c67f28de00d397be301821b06708fca0be", + "reference": "773f97c67f28de00d397be301821b06708fca0be", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-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/", + "time": "2017-03-29T09:07:27+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", + "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "time": "2017-03-03T06:23:57+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/4d7a795d35b889bf80a0cc04e08d77cedfa917a9", + "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9", + "shasum": "" + }, + "require": { + "php": "^7.1" + }, + "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": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "time": "2018-10-04T04:07:39+00:00" + }, + { + "name": "sebastian/type", + "version": "1.1.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "3aaaa15fa71d27650d62a948be022fe3b48541a3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/3aaaa15fa71d27650d62a948be022fe3b48541a3", + "reference": "3aaaa15fa71d27650d62a948be022fe3b48541a3", + "shasum": "" + }, + "require": { + "php": "^7.2" + }, + "require-dev": { + "phpunit/phpunit": "^8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.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": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "time": "2019-07-02T08:10:15+00:00" + }, + { + "name": "sebastian/version", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "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": "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", + "time": "2016-10-03T07:35:21+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.18.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "1c302646f6efc070cd46856e600e5e0684d6b454" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/1c302646f6efc070cd46856e600e5e0684d6b454", + "reference": "1c302646f6efc070cd46856e600e5e0684d6b454", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.18-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "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-07-14T12:35:20+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", + "ext-json": "*" + }, + "platform-dev": [], + "plugin-api-version": "1.1.0" +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..434a8024631d5f6f5020d1746e49330cac9df937 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,18 @@ +version: '3.7' + +volumes: + logs: + driver: local + +services: + slim: + image: php:7-alpine + working_dir: /var/www + command: php -S 0.0.0.0:8080 -t public + environment: + docker: "true" + ports: + - 8080:8080 + volumes: + - .:/var/www + - logs:/var/www/logs diff --git a/logs/README.md b/logs/README.md new file mode 100644 index 0000000000000000000000000000000000000000..d4a602efdfc53d49a4d216eed39f4bf39e126f0a --- /dev/null +++ b/logs/README.md @@ -0,0 +1 @@ +Your Slim Framework application's log files will be written to this directory. diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000000000000000000000000000000000000..45876ad85bb00cf33d04e670b51371af931932f7 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,2 @@ +parameters: + level: 4 diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000000000000000000000000000000000000..f83040094141d548f1d5ca2db29f89ed1c8d08ca --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,27 @@ +<phpunit + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/7.1/phpunit.xsd" + backupGlobals="false" + backupStaticAttributes="false" + beStrictAboutTestsThatDoNotTestAnything="true" + beStrictAboutChangesToGlobalState="true" + beStrictAboutOutputDuringTests="true" + colors="true" + convertErrorsToExceptions="true" + convertNoticesToExceptions="true" + convertWarningsToExceptions="true" + processIsolation="false" + stopOnFailure="false" + bootstrap="tests/bootstrap.php" +> + <testsuites> + <testsuite name="Test Suite"> + <directory>./tests/</directory> + </testsuite> + </testsuites> + <filter> + <whitelist processUncoveredFilesFromWhitelist="true"> + <directory suffix=".php">./src/</directory> + </whitelist> + </filter> +</phpunit> diff --git a/public/.htaccess b/public/.htaccess new file mode 100644 index 0000000000000000000000000000000000000000..f5d1969562278e2a0a9685d1a4aa271af601f08d --- /dev/null +++ b/public/.htaccess @@ -0,0 +1,21 @@ +<IfModule mod_rewrite.c> + RewriteEngine On + + # Some hosts may require you to use the `RewriteBase` directive. + # Determine the RewriteBase automatically and set it as environment variable. + # If you are using Apache aliases to do mass virtual hosting or installed the + # project in a subdirectory, the base path will be prepended to allow proper + # resolution of the index.php file and to redirect to the correct URI. It will + # work in environments without path prefix as well, providing a safe, one-size + # fits all solution. But as you do not need it in this case, you can comment + # the following 2 lines to eliminate the overhead. + RewriteCond %{REQUEST_URI}::$1 ^(/.+)/(.*)::\2$ + RewriteRule ^(.*) - [E=BASE:%1] + + # If the above doesn't work you might need to set the `RewriteBase` directive manually, it should be the + # absolute physical path to the directory that contains this htaccess file. + # RewriteBase / + + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^ index.php [QSA,L] +</IfModule> diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000000000000000000000000000000000000..57722ec7f0cad70058d5168f02b9def918e08919 --- /dev/null +++ b/public/index.php @@ -0,0 +1,73 @@ +<?php +declare(strict_types=1); + +use App\Application\Handlers\HttpErrorHandler; +use App\Application\Handlers\ShutdownHandler; +use App\Application\ResponseEmitter\ResponseEmitter; +use DI\ContainerBuilder; +use Slim\Factory\AppFactory; +use Slim\Factory\ServerRequestCreatorFactory; + +require __DIR__ . '/../vendor/autoload.php'; + +// Instantiate PHP-DI ContainerBuilder +$containerBuilder = new ContainerBuilder(); + +if (false) { // Should be set to true in production + $containerBuilder->enableCompilation(__DIR__ . '/../var/cache'); +} + +// Set up settings +$settings = require __DIR__ . '/../app/settings.php'; +$settings($containerBuilder); + +// Set up dependencies +$dependencies = require __DIR__ . '/../app/dependencies.php'; +$dependencies($containerBuilder); + +// Set up repositories +$repositories = require __DIR__ . '/../app/repositories.php'; +$repositories($containerBuilder); + +// Build PHP-DI Container instance +$container = $containerBuilder->build(); + +// Instantiate the app +AppFactory::setContainer($container); +$app = AppFactory::create(); +$callableResolver = $app->getCallableResolver(); + +// Register middleware +$middleware = require __DIR__ . '/../app/middleware.php'; +$middleware($app); + +// Register routes +$routes = require __DIR__ . '/../app/routes.php'; +$routes($app); + +/** @var bool $displayErrorDetails */ +$displayErrorDetails = $container->get('settings')['displayErrorDetails']; + +// Create Request object from globals +$serverRequestCreator = ServerRequestCreatorFactory::create(); +$request = $serverRequestCreator->createServerRequestFromGlobals(); + +// Create Error Handler +$responseFactory = $app->getResponseFactory(); +$errorHandler = new HttpErrorHandler($callableResolver, $responseFactory); + +// Create Shutdown Handler +$shutdownHandler = new ShutdownHandler($request, $errorHandler, $displayErrorDetails); +register_shutdown_function($shutdownHandler); + +// Add Routing Middleware +$app->addRoutingMiddleware(); + +// Add Error Middleware +$errorMiddleware = $app->addErrorMiddleware($displayErrorDetails, false, false); +$errorMiddleware->setDefaultErrorHandler($errorHandler); + +// Run App & Emit Response +$response = $app->handle($request); +$responseEmitter = new ResponseEmitter(); +$responseEmitter->emit($response); diff --git a/src/Application/Actions/Action.php b/src/Application/Actions/Action.php new file mode 100644 index 0000000000000000000000000000000000000000..107d7c45e3349e0438bdc4c37362c9b22b381863 --- /dev/null +++ b/src/Application/Actions/Action.php @@ -0,0 +1,124 @@ +<?php +declare(strict_types=1); + +namespace App\Application\Actions; + +use App\Domain\DomainException\DomainRecordNotFoundException; +use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Log\LoggerInterface; +use Slim\Exception\HttpBadRequestException; +use Slim\Exception\HttpNotFoundException; + +abstract class Action +{ + /** + * @var LoggerInterface + */ + protected $logger; + + /** + * @var Request + */ + protected $request; + + /** + * @var Response + */ + protected $response; + + /** + * @var array + */ + protected $args; + + /** + * @param LoggerInterface $logger + */ + public function __construct(LoggerInterface $logger) + { + $this->logger = $logger; + } + + /** + * @param Request $request + * @param Response $response + * @param array $args + * @return Response + * @throws HttpNotFoundException + * @throws HttpBadRequestException + */ + public function __invoke(Request $request, Response $response, $args): Response + { + $this->request = $request; + $this->response = $response; + $this->args = $args; + + try { + return $this->action(); + } catch (DomainRecordNotFoundException $e) { + throw new HttpNotFoundException($this->request, $e->getMessage()); + } + } + + /** + * @return Response + * @throws DomainRecordNotFoundException + * @throws HttpBadRequestException + */ + abstract protected function action(): Response; + + /** + * @return array|object + * @throws HttpBadRequestException + */ + protected function getFormData() + { + $input = json_decode(file_get_contents('php://input')); + + if (json_last_error() !== JSON_ERROR_NONE) { + throw new HttpBadRequestException($this->request, 'Malformed JSON input.'); + } + + return $input; + } + + /** + * @param string $name + * @return mixed + * @throws HttpBadRequestException + */ + protected function resolveArg(string $name) + { + if (!isset($this->args[$name])) { + throw new HttpBadRequestException($this->request, "Could not resolve argument `{$name}`."); + } + + return $this->args[$name]; + } + + /** + * @param array|object|null $data + * @return Response + */ + protected function respondWithData($data = null, int $statusCode = 200): Response + { + $payload = new ActionPayload($statusCode, $data); + + return $this->respond($payload); + } + + /** + * @param ActionPayload $payload + * @return Response + */ + protected function respond(ActionPayload $payload): Response + { + $json = json_encode($payload, JSON_PRETTY_PRINT); + $this->response->getBody()->write($json); + + return $this->response + ->withHeader('Content-Type', 'application/json') + ->withStatus($payload->getStatusCode()); + } +} diff --git a/src/Application/Actions/ActionError.php b/src/Application/Actions/ActionError.php new file mode 100644 index 0000000000000000000000000000000000000000..731b4ff94396c6190c3e2d24beac9de2a14b0a69 --- /dev/null +++ b/src/Application/Actions/ActionError.php @@ -0,0 +1,88 @@ +<?php +declare(strict_types=1); + +namespace App\Application\Actions; + +use JsonSerializable; + +class ActionError implements JsonSerializable +{ + public const BAD_REQUEST = 'BAD_REQUEST'; + public const INSUFFICIENT_PRIVILEGES = 'INSUFFICIENT_PRIVILEGES'; + public const NOT_ALLOWED = 'NOT_ALLOWED'; + public const NOT_IMPLEMENTED = 'NOT_IMPLEMENTED'; + public const RESOURCE_NOT_FOUND = 'RESOURCE_NOT_FOUND'; + public const SERVER_ERROR = 'SERVER_ERROR'; + public const UNAUTHENTICATED = 'UNAUTHENTICATED'; + public const VALIDATION_ERROR = 'VALIDATION_ERROR'; + public const VERIFICATION_ERROR = 'VERIFICATION_ERROR'; + + /** + * @var string + */ + private $type; + + /** + * @var string + */ + private $description; + + /** + * @param string $type + * @param string|null $description + */ + public function __construct(string $type, ?string $description) + { + $this->type = $type; + $this->description = $description; + } + + /** + * @return string + */ + public function getType(): string + { + return $this->type; + } + + /** + * @param string $type + * @return self + */ + public function setType(string $type): self + { + $this->type = $type; + return $this; + } + + /** + * @return string + */ + public function getDescription(): string + { + return $this->description; + } + + /** + * @param string|null $description + * @return self + */ + public function setDescription(?string $description = null): self + { + $this->description = $description; + return $this; + } + + /** + * @return array + */ + public function jsonSerialize() + { + $payload = [ + 'type' => $this->type, + 'description' => $this->description, + ]; + + return $payload; + } +} diff --git a/src/Application/Actions/ActionPayload.php b/src/Application/Actions/ActionPayload.php new file mode 100644 index 0000000000000000000000000000000000000000..a5954e1676c02acdaf2d01f5544f1e6c35e14349 --- /dev/null +++ b/src/Application/Actions/ActionPayload.php @@ -0,0 +1,81 @@ +<?php +declare(strict_types=1); + +namespace App\Application\Actions; + +use JsonSerializable; + +class ActionPayload implements JsonSerializable +{ + /** + * @var int + */ + private $statusCode; + + /** + * @var array|object|null + */ + private $data; + + /** + * @var ActionError|null + */ + private $error; + + /** + * @param int $statusCode + * @param array|object|null $data + * @param ActionError|null $error + */ + public function __construct( + int $statusCode = 200, + $data = null, + ?ActionError $error = null + ) { + $this->statusCode = $statusCode; + $this->data = $data; + $this->error = $error; + } + + /** + * @return int + */ + public function getStatusCode(): int + { + return $this->statusCode; + } + + /** + * @return array|null|object + */ + public function getData() + { + return $this->data; + } + + /** + * @return ActionError|null + */ + public function getError(): ?ActionError + { + return $this->error; + } + + /** + * @return array + */ + public function jsonSerialize() + { + $payload = [ + 'statusCode' => $this->statusCode, + ]; + + if ($this->data !== null) { + $payload['data'] = $this->data; + } elseif ($this->error !== null) { + $payload['error'] = $this->error; + } + + return $payload; + } +} diff --git a/src/Application/Actions/User/ListUsersAction.php b/src/Application/Actions/User/ListUsersAction.php new file mode 100644 index 0000000000000000000000000000000000000000..21b47503f4024b8a24a5c47c5e5ca2be24ecf82c --- /dev/null +++ b/src/Application/Actions/User/ListUsersAction.php @@ -0,0 +1,21 @@ +<?php +declare(strict_types=1); + +namespace App\Application\Actions\User; + +use Psr\Http\Message\ResponseInterface as Response; + +class ListUsersAction extends UserAction +{ + /** + * {@inheritdoc} + */ + protected function action(): Response + { + $users = $this->userRepository->findAll(); + + $this->logger->info("Users list was viewed."); + + return $this->respondWithData($users); + } +} diff --git a/src/Application/Actions/User/UserAction.php b/src/Application/Actions/User/UserAction.php new file mode 100644 index 0000000000000000000000000000000000000000..45424833ec79ec19c86c04061fdc0f03c396f814 --- /dev/null +++ b/src/Application/Actions/User/UserAction.php @@ -0,0 +1,26 @@ +<?php +declare(strict_types=1); + +namespace App\Application\Actions\User; + +use App\Application\Actions\Action; +use App\Domain\User\UserRepository; +use Psr\Log\LoggerInterface; + +abstract class UserAction extends Action +{ + /** + * @var UserRepository + */ + protected $userRepository; + + /** + * @param LoggerInterface $logger + * @param UserRepository $userRepository + */ + public function __construct(LoggerInterface $logger, UserRepository $userRepository) + { + parent::__construct($logger); + $this->userRepository = $userRepository; + } +} diff --git a/src/Application/Actions/User/ViewUserAction.php b/src/Application/Actions/User/ViewUserAction.php new file mode 100644 index 0000000000000000000000000000000000000000..519d5fa1f7d4871bfbc046c624679e991a949060 --- /dev/null +++ b/src/Application/Actions/User/ViewUserAction.php @@ -0,0 +1,22 @@ +<?php +declare(strict_types=1); + +namespace App\Application\Actions\User; + +use Psr\Http\Message\ResponseInterface as Response; + +class ViewUserAction extends UserAction +{ + /** + * {@inheritdoc} + */ + protected function action(): Response + { + $userId = (int) $this->resolveArg('id'); + $user = $this->userRepository->findUserOfId($userId); + + $this->logger->info("User of id `${userId}` was viewed."); + + return $this->respondWithData($user); + } +} diff --git a/src/Application/Handlers/HttpErrorHandler.php b/src/Application/Handlers/HttpErrorHandler.php new file mode 100644 index 0000000000000000000000000000000000000000..1fc31f67dc322b91d872a9ca6ab7d3fd12655dd5 --- /dev/null +++ b/src/Application/Handlers/HttpErrorHandler.php @@ -0,0 +1,69 @@ +<?php +declare(strict_types=1); + +namespace App\Application\Handlers; + +use App\Application\Actions\ActionError; +use App\Application\Actions\ActionPayload; +use Exception; +use Psr\Http\Message\ResponseInterface as Response; +use Slim\Exception\HttpBadRequestException; +use Slim\Exception\HttpException; +use Slim\Exception\HttpForbiddenException; +use Slim\Exception\HttpMethodNotAllowedException; +use Slim\Exception\HttpNotFoundException; +use Slim\Exception\HttpNotImplementedException; +use Slim\Exception\HttpUnauthorizedException; +use Slim\Handlers\ErrorHandler as SlimErrorHandler; +use Throwable; + +class HttpErrorHandler extends SlimErrorHandler +{ + /** + * @inheritdoc + */ + protected function respond(): Response + { + $exception = $this->exception; + $statusCode = 500; + $error = new ActionError( + ActionError::SERVER_ERROR, + 'An internal error has occurred while processing your request.' + ); + + if ($exception instanceof HttpException) { + $statusCode = $exception->getCode(); + $error->setDescription($exception->getMessage()); + + if ($exception instanceof HttpNotFoundException) { + $error->setType(ActionError::RESOURCE_NOT_FOUND); + } elseif ($exception instanceof HttpMethodNotAllowedException) { + $error->setType(ActionError::NOT_ALLOWED); + } elseif ($exception instanceof HttpUnauthorizedException) { + $error->setType(ActionError::UNAUTHENTICATED); + } elseif ($exception instanceof HttpForbiddenException) { + $error->setType(ActionError::INSUFFICIENT_PRIVILEGES); + } elseif ($exception instanceof HttpBadRequestException) { + $error->setType(ActionError::BAD_REQUEST); + } elseif ($exception instanceof HttpNotImplementedException) { + $error->setType(ActionError::NOT_IMPLEMENTED); + } + } + + if ( + !($exception instanceof HttpException) + && $exception instanceof Throwable + && $this->displayErrorDetails + ) { + $error->setDescription($exception->getMessage()); + } + + $payload = new ActionPayload($statusCode, null, $error); + $encodedPayload = json_encode($payload, JSON_PRETTY_PRINT); + + $response = $this->responseFactory->createResponse($statusCode); + $response->getBody()->write($encodedPayload); + + return $response->withHeader('Content-Type', 'application/json'); + } +} diff --git a/src/Application/Handlers/ShutdownHandler.php b/src/Application/Handlers/ShutdownHandler.php new file mode 100644 index 0000000000000000000000000000000000000000..9a593f7526e4e78b371e6742092701c08dff10ef --- /dev/null +++ b/src/Application/Handlers/ShutdownHandler.php @@ -0,0 +1,83 @@ +<?php +declare(strict_types=1); + +namespace App\Application\Handlers; + +use App\Application\ResponseEmitter\ResponseEmitter; +use Psr\Http\Message\ServerRequestInterface as Request; +use Slim\Exception\HttpInternalServerErrorException; + +class ShutdownHandler +{ + /** + * @var Request + */ + private $request; + + /** + * @var HttpErrorHandler + */ + private $errorHandler; + + /** + * @var bool + */ + private $displayErrorDetails; + + /** + * ShutdownHandler constructor. + * + * @param Request $request + * @param HttpErrorHandler $errorHandler + * @param bool $displayErrorDetails + */ + public function __construct( + Request $request, + HttpErrorHandler $errorHandler, + bool $displayErrorDetails + ) { + $this->request = $request; + $this->errorHandler = $errorHandler; + $this->displayErrorDetails = $displayErrorDetails; + } + + public function __invoke() + { + $error = error_get_last(); + if ($error) { + $errorFile = $error['file']; + $errorLine = $error['line']; + $errorMessage = $error['message']; + $errorType = $error['type']; + $message = 'An error while processing your request. Please try again later.'; + + if ($this->displayErrorDetails) { + switch ($errorType) { + case E_USER_ERROR: + $message = "FATAL ERROR: {$errorMessage}. "; + $message .= " on line {$errorLine} in file {$errorFile}."; + break; + + case E_USER_WARNING: + $message = "WARNING: {$errorMessage}"; + break; + + case E_USER_NOTICE: + $message = "NOTICE: {$errorMessage}"; + break; + + default: + $message = "ERROR: {$errorMessage}"; + $message .= " on line {$errorLine} in file {$errorFile}."; + break; + } + } + + $exception = new HttpInternalServerErrorException($this->request, $message); + $response = $this->errorHandler->__invoke($this->request, $exception, $this->displayErrorDetails, false, false); + + $responseEmitter = new ResponseEmitter(); + $responseEmitter->emit($response); + } + } +} diff --git a/src/Application/Middleware/SessionMiddleware.php b/src/Application/Middleware/SessionMiddleware.php new file mode 100644 index 0000000000000000000000000000000000000000..cbfc1d6ed17a13e78b60d6c9bdcb42f36fd01692 --- /dev/null +++ b/src/Application/Middleware/SessionMiddleware.php @@ -0,0 +1,25 @@ +<?php +declare(strict_types=1); + +namespace App\Application\Middleware; + +use Psr\Http\Message\ResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Server\MiddlewareInterface as Middleware; +use Psr\Http\Server\RequestHandlerInterface as RequestHandler; + +class SessionMiddleware implements Middleware +{ + /** + * {@inheritdoc} + */ + public function process(Request $request, RequestHandler $handler): Response + { + if (isset($_SERVER['HTTP_AUTHORIZATION'])) { + session_start(); + $request = $request->withAttribute('session', $_SESSION); + } + + return $handler->handle($request); + } +} diff --git a/src/Application/ResponseEmitter/ResponseEmitter.php b/src/Application/ResponseEmitter/ResponseEmitter.php new file mode 100644 index 0000000000000000000000000000000000000000..cebe4435f78c7f60403c47b3c82c6068a0fb420d --- /dev/null +++ b/src/Application/ResponseEmitter/ResponseEmitter.php @@ -0,0 +1,34 @@ +<?php +declare(strict_types=1); + +namespace App\Application\ResponseEmitter; + +use Psr\Http\Message\ResponseInterface; +use Slim\ResponseEmitter as SlimResponseEmitter; + +class ResponseEmitter extends SlimResponseEmitter +{ + /** + * {@inheritdoc} + */ + public function emit(ResponseInterface $response): void + { + // This variable should be set to the allowed host from which your API can be accessed with + $origin = isset($_SERVER['HTTP_ORIGIN']) ? $_SERVER['HTTP_ORIGIN'] : ''; + + $response = $response + ->withHeader('Access-Control-Allow-Credentials', 'true') + ->withHeader('Access-Control-Allow-Origin', $origin) + ->withHeader('Access-Control-Allow-Headers', 'X-Requested-With, Content-Type, Accept, Origin, Authorization') + ->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS') + ->withHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') + ->withAddedHeader('Cache-Control', 'post-check=0, pre-check=0') + ->withHeader('Pragma', 'no-cache'); + + if (ob_get_contents()) { + ob_clean(); + } + + parent::emit($response); + } +} diff --git a/src/Domain/DomainException/DomainException.php b/src/Domain/DomainException/DomainException.php new file mode 100644 index 0000000000000000000000000000000000000000..3ffe5528e14858f31d513523946d0a4e3d25377d --- /dev/null +++ b/src/Domain/DomainException/DomainException.php @@ -0,0 +1,10 @@ +<?php +declare(strict_types=1); + +namespace App\Domain\DomainException; + +use Exception; + +abstract class DomainException extends Exception +{ +} diff --git a/src/Domain/DomainException/DomainRecordNotFoundException.php b/src/Domain/DomainException/DomainRecordNotFoundException.php new file mode 100644 index 0000000000000000000000000000000000000000..229e10971e890632aa1783ee67d33e02e54bc830 --- /dev/null +++ b/src/Domain/DomainException/DomainRecordNotFoundException.php @@ -0,0 +1,8 @@ +<?php +declare(strict_types=1); + +namespace App\Domain\DomainException; + +class DomainRecordNotFoundException extends DomainException +{ +} diff --git a/src/Domain/User/User.php b/src/Domain/User/User.php new file mode 100644 index 0000000000000000000000000000000000000000..553c3d377b777fcfeb9e5033014b3cd649621188 --- /dev/null +++ b/src/Domain/User/User.php @@ -0,0 +1,88 @@ +<?php +declare(strict_types=1); + +namespace App\Domain\User; + +use JsonSerializable; + +class User implements JsonSerializable +{ + /** + * @var int|null + */ + private $id; + + /** + * @var string + */ + private $username; + + /** + * @var string + */ + private $firstName; + + /** + * @var string + */ + private $lastName; + + /** + * @param int|null $id + * @param string $username + * @param string $firstName + * @param string $lastName + */ + public function __construct(?int $id, string $username, string $firstName, string $lastName) + { + $this->id = $id; + $this->username = strtolower($username); + $this->firstName = ucfirst($firstName); + $this->lastName = ucfirst($lastName); + } + + /** + * @return int|null + */ + public function getId(): ?int + { + return $this->id; + } + + /** + * @return string + */ + public function getUsername(): string + { + return $this->username; + } + + /** + * @return string + */ + public function getFirstName(): string + { + return $this->firstName; + } + + /** + * @return string + */ + public function getLastName(): string + { + return $this->lastName; + } + + /** + * @return array + */ + public function jsonSerialize() + { + return [ + 'id' => $this->id, + 'username' => $this->username, + 'firstName' => $this->firstName, + 'lastName' => $this->lastName, + ]; + } +} diff --git a/src/Domain/User/UserNotFoundException.php b/src/Domain/User/UserNotFoundException.php new file mode 100644 index 0000000000000000000000000000000000000000..2080774ab4fcda9a4ba8e9f8a705ca2ad1a6f3c2 --- /dev/null +++ b/src/Domain/User/UserNotFoundException.php @@ -0,0 +1,11 @@ +<?php +declare(strict_types=1); + +namespace App\Domain\User; + +use App\Domain\DomainException\DomainRecordNotFoundException; + +class UserNotFoundException extends DomainRecordNotFoundException +{ + public $message = 'The user you requested does not exist.'; +} diff --git a/src/Domain/User/UserRepository.php b/src/Domain/User/UserRepository.php new file mode 100644 index 0000000000000000000000000000000000000000..7085ebfa49b01619d676c02a62b594da7ca2be4c --- /dev/null +++ b/src/Domain/User/UserRepository.php @@ -0,0 +1,19 @@ +<?php +declare(strict_types=1); + +namespace App\Domain\User; + +interface UserRepository +{ + /** + * @return User[] + */ + public function findAll(): array; + + /** + * @param int $id + * @return User + * @throws UserNotFoundException + */ + public function findUserOfId(int $id): User; +} diff --git a/src/Infrastructure/Persistence/User/InMemoryUserRepository.php b/src/Infrastructure/Persistence/User/InMemoryUserRepository.php new file mode 100644 index 0000000000000000000000000000000000000000..93cb77129c669b34e5ae789cc8c4fe58308d9997 --- /dev/null +++ b/src/Infrastructure/Persistence/User/InMemoryUserRepository.php @@ -0,0 +1,52 @@ +<?php +declare(strict_types=1); + +namespace App\Infrastructure\Persistence\User; + +use App\Domain\User\User; +use App\Domain\User\UserNotFoundException; +use App\Domain\User\UserRepository; + +class InMemoryUserRepository implements UserRepository +{ + /** + * @var User[] + */ + private $users; + + /** + * InMemoryUserRepository constructor. + * + * @param array|null $users + */ + public function __construct(array $users = null) + { + $this->users = $users ?? [ + 1 => new User(1, 'bill.gates', 'Bill', 'Gates'), + 2 => new User(2, 'steve.jobs', 'Steve', 'Jobs'), + 3 => new User(3, 'mark.zuckerberg', 'Mark', 'Zuckerberg'), + 4 => new User(4, 'evan.spiegel', 'Evan', 'Spiegel'), + 5 => new User(5, 'jack.dorsey', 'Jack', 'Dorsey'), + ]; + } + + /** + * {@inheritdoc} + */ + public function findAll(): array + { + return array_values($this->users); + } + + /** + * {@inheritdoc} + */ + public function findUserOfId(int $id): User + { + if (!isset($this->users[$id])) { + throw new UserNotFoundException(); + } + + return $this->users[$id]; + } +} diff --git a/tests/Application/Actions/ActionTest.php b/tests/Application/Actions/ActionTest.php new file mode 100644 index 0000000000000000000000000000000000000000..69d41d22044f333420797c0ce1298abb14d83cbb --- /dev/null +++ b/tests/Application/Actions/ActionTest.php @@ -0,0 +1,78 @@ +<?php +declare(strict_types=1); + +namespace Tests\Application\Actions; + +use App\Application\Actions\Action; +use App\Application\Actions\ActionPayload; +use DateTimeImmutable; +use Psr\Http\Message\ResponseInterface as Response; +use Psr\Log\LoggerInterface; +use Tests\TestCase; + +class ActionTest extends TestCase +{ + public function testActionSetsHttpCodeInRespond() + { + $app = $this->getAppInstance(); + $container = $app->getContainer(); + $logger = $container->get(LoggerInterface::class); + + $testAction = new class($logger) extends Action { + public function __construct( + LoggerInterface $loggerInterface + ) { + parent::__construct($loggerInterface); + } + + public function action() :Response + { + return $this->respond( + new ActionPayload( + 202, + [ + 'willBeDoneAt' => (new DateTimeImmutable())->format(DateTimeImmutable::ATOM) + ] + ) + ); + } + }; + + $app->get('/test-action-response-code', $testAction); + $request = $this->createRequest('GET', '/test-action-response-code'); + $response = $app->handle($request); + + $this->assertEquals(202, $response->getStatusCode()); + } + + public function testActionSetsHttpCodeRespondData() + { + $app = $this->getAppInstance(); + $container = $app->getContainer(); + $logger = $container->get(LoggerInterface::class); + + $testAction = new class($logger) extends Action { + public function __construct( + LoggerInterface $loggerInterface + ) { + parent::__construct($loggerInterface); + } + + public function action() :Response + { + return $this->respondWithData( + [ + 'willBeDoneAt' => (new DateTimeImmutable())->format(DateTimeImmutable::ATOM) + ], + 202 + ); + } + }; + + $app->get('/test-action-response-code', $testAction); + $request = $this->createRequest('GET', '/test-action-response-code'); + $response = $app->handle($request); + + $this->assertEquals(202, $response->getStatusCode()); + } +} diff --git a/tests/Application/Actions/User/ListUserActionTest.php b/tests/Application/Actions/User/ListUserActionTest.php new file mode 100644 index 0000000000000000000000000000000000000000..f2d79c8a9fad944d7f2c7876604730757cf01dda --- /dev/null +++ b/tests/Application/Actions/User/ListUserActionTest.php @@ -0,0 +1,40 @@ +<?php +declare(strict_types=1); + +namespace Tests\Application\Actions\User; + +use App\Application\Actions\ActionPayload; +use App\Domain\User\UserRepository; +use App\Domain\User\User; +use DI\Container; +use Tests\TestCase; + +class ListUserActionTest extends TestCase +{ + public function testAction() + { + $app = $this->getAppInstance(); + + /** @var Container $container */ + $container = $app->getContainer(); + + $user = new User(1, 'bill.gates', 'Bill', 'Gates'); + + $userRepositoryProphecy = $this->prophesize(UserRepository::class); + $userRepositoryProphecy + ->findAll() + ->willReturn([$user]) + ->shouldBeCalledOnce(); + + $container->set(UserRepository::class, $userRepositoryProphecy->reveal()); + + $request = $this->createRequest('GET', '/users'); + $response = $app->handle($request); + + $payload = (string) $response->getBody(); + $expectedPayload = new ActionPayload(200, [$user]); + $serializedPayload = json_encode($expectedPayload, JSON_PRETTY_PRINT); + + $this->assertEquals($serializedPayload, $payload); + } +} diff --git a/tests/Application/Actions/User/ViewUserActionTest.php b/tests/Application/Actions/User/ViewUserActionTest.php new file mode 100644 index 0000000000000000000000000000000000000000..0b30cbc342e2959130c111d486c706359d5f9f68 --- /dev/null +++ b/tests/Application/Actions/User/ViewUserActionTest.php @@ -0,0 +1,79 @@ +<?php +declare(strict_types=1); + +namespace Tests\Application\Actions\User; + +use App\Application\Actions\ActionError; +use App\Application\Actions\ActionPayload; +use App\Application\Handlers\HttpErrorHandler; +use App\Domain\User\User; +use App\Domain\User\UserNotFoundException; +use App\Domain\User\UserRepository; +use DI\Container; +use Slim\Middleware\ErrorMiddleware; +use Tests\TestCase; + +class ViewUserActionTest extends TestCase +{ + public function testAction() + { + $app = $this->getAppInstance(); + + /** @var Container $container */ + $container = $app->getContainer(); + + $user = new User(1, 'bill.gates', 'Bill', 'Gates'); + + $userRepositoryProphecy = $this->prophesize(UserRepository::class); + $userRepositoryProphecy + ->findUserOfId(1) + ->willReturn($user) + ->shouldBeCalledOnce(); + + $container->set(UserRepository::class, $userRepositoryProphecy->reveal()); + + $request = $this->createRequest('GET', '/users/1'); + $response = $app->handle($request); + + $payload = (string) $response->getBody(); + $expectedPayload = new ActionPayload(200, $user); + $serializedPayload = json_encode($expectedPayload, JSON_PRETTY_PRINT); + + $this->assertEquals($serializedPayload, $payload); + } + + public function testActionThrowsUserNotFoundException() + { + $app = $this->getAppInstance(); + + $callableResolver = $app->getCallableResolver(); + $responseFactory = $app->getResponseFactory(); + + $errorHandler = new HttpErrorHandler($callableResolver, $responseFactory); + $errorMiddleware = new ErrorMiddleware($callableResolver, $responseFactory, true, false ,false); + $errorMiddleware->setDefaultErrorHandler($errorHandler); + + $app->add($errorMiddleware); + + /** @var Container $container */ + $container = $app->getContainer(); + + $userRepositoryProphecy = $this->prophesize(UserRepository::class); + $userRepositoryProphecy + ->findUserOfId(1) + ->willThrow(new UserNotFoundException()) + ->shouldBeCalledOnce(); + + $container->set(UserRepository::class, $userRepositoryProphecy->reveal()); + + $request = $this->createRequest('GET', '/users/1'); + $response = $app->handle($request); + + $payload = (string) $response->getBody(); + $expectedError = new ActionError(ActionError::RESOURCE_NOT_FOUND, 'The user you requested does not exist.'); + $expectedPayload = new ActionPayload(404, null, $expectedError); + $serializedPayload = json_encode($expectedPayload, JSON_PRETTY_PRINT); + + $this->assertEquals($serializedPayload, $payload); + } +} diff --git a/tests/Domain/User/UserTest.php b/tests/Domain/User/UserTest.php new file mode 100644 index 0000000000000000000000000000000000000000..03746b13218fa5c1c853122f4233ed758da97600 --- /dev/null +++ b/tests/Domain/User/UserTest.php @@ -0,0 +1,59 @@ +<?php +declare(strict_types=1); + +namespace Tests\Domain\User; + +use App\Domain\User\User; +use Tests\TestCase; + +class UserTest extends TestCase +{ + public function userProvider() + { + return [ + [1, 'bill.gates', 'Bill', 'Gates'], + [2, 'steve.jobs', 'Steve', 'Jobs'], + [3, 'mark.zuckerberg', 'Mark', 'Zuckerberg'], + [4, 'evan.spiegel', 'Evan', 'Spiegel'], + [5, 'jack.dorsey', 'Jack', 'Dorsey'], + ]; + } + + /** + * @dataProvider userProvider + * @param int $id + * @param string $username + * @param string $firstName + * @param string $lastName + */ + public function testGetters(int $id, string $username, string $firstName, string $lastName) + { + $user = new User($id, $username, $firstName, $lastName); + + $this->assertEquals($id, $user->getId()); + $this->assertEquals($username, $user->getUsername()); + $this->assertEquals($firstName, $user->getFirstName()); + $this->assertEquals($lastName, $user->getLastName()); + } + + /** + * @dataProvider userProvider + * @param int $id + * @param string $username + * @param string $firstName + * @param string $lastName + */ + public function testJsonSerialize(int $id, string $username, string $firstName, string $lastName) + { + $user = new User($id, $username, $firstName, $lastName); + + $expectedPayload = json_encode([ + 'id' => $id, + 'username' => $username, + 'firstName' => $firstName, + 'lastName' => $lastName, + ]); + + $this->assertEquals($expectedPayload, json_encode($user)); + } +} diff --git a/tests/Infrastructure/Persistence/User/InMemoryUserRepositoryTest.php b/tests/Infrastructure/Persistence/User/InMemoryUserRepositoryTest.php new file mode 100644 index 0000000000000000000000000000000000000000..f82af6d4d9171aa2f638aea780fd28f41dc858cf --- /dev/null +++ b/tests/Infrastructure/Persistence/User/InMemoryUserRepositoryTest.php @@ -0,0 +1,52 @@ +<?php +declare(strict_types=1); + +namespace Tests\Infrastructure\Persistence\User; + +use App\Domain\User\User; +use App\Domain\User\UserNotFoundException; +use App\Infrastructure\Persistence\User\InMemoryUserRepository; +use Tests\TestCase; + +class InMemoryUserRepositoryTest extends TestCase +{ + public function testFindAll() + { + $user = new User(1, 'bill.gates', 'Bill', 'Gates'); + + $userRepository = new InMemoryUserRepository([1 => $user]); + + $this->assertEquals([$user], $userRepository->findAll()); + } + + public function testFindAllUsersByDefault() + { + $users = [ + 1 => new User(1, 'bill.gates', 'Bill', 'Gates'), + 2 => new User(2, 'steve.jobs', 'Steve', 'Jobs'), + 3 => new User(3, 'mark.zuckerberg', 'Mark', 'Zuckerberg'), + 4 => new User(4, 'evan.spiegel', 'Evan', 'Spiegel'), + 5 => new User(5, 'jack.dorsey', 'Jack', 'Dorsey'), + ]; + + $userRepository = new InMemoryUserRepository(); + + $this->assertEquals(array_values($users), $userRepository->findAll()); + } + + public function testFindUserOfId() + { + $user = new User(1, 'bill.gates', 'Bill', 'Gates'); + + $userRepository = new InMemoryUserRepository([1 => $user]); + + $this->assertEquals($user, $userRepository->findUserOfId(1)); + } + + public function testFindUserOfIdThrowsNotFoundException() + { + $userRepository = new InMemoryUserRepository([]); + $this->expectException(UserNotFoundException::class); + $userRepository->findUserOfId(1); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000000000000000000000000000000000000..f677b3a62035679b0be01eb431c7d497e9ce33aa --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,86 @@ +<?php +declare(strict_types=1); + +namespace Tests; + +use DI\ContainerBuilder; +use Exception; +use PHPUnit\Framework\TestCase as PHPUnit_TestCase; +use Psr\Http\Message\ServerRequestInterface as Request; +use Slim\App; +use Slim\Factory\AppFactory; +use Slim\Psr7\Factory\StreamFactory; +use Slim\Psr7\Headers; +use Slim\Psr7\Request as SlimRequest; +use Slim\Psr7\Uri; + +class TestCase extends PHPUnit_TestCase +{ + /** + * @return App + * @throws Exception + */ + protected function getAppInstance(): App + { + // Instantiate PHP-DI ContainerBuilder + $containerBuilder = new ContainerBuilder(); + + // Container intentionally not compiled for tests. + + // Set up settings + $settings = require __DIR__ . '/../app/settings.php'; + $settings($containerBuilder); + + // Set up dependencies + $dependencies = require __DIR__ . '/../app/dependencies.php'; + $dependencies($containerBuilder); + + // Set up repositories + $repositories = require __DIR__ . '/../app/repositories.php'; + $repositories($containerBuilder); + + // Build PHP-DI Container instance + $container = $containerBuilder->build(); + + // Instantiate the app + AppFactory::setContainer($container); + $app = AppFactory::create(); + + // Register middleware + $middleware = require __DIR__ . '/../app/middleware.php'; + $middleware($app); + + // Register routes + $routes = require __DIR__ . '/../app/routes.php'; + $routes($app); + + return $app; + } + + /** + * @param string $method + * @param string $path + * @param array $headers + * @param array $cookies + * @param array $serverParams + * @return Request + */ + protected function createRequest( + string $method, + string $path, + array $headers = ['HTTP_ACCEPT' => 'application/json'], + array $cookies = [], + array $serverParams = [] + ): Request { + $uri = new Uri('', '', 80, $path); + $handle = fopen('php://temp', 'w+'); + $stream = (new StreamFactory())->createStreamFromResource($handle); + + $h = new Headers(); + foreach ($headers as $name => $value) { + $h->addHeader($name, $value); + } + + return new SlimRequest($method, $uri, $h, $cookies, $serverParams, $stream); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000000000000000000000000000000000000..3de1bcda3ba4e84a77895202b90550f245688df9 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,3 @@ +<?php +require __DIR__ . '/../vendor/autoload.php'; + diff --git a/var/cache/.gitignore b/var/cache/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..d6b7ef32c8478a48c3994dcadc86837f4371184d --- /dev/null +++ b/var/cache/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore