├── .env.example ├── .gitignore ├── .gitlab-ci.yml ├── .php_cs ├── Dockerfile ├── README.md ├── bin └── cli ├── composer.json ├── composer.lock ├── docker-compose.yml ├── docs └── todos.openapi.yml ├── index.php ├── infra ├── docker-compose.ci.override.yml ├── nginx │ ├── Dockerfile │ └── nginx.conf └── php │ ├── php-fpm.conf │ └── php.ini ├── phpstan.neon ├── phpunit.xml.dist ├── src ├── App.php ├── Config │ ├── DependencyInjection.php │ ├── LoggerFactory.php │ ├── PdoFactory.php │ └── RouterFactory.php ├── Http │ ├── RouteHandler.php │ └── RouteHandler │ │ ├── Home.php │ │ ├── ToDoCreate.php │ │ ├── ToDoDelete.php │ │ ├── ToDoList.php │ │ ├── ToDoRead.php │ │ └── ToDoUpdate.php ├── Model │ ├── InvalidDataException.php │ ├── ToDo.php │ └── ToDoDataMapper.php └── _functions.php └── test ├── AppIntegrationTest.php ├── AppUnitTest.php ├── Http └── RouteHandler │ ├── ToDoCreateUnitTest.php │ ├── ToDoDeleteUnitTest.php │ ├── ToDoListUnitTest.php │ ├── ToDoReadUnitTest.php │ └── ToDoUpdateUnitTest.php └── Model ├── ToDoDataMapperIntegrationTest.php └── ToDoUnitTest.php /.env.example: -------------------------------------------------------------------------------- 1 | # app configuration 2 | 3 | # enables/disables verbose stack traces on errors 4 | DEBUG=true 5 | 6 | # enables/disables the DI container cache 7 | CACHE=false 8 | 9 | # each key-value pair provides credentials of an authenticated user 10 | AUTH_USERS='{ "user": "password" }' 11 | 12 | # PostgreSQL credentials 13 | DB_USER=app 14 | DB_PASSWORD=password 15 | DB_NAME=app 16 | DB_HOST=database 17 | DB_PORT=5432 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.env 2 | /var 3 | /vendor 4 | *.cache 5 | docker-compose.override.yml 6 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - build 3 | - test 4 | - deploy 5 | 6 | image: 7 | name: docker/compose:1.24.0 8 | entrypoint: [""] 9 | 10 | services: 11 | - docker:18.09-dind 12 | 13 | variables: 14 | DOCKER_HOST: tcp://docker:2375 15 | APP_IMAGE: $CI_REGISTRY_IMAGE 16 | APP_DEV_IMAGE: ${CI_REGISTRY_IMAGE}/dev 17 | APP_BASE_IMAGE: ${CI_REGISTRY_IMAGE}/base 18 | HTTP_PROXY_IMAGE: ${CI_REGISTRY_IMAGE}/http-proxy 19 | 20 | 21 | ### BUILD 22 | 23 | build: 24 | stage: build 25 | image: docker:18.09 26 | before_script: 27 | - docker info 28 | - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY 29 | - apk add --no-cache parallel 30 | script: 31 | - "parallel docker pull ::: \ 32 | ${APP_BASE_IMAGE}:${CI_COMMIT_REF_SLUG} \ 33 | ${APP_DEV_IMAGE}:${CI_COMMIT_REF_SLUG} \ 34 | ${APP_IMAGE}:${CI_COMMIT_REF_SLUG} \ 35 | ${HTTP_PROXY_IMAGE}:${CI_COMMIT_REF_SLUG} \ 36 | || true" 37 | - docker build . -t $APP_BASE_IMAGE:$CI_COMMIT_REF_SLUG --target base 38 | --cache-from $APP_BASE_IMAGE:$CI_COMMIT_REF_SLUG 39 | - docker build . -t $APP_DEV_IMAGE:$CI_COMMIT_REF_SLUG --target dev 40 | --cache-from $APP_BASE_IMAGE:$CI_COMMIT_REF_SLUG 41 | --cache-from $APP_DEV_IMAGE:$CI_COMMIT_REF_SLUG 42 | - docker build . -t $APP_IMAGE:$CI_COMMIT_REF_SLUG 43 | --cache-from $APP_BASE_IMAGE:$CI_COMMIT_REF_SLUG 44 | --cache-from $APP_DEV_IMAGE:$CI_COMMIT_REF_SLUG 45 | --cache-from $APP_IMAGE:$CI_COMMIT_REF_SLUG 46 | - docker build infra/nginx -t $HTTP_PROXY_IMAGE:$CI_COMMIT_REF_SLUG 47 | - "parallel docker push ::: \ 48 | ${APP_IMAGE}:${CI_COMMIT_REF_SLUG} \ 49 | ${APP_DEV_IMAGE}:${CI_COMMIT_REF_SLUG} \ 50 | ${APP_BASE_IMAGE}:${CI_COMMIT_REF_SLUG} \ 51 | ${HTTP_PROXY_IMAGE}:${CI_COMMIT_REF_SLUG} \ 52 | " 53 | 54 | ### TEST 55 | 56 | .template: &test 57 | stage: test 58 | before_script: 59 | - cp infra/docker-compose.ci.override.yml docker-compose.override.yml 60 | - cp .env.example .env 61 | 62 | # we need to resort to yaml processing, because, well... yaml. 63 | - apk add --no-cache curl 64 | - curl -sSL https://github.com/mikefarah/yq/releases/download/2.2.1/yq_linux_amd64 -o /usr/local/bin/yq 65 | && chmod +x /usr/local/bin/yq 66 | - yq d -i docker-compose.yml services.app.volumes 67 | 68 | - docker info 69 | - docker login -u gitlab-ci-token -p ${CI_JOB_TOKEN} ${CI_REGISTRY} 70 | - docker-compose config 71 | 72 | test: 73 | <<: *test 74 | script: 75 | - docker-compose pull 76 | - docker-compose up -d || (docker-compose ps && docker-compose logs app && exit 1) 77 | - docker-compose exec -T app sh -c 'wait-for $DB_HOST:$DB_PORT -t 60 -- phpunit' 78 | 79 | coding style: 80 | <<: *test 81 | script: 82 | - docker-compose pull app 83 | - docker-compose run --rm --no-deps -T app php-cs-fixer fix -v --dry-run 84 | allow_failure: true 85 | 86 | static analysis: 87 | <<: *test 88 | script: 89 | - docker-compose pull app 90 | - docker-compose run --rm --no-deps -T app phpstan analyse src test 91 | -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | in([ 5 | __DIR__.'/src', 6 | __DIR__.'/test', 7 | __DIR__.'/bin', 8 | ]) 9 | ->append([ 10 | __FILE__, 11 | __DIR__.'/index.php', 12 | ]) 13 | ; 14 | 15 | return PhpCsFixer\Config::create() 16 | ->setRiskyAllowed(true) 17 | ->setFinder($finder) 18 | ->setRules([ 19 | '@PSR2' => true, 20 | '@PHP71Migration' => true, 21 | '@PHP71Migration:risky' => true, 22 | '@PHP73Migration' => true, 23 | 'heredoc_indentation' => false, 24 | 'array_syntax' => ['syntax' => 'short'], 25 | 'class_attributes_separation' => ['elements' => ['method', 'property']], 26 | 'phpdoc_summary' => false, 27 | 'yoda_style' => null, 28 | 'no_unused_imports' => true, 29 | 'phpdoc_types_order' => ['null_adjustment' => 'always_last', 'sort_algorithm' => 'none'], 30 | ]) 31 | ; 32 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG COMPOSER_FLAGS="--no-interaction --no-suggest --no-progress --ansi" 2 | 3 | ###### base stage ###### 4 | FROM php:7.4-fpm-alpine as base 5 | 6 | ARG COMPOSER_FLAGS 7 | ARG COMPOSER_VERSION="1.10.5" 8 | ARG PHP_FPM_HEALTHCHECK_VERSION="v0.5.0" 9 | ARG WAIT_FOR_IT_VERSION="c096cface5fbd9f2d6b037391dfecae6fde1362e" 10 | 11 | # global dependencies 12 | RUN apk add --no-cache bash fcgi postgresql-dev 13 | 14 | # php extensions 15 | RUN apk add --no-cache --virtual .phpize-deps $PHPIZE_DEPS \ 16 | && docker-php-ext-install -j$(getconf _NPROCESSORS_ONLN) pdo_pgsql \ 17 | && apk del .phpize-deps 18 | 19 | # local dependencies 20 | RUN curl -fsSL https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer --version=$COMPOSER_VERSION && \ 21 | curl -fsSL https://raw.githubusercontent.com/renatomefi/php-fpm-healthcheck/$PHP_FPM_HEALTHCHECK_VERSION/php-fpm-healthcheck \ 22 | -o /usr/local/bin/php-fpm-healthcheck && chmod +x /usr/local/bin/php-fpm-healthcheck && \ 23 | curl -fsSL https://raw.githubusercontent.com/vishnubob/wait-for-it/$WAIT_FOR_IT_VERSION/wait-for-it.sh \ 24 | -o /usr/local/bin/wait-for && chmod +x /usr/local/bin/wait-for 25 | 26 | # composer environment 27 | ENV COMPOSER_HOME=/opt/composer 28 | ENV COMPOSER_ALLOW_SUPERUSER=1 29 | ENV PATH=${PATH}:${COMPOSER_HOME}/vendor/bin:/app/vendor/bin:/app/bin 30 | 31 | # global composer dependencies 32 | RUN composer global require hirak/prestissimo $COMPOSER_FLAGS 33 | 34 | # custom php config 35 | COPY infra/php/php.ini /usr/local/etc/php/ 36 | COPY infra/php/php-fpm.conf /usr/local/etc/php-fpm.d/zz-custom.conf 37 | 38 | WORKDIR /app 39 | 40 | ###### dev stage ###### 41 | FROM base as dev 42 | 43 | ARG COMPOSER_FLAGS 44 | ARG PHP_CS_FIXER_VERSION="v2.16.3" 45 | ARG PHPSTAN_VERSION="0.12.19" 46 | ARG COMPOSER_REQUIRE_CHECKER_VERSION="2.1.0" 47 | ARG XDEBUG_ENABLER_VERSION="facd52cdc1a09fe7e82d6188bb575ed54ab2bc72" 48 | ARG XDEBUG_VERSION="2.9.5" 49 | 50 | # php extensions 51 | RUN apk add --no-cache --virtual .phpize-deps $PHPIZE_DEPS \ 52 | && pecl install xdebug-$XDEBUG_VERSION \ 53 | && apk del .phpize-deps 54 | 55 | # global development deps 56 | RUN apk add --no-cache postgresql-client && \ 57 | curl -fsSL https://gist.githubusercontent.com/stefanotorresi/9f48f8c476b17c44d68535630522a2be/raw/$XDEBUG_ENABLER_VERSION/xdebug \ 58 | -o /usr/local/bin/xdebug && chmod +x /usr/local/bin/xdebug 59 | 60 | # global composer dependencies 61 | RUN composer global require \ 62 | friendsofphp/php-cs-fixer:$PHP_CS_FIXER_VERSION \ 63 | phpstan/phpstan:$PHPSTAN_VERSION \ 64 | phpstan/phpstan-beberlei-assert \ 65 | phpstan/phpstan-phpunit \ 66 | maglnet/composer-require-checker:$COMPOSER_REQUIRE_CHECKER_VERSION 67 | 68 | # project composer dependencies 69 | COPY composer.* ./ 70 | RUN composer install $COMPOSER_FLAGS --no-scripts --no-autoloader 71 | 72 | # copy project sources 73 | COPY . ./ 74 | 75 | # rerun composer to trigger scripts and dump the autoloader 76 | RUN composer install $COMPOSER_FLAGS 77 | 78 | 79 | ###### production stage ###### 80 | FROM base 81 | 82 | ARG COMPOSER_FLAGS 83 | 84 | # project composer dependencies 85 | COPY composer.* ./ 86 | RUN composer install $COMPOSER_FLAGS --no-scripts --no-autoloader --no-dev 87 | 88 | # copy project sources cherry picking only production files 89 | COPY index.php ./ 90 | COPY src ./src 91 | 92 | # rerun composer to trigger scripts and dump the autoloader 93 | RUN composer install $COMPOSER_FLAGS --no-dev --optimize-autoloader 94 | 95 | HEALTHCHECK --interval=30s --timeout=2s CMD php-fpm-healthcheck 96 | 97 | RUN addgroup -S app && adduser -D -G app -S app && chown app:app . 98 | USER app 99 | ENV HOME=/home/app 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ToDo API 2 | 3 | ## Table of Contents 4 | 5 | - [Design](#design) 6 | - [Infrastructure](#infrastructure) 7 | - [First run](#first-run) 8 | - [Dev tools](#dev-tools) 9 | - [Configuration](#configuration) 10 | 11 | ## Design 12 | 13 | The project is a PHP framework-less web application based on [PSR-7](https://www.php-fig.org/psr/psr-7/), [PSR-11](https://www.php-fig.org/psr/psr-11/) and [PSR-15](https://www.php-fig.org/psr/psr-15/) standard interfaces. 14 | 15 | The main third party libraries used as building blocks are: 16 | - [`zendframweork/zend-diactoros`](https://github.com/zendframework/zend-diactoros) for the PSR-7 HTTP messages; 17 | - [`league/route`](https://github.com/thephpleague/route) for a PSR-15 HTTP router; 18 | - [`php-di/php-di`](https://github.com/PHP-DI/PHP-DI) for a PSR-11 Dependency Injection container. 19 | 20 | This allows for a very clean architecture with a straightforward lifecycle: 21 | 1. bootstrap the application 22 | 2. instantiate a request 23 | 3. the application processes the request, producing a response 24 | 4. the response is emitted 25 | 26 | The application composes a PSR-15 middleware pipeline, the main middleware being the router, which forwards the request to the appropriate HTTP handler (see [src/Http/RouteHandler](src/Http/RouteHandler)). 27 | 28 | The handlers themselves interact with the core domain model, which is a simple DBAL for the CRUD operations. 29 | 30 | The resulting overall design is much simpler than a run-of-the-mill MVC framework application. 31 | 32 | The storage backend is abstracted by a very simple DataMapper implementation built upon the native PDO extension. 33 | 34 | The API itself is specified with the OpenAPI 3.0 standard (see [todos.openapi.yml](docs/todos.openapi.yml)), and user readable docs are generated on the fly. 35 | 36 | Additional features like HTTP authentication and content negotiation are implemented via middleware. 37 | 38 | A small CLI is provided to remove some toil. 39 | 40 | ### Infrastructure 41 | 42 | The container setup has its main entry point in the reverse proxy service (NGINX). 43 | 44 | Requests are forwarded to the `index.php` file by explicitly setting FASTCGI parameters, and this allows to get entirely rid of the document root in the main application container. 45 | 46 | The main [Dockerfile](Dockerfile) produces a production-ready, multi-stage image, and the provided `docker-compose.yml` file uses the `dev` stage, which includes development tools like `phpunit`, `php-cs-fixer`, `phpstan` and others. 47 | 48 | The HTTP reverse proxy also forwards requests in the `/docs` path to a `swagger-ui` instance. 49 | 50 | HTTPS traffic is not provided, as something like this would probably live behind a TLS terminating LB. 51 | 52 | A GitLab CI configuration has been included, because I'm very used to CI and I quickly get bothered to run all the checks manually... ;) 53 | 54 | 55 | ## First run 56 | 57 | Simply spin it up with `docker-compose`, wait for the database to be ready, and and create a database schema with the provided CLI: 58 | 59 | ``` 60 | docker-compose up -d 61 | docker-compose exec app wait-for database:5432 62 | docker-compose exec app cli create-schema 63 | ``` 64 | 65 | The application will be exposed at [http://localhost](http://localhost). 66 | API docs can be reached at [http://localhost/docs](http://localhost/docs). 67 | 68 | ### Dev tools 69 | 70 | There are two suites of tests provided, plus configurations for coding style enforcement and static analysis. 71 | 72 | ``` 73 | docker-compose exec app phpunit --testsuite unit 74 | docker-compose exec app phpunit --testsuite integration 75 | docker-compose exec app phpstan analyse src test 76 | docker-compose exec app php-cs-fixer fix 77 | ``` 78 | 79 | Note: the integration tests will wipe out the database! 80 | 81 | ### Configuration 82 | 83 | Configuration is performed via the environment variables, which should be loaded into the app container with `docker-compose`. 84 | 85 | [Defaults](.env.example) are provided so that everything should work out of the box. 86 | 87 | Further customization can be done by providing a `docker-compose.override.yml` file, which is ignored by the VCS. 88 | 89 | For example: 90 | ```yaml 91 | # docker-compose.override.yml 92 | version: '3.7' 93 | services: 94 | app: 95 | environment: 96 | - DEBUG=false 97 | ``` 98 | -------------------------------------------------------------------------------- /bin/cli: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | get(ToDoDataMapper::class)->initSchema(); 20 | echo 'Schema created', PHP_EOL; 21 | break; 22 | case $command === 'drop-schema': 23 | $app->get(ToDoDataMapper::class)->dropSchema(); 24 | echo 'Schema dropped', PHP_EOL; 25 | break; 26 | case $command === 'prune-cache': 27 | system('rm -rfv ' . App::CACHE_DIR); 28 | echo 'Cache pruned', PHP_EOL; 29 | break; 30 | default: 31 | echo 32 | <<=6.0.0 <8" 34 | }, 35 | "suggest": { 36 | "ext-intl": "Needed to allow Assertion::count(), Assertion::isCountable(), Assertion::minCount(), and Assertion::maxCount() to operate on ResourceBundles" 37 | }, 38 | "type": "library", 39 | "autoload": { 40 | "psr-4": { 41 | "Assert\\": "lib/Assert" 42 | }, 43 | "files": [ 44 | "lib/Assert/functions.php" 45 | ] 46 | }, 47 | "notification-url": "https://packagist.org/downloads/", 48 | "license": [ 49 | "BSD-2-Clause" 50 | ], 51 | "authors": [ 52 | { 53 | "name": "Benjamin Eberlei", 54 | "email": "kontakt@beberlei.de", 55 | "role": "Lead Developer" 56 | }, 57 | { 58 | "name": "Richard Quadling", 59 | "email": "rquadling@gmail.com", 60 | "role": "Collaborator" 61 | } 62 | ], 63 | "description": "Thin assertion library for input validation in business models.", 64 | "keywords": [ 65 | "assert", 66 | "assertion", 67 | "validation" 68 | ], 69 | "time": "2019-12-19T17:51:41+00:00" 70 | }, 71 | { 72 | "name": "doctrine/instantiator", 73 | "version": "1.3.0", 74 | "source": { 75 | "type": "git", 76 | "url": "https://github.com/doctrine/instantiator.git", 77 | "reference": "ae466f726242e637cebdd526a7d991b9433bacf1" 78 | }, 79 | "dist": { 80 | "type": "zip", 81 | "url": "https://api.github.com/repos/doctrine/instantiator/zipball/ae466f726242e637cebdd526a7d991b9433bacf1", 82 | "reference": "ae466f726242e637cebdd526a7d991b9433bacf1", 83 | "shasum": "" 84 | }, 85 | "require": { 86 | "php": "^7.1" 87 | }, 88 | "require-dev": { 89 | "doctrine/coding-standard": "^6.0", 90 | "ext-pdo": "*", 91 | "ext-phar": "*", 92 | "phpbench/phpbench": "^0.13", 93 | "phpstan/phpstan-phpunit": "^0.11", 94 | "phpstan/phpstan-shim": "^0.11", 95 | "phpunit/phpunit": "^7.0" 96 | }, 97 | "type": "library", 98 | "extra": { 99 | "branch-alias": { 100 | "dev-master": "1.2.x-dev" 101 | } 102 | }, 103 | "autoload": { 104 | "psr-4": { 105 | "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" 106 | } 107 | }, 108 | "notification-url": "https://packagist.org/downloads/", 109 | "license": [ 110 | "MIT" 111 | ], 112 | "authors": [ 113 | { 114 | "name": "Marco Pivetta", 115 | "email": "ocramius@gmail.com", 116 | "homepage": "http://ocramius.github.com/" 117 | } 118 | ], 119 | "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", 120 | "homepage": "https://www.doctrine-project.org/projects/instantiator.html", 121 | "keywords": [ 122 | "constructor", 123 | "instantiate" 124 | ], 125 | "time": "2019-10-21T16:45:58+00:00" 126 | }, 127 | { 128 | "name": "jeremeamia/superclosure", 129 | "version": "2.4.0", 130 | "source": { 131 | "type": "git", 132 | "url": "https://github.com/jeremeamia/super_closure.git", 133 | "reference": "5707d5821b30b9a07acfb4d76949784aaa0e9ce9" 134 | }, 135 | "dist": { 136 | "type": "zip", 137 | "url": "https://api.github.com/repos/jeremeamia/super_closure/zipball/5707d5821b30b9a07acfb4d76949784aaa0e9ce9", 138 | "reference": "5707d5821b30b9a07acfb4d76949784aaa0e9ce9", 139 | "shasum": "" 140 | }, 141 | "require": { 142 | "nikic/php-parser": "^1.2|^2.0|^3.0|^4.0", 143 | "php": ">=5.4", 144 | "symfony/polyfill-php56": "^1.0" 145 | }, 146 | "require-dev": { 147 | "phpunit/phpunit": "^4.0|^5.0" 148 | }, 149 | "type": "library", 150 | "extra": { 151 | "branch-alias": { 152 | "dev-master": "2.4-dev" 153 | } 154 | }, 155 | "autoload": { 156 | "psr-4": { 157 | "SuperClosure\\": "src/" 158 | } 159 | }, 160 | "notification-url": "https://packagist.org/downloads/", 161 | "license": [ 162 | "MIT" 163 | ], 164 | "authors": [ 165 | { 166 | "name": "Jeremy Lindblom", 167 | "email": "jeremeamia@gmail.com", 168 | "homepage": "https://github.com/jeremeamia", 169 | "role": "Developer" 170 | } 171 | ], 172 | "description": "Serialize Closure objects, including their context and binding", 173 | "homepage": "https://github.com/jeremeamia/super_closure", 174 | "keywords": [ 175 | "closure", 176 | "function", 177 | "lambda", 178 | "parser", 179 | "serializable", 180 | "serialize", 181 | "tokenizer" 182 | ], 183 | "abandoned": "opis/closure", 184 | "time": "2018-03-21T22:21:57+00:00" 185 | }, 186 | { 187 | "name": "laminas/laminas-code", 188 | "version": "3.4.1", 189 | "source": { 190 | "type": "git", 191 | "url": "https://github.com/laminas/laminas-code.git", 192 | "reference": "1cb8f203389ab1482bf89c0e70a04849bacd7766" 193 | }, 194 | "dist": { 195 | "type": "zip", 196 | "url": "https://api.github.com/repos/laminas/laminas-code/zipball/1cb8f203389ab1482bf89c0e70a04849bacd7766", 197 | "reference": "1cb8f203389ab1482bf89c0e70a04849bacd7766", 198 | "shasum": "" 199 | }, 200 | "require": { 201 | "laminas/laminas-eventmanager": "^2.6 || ^3.0", 202 | "laminas/laminas-zendframework-bridge": "^1.0", 203 | "php": "^7.1" 204 | }, 205 | "conflict": { 206 | "phpspec/prophecy": "<1.9.0" 207 | }, 208 | "replace": { 209 | "zendframework/zend-code": "self.version" 210 | }, 211 | "require-dev": { 212 | "doctrine/annotations": "^1.7", 213 | "ext-phar": "*", 214 | "laminas/laminas-coding-standard": "^1.0", 215 | "laminas/laminas-stdlib": "^2.7 || ^3.0", 216 | "phpunit/phpunit": "^7.5.16 || ^8.4" 217 | }, 218 | "suggest": { 219 | "doctrine/annotations": "Doctrine\\Common\\Annotations >=1.0 for annotation features", 220 | "laminas/laminas-stdlib": "Laminas\\Stdlib component" 221 | }, 222 | "type": "library", 223 | "extra": { 224 | "branch-alias": { 225 | "dev-master": "3.4.x-dev", 226 | "dev-develop": "3.5.x-dev", 227 | "dev-dev-4.0": "4.0.x-dev" 228 | } 229 | }, 230 | "autoload": { 231 | "psr-4": { 232 | "Laminas\\Code\\": "src/" 233 | } 234 | }, 235 | "notification-url": "https://packagist.org/downloads/", 236 | "license": [ 237 | "BSD-3-Clause" 238 | ], 239 | "description": "Extensions to the PHP Reflection API, static code scanning, and code generation", 240 | "homepage": "https://laminas.dev", 241 | "keywords": [ 242 | "code", 243 | "laminas" 244 | ], 245 | "time": "2019-12-31T16:28:24+00:00" 246 | }, 247 | { 248 | "name": "laminas/laminas-diactoros", 249 | "version": "2.3.0", 250 | "source": { 251 | "type": "git", 252 | "url": "https://github.com/laminas/laminas-diactoros.git", 253 | "reference": "5ab185dba63ec655a2380c97711b09adc7061f89" 254 | }, 255 | "dist": { 256 | "type": "zip", 257 | "url": "https://api.github.com/repos/laminas/laminas-diactoros/zipball/5ab185dba63ec655a2380c97711b09adc7061f89", 258 | "reference": "5ab185dba63ec655a2380c97711b09adc7061f89", 259 | "shasum": "" 260 | }, 261 | "require": { 262 | "laminas/laminas-zendframework-bridge": "^1.0", 263 | "php": "^7.1", 264 | "psr/http-factory": "^1.0", 265 | "psr/http-message": "^1.0" 266 | }, 267 | "conflict": { 268 | "phpspec/prophecy": "<1.9.0" 269 | }, 270 | "provide": { 271 | "psr/http-factory-implementation": "1.0", 272 | "psr/http-message-implementation": "1.0" 273 | }, 274 | "replace": { 275 | "zendframework/zend-diactoros": "^2.2.1" 276 | }, 277 | "require-dev": { 278 | "ext-curl": "*", 279 | "ext-dom": "*", 280 | "ext-libxml": "*", 281 | "http-interop/http-factory-tests": "^0.5.0", 282 | "laminas/laminas-coding-standard": "~1.0.0", 283 | "php-http/psr7-integration-tests": "^1.0", 284 | "phpunit/phpunit": "^7.5.18" 285 | }, 286 | "type": "library", 287 | "extra": { 288 | "branch-alias": { 289 | "dev-master": "2.3.x-dev", 290 | "dev-develop": "2.4.x-dev" 291 | }, 292 | "laminas": { 293 | "config-provider": "Laminas\\Diactoros\\ConfigProvider", 294 | "module": "Laminas\\Diactoros" 295 | } 296 | }, 297 | "autoload": { 298 | "files": [ 299 | "src/functions/create_uploaded_file.php", 300 | "src/functions/marshal_headers_from_sapi.php", 301 | "src/functions/marshal_method_from_sapi.php", 302 | "src/functions/marshal_protocol_version_from_sapi.php", 303 | "src/functions/marshal_uri_from_sapi.php", 304 | "src/functions/normalize_server.php", 305 | "src/functions/normalize_uploaded_files.php", 306 | "src/functions/parse_cookie_header.php", 307 | "src/functions/create_uploaded_file.legacy.php", 308 | "src/functions/marshal_headers_from_sapi.legacy.php", 309 | "src/functions/marshal_method_from_sapi.legacy.php", 310 | "src/functions/marshal_protocol_version_from_sapi.legacy.php", 311 | "src/functions/marshal_uri_from_sapi.legacy.php", 312 | "src/functions/normalize_server.legacy.php", 313 | "src/functions/normalize_uploaded_files.legacy.php", 314 | "src/functions/parse_cookie_header.legacy.php" 315 | ], 316 | "psr-4": { 317 | "Laminas\\Diactoros\\": "src/" 318 | } 319 | }, 320 | "notification-url": "https://packagist.org/downloads/", 321 | "license": [ 322 | "BSD-3-Clause" 323 | ], 324 | "description": "PSR HTTP Message implementations", 325 | "homepage": "https://laminas.dev", 326 | "keywords": [ 327 | "http", 328 | "laminas", 329 | "psr", 330 | "psr-7" 331 | ], 332 | "funding": [ 333 | { 334 | "url": "https://funding.communitybridge.org/projects/laminas-project", 335 | "type": "community_bridge" 336 | } 337 | ], 338 | "time": "2020-04-27T17:07:01+00:00" 339 | }, 340 | { 341 | "name": "laminas/laminas-eventmanager", 342 | "version": "3.2.1", 343 | "source": { 344 | "type": "git", 345 | "url": "https://github.com/laminas/laminas-eventmanager.git", 346 | "reference": "ce4dc0bdf3b14b7f9815775af9dfee80a63b4748" 347 | }, 348 | "dist": { 349 | "type": "zip", 350 | "url": "https://api.github.com/repos/laminas/laminas-eventmanager/zipball/ce4dc0bdf3b14b7f9815775af9dfee80a63b4748", 351 | "reference": "ce4dc0bdf3b14b7f9815775af9dfee80a63b4748", 352 | "shasum": "" 353 | }, 354 | "require": { 355 | "laminas/laminas-zendframework-bridge": "^1.0", 356 | "php": "^5.6 || ^7.0" 357 | }, 358 | "replace": { 359 | "zendframework/zend-eventmanager": "self.version" 360 | }, 361 | "require-dev": { 362 | "athletic/athletic": "^0.1", 363 | "container-interop/container-interop": "^1.1.0", 364 | "laminas/laminas-coding-standard": "~1.0.0", 365 | "laminas/laminas-stdlib": "^2.7.3 || ^3.0", 366 | "phpunit/phpunit": "^5.7.27 || ^6.5.8 || ^7.1.2" 367 | }, 368 | "suggest": { 369 | "container-interop/container-interop": "^1.1.0, to use the lazy listeners feature", 370 | "laminas/laminas-stdlib": "^2.7.3 || ^3.0, to use the FilterChain feature" 371 | }, 372 | "type": "library", 373 | "extra": { 374 | "branch-alias": { 375 | "dev-master": "3.2-dev", 376 | "dev-develop": "3.3-dev" 377 | } 378 | }, 379 | "autoload": { 380 | "psr-4": { 381 | "Laminas\\EventManager\\": "src/" 382 | } 383 | }, 384 | "notification-url": "https://packagist.org/downloads/", 385 | "license": [ 386 | "BSD-3-Clause" 387 | ], 388 | "description": "Trigger and listen to events within a PHP application", 389 | "homepage": "https://laminas.dev", 390 | "keywords": [ 391 | "event", 392 | "eventmanager", 393 | "events", 394 | "laminas" 395 | ], 396 | "time": "2019-12-31T16:44:52+00:00" 397 | }, 398 | { 399 | "name": "laminas/laminas-httphandlerrunner", 400 | "version": "1.1.0", 401 | "source": { 402 | "type": "git", 403 | "url": "https://github.com/laminas/laminas-httphandlerrunner.git", 404 | "reference": "296f5ff35074dd981d1570a66b95596c81808087" 405 | }, 406 | "dist": { 407 | "type": "zip", 408 | "url": "https://api.github.com/repos/laminas/laminas-httphandlerrunner/zipball/296f5ff35074dd981d1570a66b95596c81808087", 409 | "reference": "296f5ff35074dd981d1570a66b95596c81808087", 410 | "shasum": "" 411 | }, 412 | "require": { 413 | "laminas/laminas-zendframework-bridge": "^1.0", 414 | "php": "^7.1", 415 | "psr/http-message": "^1.0", 416 | "psr/http-message-implementation": "^1.0", 417 | "psr/http-server-handler": "^1.0" 418 | }, 419 | "replace": { 420 | "zendframework/zend-httphandlerrunner": "self.version" 421 | }, 422 | "require-dev": { 423 | "laminas/laminas-coding-standard": "~1.0.0", 424 | "laminas/laminas-diactoros": "^1.7 || ^2.1.1", 425 | "phpunit/phpunit": "^7.0.2" 426 | }, 427 | "type": "library", 428 | "extra": { 429 | "branch-alias": { 430 | "dev-master": "1.1.x-dev", 431 | "dev-develop": "1.2.x-dev" 432 | }, 433 | "laminas": { 434 | "config-provider": "Laminas\\HttpHandlerRunner\\ConfigProvider" 435 | } 436 | }, 437 | "autoload": { 438 | "psr-4": { 439 | "Laminas\\HttpHandlerRunner\\": "src/" 440 | } 441 | }, 442 | "notification-url": "https://packagist.org/downloads/", 443 | "license": [ 444 | "BSD-3-Clause" 445 | ], 446 | "description": "Execute PSR-15 RequestHandlerInterface instances and emit responses they generate.", 447 | "homepage": "https://laminas.dev", 448 | "keywords": [ 449 | "components", 450 | "laminas", 451 | "mezzio", 452 | "psr-15", 453 | "psr-7" 454 | ], 455 | "time": "2019-12-31T17:06:16+00:00" 456 | }, 457 | { 458 | "name": "laminas/laminas-zendframework-bridge", 459 | "version": "1.0.3", 460 | "source": { 461 | "type": "git", 462 | "url": "https://github.com/laminas/laminas-zendframework-bridge.git", 463 | "reference": "bfbbdb6c998d50dbf69d2187cb78a5f1fa36e1e9" 464 | }, 465 | "dist": { 466 | "type": "zip", 467 | "url": "https://api.github.com/repos/laminas/laminas-zendframework-bridge/zipball/bfbbdb6c998d50dbf69d2187cb78a5f1fa36e1e9", 468 | "reference": "bfbbdb6c998d50dbf69d2187cb78a5f1fa36e1e9", 469 | "shasum": "" 470 | }, 471 | "require": { 472 | "php": "^5.6 || ^7.0" 473 | }, 474 | "require-dev": { 475 | "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.1", 476 | "squizlabs/php_codesniffer": "^3.5" 477 | }, 478 | "type": "library", 479 | "extra": { 480 | "branch-alias": { 481 | "dev-master": "1.0.x-dev", 482 | "dev-develop": "1.1.x-dev" 483 | }, 484 | "laminas": { 485 | "module": "Laminas\\ZendFrameworkBridge" 486 | } 487 | }, 488 | "autoload": { 489 | "files": [ 490 | "src/autoload.php" 491 | ], 492 | "psr-4": { 493 | "Laminas\\ZendFrameworkBridge\\": "src//" 494 | } 495 | }, 496 | "notification-url": "https://packagist.org/downloads/", 497 | "license": [ 498 | "BSD-3-Clause" 499 | ], 500 | "description": "Alias legacy ZF class names to Laminas Project equivalents.", 501 | "keywords": [ 502 | "ZendFramework", 503 | "autoloading", 504 | "laminas", 505 | "zf" 506 | ], 507 | "time": "2020-04-03T16:01:00+00:00" 508 | }, 509 | { 510 | "name": "lcobucci/clock", 511 | "version": "1.3.0", 512 | "source": { 513 | "type": "git", 514 | "url": "https://github.com/lcobucci/clock.git", 515 | "reference": "70f25bddfb21a9cbf8d45cc53571de8ab45f5b69" 516 | }, 517 | "dist": { 518 | "type": "zip", 519 | "url": "https://api.github.com/repos/lcobucci/clock/zipball/70f25bddfb21a9cbf8d45cc53571de8ab45f5b69", 520 | "reference": "70f25bddfb21a9cbf8d45cc53571de8ab45f5b69", 521 | "shasum": "" 522 | }, 523 | "require": { 524 | "php": "^7.4" 525 | }, 526 | "require-dev": { 527 | "infection/infection": "^0.15", 528 | "lcobucci/coding-standard": "^4.0", 529 | "phpstan/extension-installer": "^1.0", 530 | "phpstan/phpstan": "^0.12", 531 | "phpstan/phpstan-deprecation-rules": "^0.12", 532 | "phpstan/phpstan-phpunit": "^0.12", 533 | "phpstan/phpstan-strict-rules": "^0.12", 534 | "phpunit/phpunit": "^9.0" 535 | }, 536 | "type": "library", 537 | "extra": { 538 | "branch-alias": { 539 | "dev-master": "1.3-dev" 540 | } 541 | }, 542 | "autoload": { 543 | "psr-4": { 544 | "Lcobucci\\Clock\\": "src" 545 | } 546 | }, 547 | "notification-url": "https://packagist.org/downloads/", 548 | "license": [ 549 | "MIT" 550 | ], 551 | "authors": [ 552 | { 553 | "name": "Luís Cobucci", 554 | "email": "lcobucci@gmail.com" 555 | } 556 | ], 557 | "description": "Yet another clock abstraction", 558 | "time": "2020-02-26T20:20:12+00:00" 559 | }, 560 | { 561 | "name": "league/route", 562 | "version": "4.3.1", 563 | "source": { 564 | "type": "git", 565 | "url": "https://github.com/thephpleague/route.git", 566 | "reference": "e9ca722dc52d6652057e6fc448572a765b294f24" 567 | }, 568 | "dist": { 569 | "type": "zip", 570 | "url": "https://api.github.com/repos/thephpleague/route/zipball/e9ca722dc52d6652057e6fc448572a765b294f24", 571 | "reference": "e9ca722dc52d6652057e6fc448572a765b294f24", 572 | "shasum": "" 573 | }, 574 | "require": { 575 | "nikic/fast-route": "^1.0", 576 | "php": ">=7.1", 577 | "psr/container": "^1.0", 578 | "psr/http-factory": "^1.0", 579 | "psr/http-message": "^1.0", 580 | "psr/http-server-handler": "^1.0", 581 | "psr/http-server-middleware": "^1.0" 582 | }, 583 | "replace": { 584 | "orno/http": "~1.0", 585 | "orno/route": "~1.0" 586 | }, 587 | "require-dev": { 588 | "phpstan/phpstan": "^0.10.3", 589 | "phpstan/phpstan-phpunit": "^0.10.0", 590 | "phpunit/phpunit": "^7.0", 591 | "squizlabs/php_codesniffer": "^3.3" 592 | }, 593 | "type": "library", 594 | "extra": { 595 | "branch-alias": { 596 | "dev-4.x": "4.x-dev", 597 | "dev-3.x": "3.x-dev", 598 | "dev-2.x": "2.x-dev", 599 | "dev-1.x": "1.x-dev" 600 | } 601 | }, 602 | "autoload": { 603 | "psr-4": { 604 | "League\\Route\\": "src" 605 | } 606 | }, 607 | "notification-url": "https://packagist.org/downloads/", 608 | "license": [ 609 | "MIT" 610 | ], 611 | "authors": [ 612 | { 613 | "name": "Phil Bennett", 614 | "email": "philipobenito@gmail.com", 615 | "role": "Developer" 616 | } 617 | ], 618 | "description": "Fast routing and dispatch component including PSR-15 middleware, built on top of FastRoute.", 619 | "homepage": "https://github.com/thephpleague/route", 620 | "keywords": [ 621 | "dispatcher", 622 | "league", 623 | "psr-15", 624 | "psr-7", 625 | "psr15", 626 | "psr7", 627 | "route", 628 | "router" 629 | ], 630 | "time": "2019-07-01T19:36:07+00:00" 631 | }, 632 | { 633 | "name": "middlewares/http-authentication", 634 | "version": "v1.1.0", 635 | "source": { 636 | "type": "git", 637 | "url": "https://github.com/middlewares/http-authentication.git", 638 | "reference": "4f719e5205c9d0fb96bf7971379caf5fbbc6b89f" 639 | }, 640 | "dist": { 641 | "type": "zip", 642 | "url": "https://api.github.com/repos/middlewares/http-authentication/zipball/4f719e5205c9d0fb96bf7971379caf5fbbc6b89f", 643 | "reference": "4f719e5205c9d0fb96bf7971379caf5fbbc6b89f", 644 | "shasum": "" 645 | }, 646 | "require": { 647 | "middlewares/utils": "^2.1", 648 | "php": "^7.0", 649 | "psr/http-server-middleware": "^1.0" 650 | }, 651 | "require-dev": { 652 | "friendsofphp/php-cs-fixer": "^2.0", 653 | "phpunit/phpunit": "^6.0|^7.0", 654 | "squizlabs/php_codesniffer": "^3.0", 655 | "zendframework/zend-diactoros": "^1.3" 656 | }, 657 | "type": "library", 658 | "autoload": { 659 | "psr-4": { 660 | "Middlewares\\": "src/" 661 | } 662 | }, 663 | "notification-url": "https://packagist.org/downloads/", 664 | "license": [ 665 | "MIT" 666 | ], 667 | "description": "Middleware to implement Basic and Digest Http authentication", 668 | "homepage": "https://github.com/middlewares/http-authentication", 669 | "keywords": [ 670 | "Authentication", 671 | "basic", 672 | "digest", 673 | "http", 674 | "middleware", 675 | "psr-15", 676 | "psr-7", 677 | "server" 678 | ], 679 | "time": "2018-08-04T10:41:49+00:00" 680 | }, 681 | { 682 | "name": "middlewares/negotiation", 683 | "version": "v1.1.0", 684 | "source": { 685 | "type": "git", 686 | "url": "https://github.com/middlewares/negotiation.git", 687 | "reference": "4c8cee6e923834ec26905bab29788265a46134bd" 688 | }, 689 | "dist": { 690 | "type": "zip", 691 | "url": "https://api.github.com/repos/middlewares/negotiation/zipball/4c8cee6e923834ec26905bab29788265a46134bd", 692 | "reference": "4c8cee6e923834ec26905bab29788265a46134bd", 693 | "shasum": "" 694 | }, 695 | "require": { 696 | "middlewares/utils": "^2.1", 697 | "php": "^7.0", 698 | "psr/http-server-middleware": "^1.0", 699 | "willdurand/negotiation": "^2.1" 700 | }, 701 | "require-dev": { 702 | "friendsofphp/php-cs-fixer": "^2.0", 703 | "phpunit/phpunit": "^6.0|^7.0", 704 | "squizlabs/php_codesniffer": "^3.0", 705 | "zendframework/zend-diactoros": "^1.3" 706 | }, 707 | "type": "library", 708 | "autoload": { 709 | "psr-4": { 710 | "Middlewares\\": "src/" 711 | } 712 | }, 713 | "notification-url": "https://packagist.org/downloads/", 714 | "license": [ 715 | "MIT" 716 | ], 717 | "description": "Middleware to implement content negotiation", 718 | "homepage": "https://github.com/middlewares/negotiation", 719 | "keywords": [ 720 | "content", 721 | "encoding", 722 | "http", 723 | "language", 724 | "middleware", 725 | "negotiation", 726 | "psr-15", 727 | "psr-7", 728 | "server" 729 | ], 730 | "time": "2018-08-04T10:41:52+00:00" 731 | }, 732 | { 733 | "name": "middlewares/payload", 734 | "version": "v2.1.1", 735 | "source": { 736 | "type": "git", 737 | "url": "https://github.com/middlewares/payload.git", 738 | "reference": "f2e12a0567beb8beb2dfa51af5661bcd723ff52c" 739 | }, 740 | "dist": { 741 | "type": "zip", 742 | "url": "https://api.github.com/repos/middlewares/payload/zipball/f2e12a0567beb8beb2dfa51af5661bcd723ff52c", 743 | "reference": "f2e12a0567beb8beb2dfa51af5661bcd723ff52c", 744 | "shasum": "" 745 | }, 746 | "require": { 747 | "middlewares/utils": "^2.1", 748 | "php": "^7.0", 749 | "psr/http-server-middleware": "^1.0" 750 | }, 751 | "require-dev": { 752 | "friendsofphp/php-cs-fixer": "^2.0", 753 | "phpstan/phpstan": "^0.9.2|^0.10.3", 754 | "phpunit/phpunit": "^6.0|^7.0", 755 | "squizlabs/php_codesniffer": "^3.0", 756 | "zendframework/zend-diactoros": "^1.3" 757 | }, 758 | "suggest": { 759 | "middlewares/csv-payload": "Adds support for parsing CSV body of request" 760 | }, 761 | "type": "library", 762 | "autoload": { 763 | "psr-4": { 764 | "Middlewares\\": "src/" 765 | } 766 | }, 767 | "notification-url": "https://packagist.org/downloads/", 768 | "license": [ 769 | "MIT" 770 | ], 771 | "description": "Middleware to parse the body of the request with support for json, csv and url-encode", 772 | "homepage": "https://github.com/middlewares/payload", 773 | "keywords": [ 774 | "http", 775 | "json", 776 | "middleware", 777 | "payload", 778 | "psr-15", 779 | "psr-7", 780 | "server", 781 | "url-encode" 782 | ], 783 | "time": "2018-11-08T08:45:32+00:00" 784 | }, 785 | { 786 | "name": "middlewares/utils", 787 | "version": "v2.2.0", 788 | "source": { 789 | "type": "git", 790 | "url": "https://github.com/middlewares/utils.git", 791 | "reference": "7dc49454b4fbf249226023c7b77658b6068abfbc" 792 | }, 793 | "dist": { 794 | "type": "zip", 795 | "url": "https://api.github.com/repos/middlewares/utils/zipball/7dc49454b4fbf249226023c7b77658b6068abfbc", 796 | "reference": "7dc49454b4fbf249226023c7b77658b6068abfbc", 797 | "shasum": "" 798 | }, 799 | "require": { 800 | "php": "^7.0", 801 | "psr/container": "^1.0", 802 | "psr/http-factory": "^1.0", 803 | "psr/http-message": "^1.0", 804 | "psr/http-server-middleware": "^1.0" 805 | }, 806 | "require-dev": { 807 | "friendsofphp/php-cs-fixer": "^2.0", 808 | "guzzlehttp/psr7": "^1.3", 809 | "phpunit/phpunit": "^6.0|^7.0", 810 | "slim/http": "^0.3", 811 | "squizlabs/php_codesniffer": "^3.0", 812 | "zendframework/zend-diactoros": "^1.3" 813 | }, 814 | "type": "library", 815 | "autoload": { 816 | "psr-4": { 817 | "Middlewares\\Utils\\": "src/" 818 | } 819 | }, 820 | "notification-url": "https://packagist.org/downloads/", 821 | "license": [ 822 | "MIT" 823 | ], 824 | "description": "Common utils to create PSR-15 middleware packages", 825 | "homepage": "https://github.com/middlewares/utils", 826 | "keywords": [ 827 | "PSR-11", 828 | "http", 829 | "middleware", 830 | "psr-15", 831 | "psr-17", 832 | "psr-7" 833 | ], 834 | "time": "2019-03-05T22:06:37+00:00" 835 | }, 836 | { 837 | "name": "monolog/monolog", 838 | "version": "1.25.3", 839 | "source": { 840 | "type": "git", 841 | "url": "https://github.com/Seldaek/monolog.git", 842 | "reference": "fa82921994db851a8becaf3787a9e73c5976b6f1" 843 | }, 844 | "dist": { 845 | "type": "zip", 846 | "url": "https://api.github.com/repos/Seldaek/monolog/zipball/fa82921994db851a8becaf3787a9e73c5976b6f1", 847 | "reference": "fa82921994db851a8becaf3787a9e73c5976b6f1", 848 | "shasum": "" 849 | }, 850 | "require": { 851 | "php": ">=5.3.0", 852 | "psr/log": "~1.0" 853 | }, 854 | "provide": { 855 | "psr/log-implementation": "1.0.0" 856 | }, 857 | "require-dev": { 858 | "aws/aws-sdk-php": "^2.4.9 || ^3.0", 859 | "doctrine/couchdb": "~1.0@dev", 860 | "graylog2/gelf-php": "~1.0", 861 | "jakub-onderka/php-parallel-lint": "0.9", 862 | "php-amqplib/php-amqplib": "~2.4", 863 | "php-console/php-console": "^3.1.3", 864 | "phpunit/phpunit": "~4.5", 865 | "phpunit/phpunit-mock-objects": "2.3.0", 866 | "ruflin/elastica": ">=0.90 <3.0", 867 | "sentry/sentry": "^0.13", 868 | "swiftmailer/swiftmailer": "^5.3|^6.0" 869 | }, 870 | "suggest": { 871 | "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", 872 | "doctrine/couchdb": "Allow sending log messages to a CouchDB server", 873 | "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", 874 | "ext-mongo": "Allow sending log messages to a MongoDB server", 875 | "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", 876 | "mongodb/mongodb": "Allow sending log messages to a MongoDB server via PHP Driver", 877 | "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", 878 | "php-console/php-console": "Allow sending log messages to Google Chrome", 879 | "rollbar/rollbar": "Allow sending log messages to Rollbar", 880 | "ruflin/elastica": "Allow sending log messages to an Elastic Search server", 881 | "sentry/sentry": "Allow sending log messages to a Sentry server" 882 | }, 883 | "type": "library", 884 | "extra": { 885 | "branch-alias": { 886 | "dev-master": "2.0.x-dev" 887 | } 888 | }, 889 | "autoload": { 890 | "psr-4": { 891 | "Monolog\\": "src/Monolog" 892 | } 893 | }, 894 | "notification-url": "https://packagist.org/downloads/", 895 | "license": [ 896 | "MIT" 897 | ], 898 | "authors": [ 899 | { 900 | "name": "Jordi Boggiano", 901 | "email": "j.boggiano@seld.be", 902 | "homepage": "http://seld.be" 903 | } 904 | ], 905 | "description": "Sends your logs to files, sockets, inboxes, databases and various web services", 906 | "homepage": "http://github.com/Seldaek/monolog", 907 | "keywords": [ 908 | "log", 909 | "logging", 910 | "psr-3" 911 | ], 912 | "time": "2019-12-20T14:15:16+00:00" 913 | }, 914 | { 915 | "name": "nikic/fast-route", 916 | "version": "v1.3.0", 917 | "source": { 918 | "type": "git", 919 | "url": "https://github.com/nikic/FastRoute.git", 920 | "reference": "181d480e08d9476e61381e04a71b34dc0432e812" 921 | }, 922 | "dist": { 923 | "type": "zip", 924 | "url": "https://api.github.com/repos/nikic/FastRoute/zipball/181d480e08d9476e61381e04a71b34dc0432e812", 925 | "reference": "181d480e08d9476e61381e04a71b34dc0432e812", 926 | "shasum": "" 927 | }, 928 | "require": { 929 | "php": ">=5.4.0" 930 | }, 931 | "require-dev": { 932 | "phpunit/phpunit": "^4.8.35|~5.7" 933 | }, 934 | "type": "library", 935 | "autoload": { 936 | "psr-4": { 937 | "FastRoute\\": "src/" 938 | }, 939 | "files": [ 940 | "src/functions.php" 941 | ] 942 | }, 943 | "notification-url": "https://packagist.org/downloads/", 944 | "license": [ 945 | "BSD-3-Clause" 946 | ], 947 | "authors": [ 948 | { 949 | "name": "Nikita Popov", 950 | "email": "nikic@php.net" 951 | } 952 | ], 953 | "description": "Fast request router for PHP", 954 | "keywords": [ 955 | "router", 956 | "routing" 957 | ], 958 | "time": "2018-02-13T20:26:39+00:00" 959 | }, 960 | { 961 | "name": "nikic/php-parser", 962 | "version": "v4.4.0", 963 | "source": { 964 | "type": "git", 965 | "url": "https://github.com/nikic/PHP-Parser.git", 966 | "reference": "bd43ec7152eaaab3bd8c6d0aa95ceeb1df8ee120" 967 | }, 968 | "dist": { 969 | "type": "zip", 970 | "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/bd43ec7152eaaab3bd8c6d0aa95ceeb1df8ee120", 971 | "reference": "bd43ec7152eaaab3bd8c6d0aa95ceeb1df8ee120", 972 | "shasum": "" 973 | }, 974 | "require": { 975 | "ext-tokenizer": "*", 976 | "php": ">=7.0" 977 | }, 978 | "require-dev": { 979 | "ircmaxell/php-yacc": "0.0.5", 980 | "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0" 981 | }, 982 | "bin": [ 983 | "bin/php-parse" 984 | ], 985 | "type": "library", 986 | "extra": { 987 | "branch-alias": { 988 | "dev-master": "4.3-dev" 989 | } 990 | }, 991 | "autoload": { 992 | "psr-4": { 993 | "PhpParser\\": "lib/PhpParser" 994 | } 995 | }, 996 | "notification-url": "https://packagist.org/downloads/", 997 | "license": [ 998 | "BSD-3-Clause" 999 | ], 1000 | "authors": [ 1001 | { 1002 | "name": "Nikita Popov" 1003 | } 1004 | ], 1005 | "description": "A PHP parser written in PHP", 1006 | "keywords": [ 1007 | "parser", 1008 | "php" 1009 | ], 1010 | "time": "2020-04-10T16:34:50+00:00" 1011 | }, 1012 | { 1013 | "name": "ocramius/package-versions", 1014 | "version": "1.8.0", 1015 | "source": { 1016 | "type": "git", 1017 | "url": "https://github.com/Ocramius/PackageVersions.git", 1018 | "reference": "421679846270a5772534828013a93be709fb13df" 1019 | }, 1020 | "dist": { 1021 | "type": "zip", 1022 | "url": "https://api.github.com/repos/Ocramius/PackageVersions/zipball/421679846270a5772534828013a93be709fb13df", 1023 | "reference": "421679846270a5772534828013a93be709fb13df", 1024 | "shasum": "" 1025 | }, 1026 | "require": { 1027 | "composer-plugin-api": "^1.1.0 || ^2.0", 1028 | "php": "^7.4.0" 1029 | }, 1030 | "require-dev": { 1031 | "composer/composer": "^1.9.3 || ^2.0@dev", 1032 | "doctrine/coding-standard": "^7.0.2", 1033 | "ext-zip": "^1.15.0", 1034 | "infection/infection": "^0.15.3", 1035 | "phpunit/phpunit": "^9.0.1", 1036 | "vimeo/psalm": "^3.9.3" 1037 | }, 1038 | "type": "composer-plugin", 1039 | "extra": { 1040 | "class": "PackageVersions\\Installer", 1041 | "branch-alias": { 1042 | "dev-master": "1.99.x-dev" 1043 | } 1044 | }, 1045 | "autoload": { 1046 | "psr-4": { 1047 | "PackageVersions\\": "src/PackageVersions" 1048 | } 1049 | }, 1050 | "notification-url": "https://packagist.org/downloads/", 1051 | "license": [ 1052 | "MIT" 1053 | ], 1054 | "authors": [ 1055 | { 1056 | "name": "Marco Pivetta", 1057 | "email": "ocramius@gmail.com" 1058 | } 1059 | ], 1060 | "description": "Composer plugin that provides efficient querying for installed package versions (no runtime IO)", 1061 | "funding": [ 1062 | { 1063 | "url": "https://github.com/Ocramius", 1064 | "type": "github" 1065 | }, 1066 | { 1067 | "url": "https://tidelift.com/funding/github/packagist/ocramius/package-versions", 1068 | "type": "tidelift" 1069 | } 1070 | ], 1071 | "time": "2020-04-06T17:43:35+00:00" 1072 | }, 1073 | { 1074 | "name": "ocramius/proxy-manager", 1075 | "version": "2.8.0", 1076 | "source": { 1077 | "type": "git", 1078 | "url": "https://github.com/Ocramius/ProxyManager.git", 1079 | "reference": "ac1dd414fd114cfc0da9930e0ab46063c2f5e62a" 1080 | }, 1081 | "dist": { 1082 | "type": "zip", 1083 | "url": "https://api.github.com/repos/Ocramius/ProxyManager/zipball/ac1dd414fd114cfc0da9930e0ab46063c2f5e62a", 1084 | "reference": "ac1dd414fd114cfc0da9930e0ab46063c2f5e62a", 1085 | "shasum": "" 1086 | }, 1087 | "require": { 1088 | "laminas/laminas-code": "^3.4.1", 1089 | "ocramius/package-versions": "^1.8.0", 1090 | "php": "~7.4.1", 1091 | "webimpress/safe-writer": "^2.0.1" 1092 | }, 1093 | "conflict": { 1094 | "doctrine/annotations": "<1.6.1", 1095 | "laminas/laminas-stdlib": "<3.2.1", 1096 | "zendframework/zend-stdlib": "<3.2.1" 1097 | }, 1098 | "require-dev": { 1099 | "doctrine/coding-standard": "^6.0.0", 1100 | "ext-phar": "*", 1101 | "infection/infection": "^0.16.2", 1102 | "nikic/php-parser": "^4.4.0", 1103 | "phpbench/phpbench": "^0.17.0", 1104 | "phpunit/phpunit": "^9.1.1", 1105 | "slevomat/coding-standard": "^5.0.4", 1106 | "squizlabs/php_codesniffer": "^3.5.4", 1107 | "vimeo/psalm": "^3.11.1" 1108 | }, 1109 | "suggest": { 1110 | "laminas/laminas-json": "To have the JsonRpc adapter (Remote Object feature)", 1111 | "laminas/laminas-soap": "To have the Soap adapter (Remote Object feature)", 1112 | "laminas/laminas-xmlrpc": "To have the XmlRpc adapter (Remote Object feature)", 1113 | "ocramius/generated-hydrator": "To have very fast object to array to object conversion for ghost objects" 1114 | }, 1115 | "type": "library", 1116 | "extra": { 1117 | "branch-alias": { 1118 | "dev-master": "3.0.x-dev" 1119 | } 1120 | }, 1121 | "autoload": { 1122 | "psr-4": { 1123 | "ProxyManager\\": "src/ProxyManager" 1124 | } 1125 | }, 1126 | "notification-url": "https://packagist.org/downloads/", 1127 | "license": [ 1128 | "MIT" 1129 | ], 1130 | "authors": [ 1131 | { 1132 | "name": "Marco Pivetta", 1133 | "email": "ocramius@gmail.com", 1134 | "homepage": "http://ocramius.github.io/" 1135 | } 1136 | ], 1137 | "description": "A library providing utilities to generate, instantiate and generally operate with Object Proxies", 1138 | "homepage": "https://github.com/Ocramius/ProxyManager", 1139 | "keywords": [ 1140 | "aop", 1141 | "lazy loading", 1142 | "proxy", 1143 | "proxy pattern", 1144 | "service proxies" 1145 | ], 1146 | "funding": [ 1147 | { 1148 | "url": "https://github.com/Ocramius", 1149 | "type": "github" 1150 | }, 1151 | { 1152 | "url": "https://tidelift.com/funding/github/packagist/ocramius/proxy-manager", 1153 | "type": "tidelift" 1154 | } 1155 | ], 1156 | "time": "2020-04-13T14:42:16+00:00" 1157 | }, 1158 | { 1159 | "name": "oscarotero/env", 1160 | "version": "v1.2.0", 1161 | "source": { 1162 | "type": "git", 1163 | "url": "https://github.com/oscarotero/env.git", 1164 | "reference": "4ab45ce5c1f2c62549208426bfa20a3d5fa008c6" 1165 | }, 1166 | "dist": { 1167 | "type": "zip", 1168 | "url": "https://api.github.com/repos/oscarotero/env/zipball/4ab45ce5c1f2c62549208426bfa20a3d5fa008c6", 1169 | "reference": "4ab45ce5c1f2c62549208426bfa20a3d5fa008c6", 1170 | "shasum": "" 1171 | }, 1172 | "require": { 1173 | "ext-ctype": "*", 1174 | "php": ">=5.2" 1175 | }, 1176 | "type": "library", 1177 | "autoload": { 1178 | "psr-0": { 1179 | "Env": "src/" 1180 | } 1181 | }, 1182 | "notification-url": "https://packagist.org/downloads/", 1183 | "license": [ 1184 | "MIT" 1185 | ], 1186 | "authors": [ 1187 | { 1188 | "name": "Oscar Otero", 1189 | "email": "oom@oscarotero.com", 1190 | "homepage": "http://oscarotero.com", 1191 | "role": "Developer" 1192 | } 1193 | ], 1194 | "description": "Simple library to consume environment variables", 1195 | "homepage": "https://github.com/oscarotero/env", 1196 | "keywords": [ 1197 | "env" 1198 | ], 1199 | "time": "2019-04-03T18:28:43+00:00" 1200 | }, 1201 | { 1202 | "name": "paragonie/random_compat", 1203 | "version": "v9.99.99", 1204 | "source": { 1205 | "type": "git", 1206 | "url": "https://github.com/paragonie/random_compat.git", 1207 | "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95" 1208 | }, 1209 | "dist": { 1210 | "type": "zip", 1211 | "url": "https://api.github.com/repos/paragonie/random_compat/zipball/84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95", 1212 | "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95", 1213 | "shasum": "" 1214 | }, 1215 | "require": { 1216 | "php": "^7" 1217 | }, 1218 | "require-dev": { 1219 | "phpunit/phpunit": "4.*|5.*", 1220 | "vimeo/psalm": "^1" 1221 | }, 1222 | "suggest": { 1223 | "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." 1224 | }, 1225 | "type": "library", 1226 | "notification-url": "https://packagist.org/downloads/", 1227 | "license": [ 1228 | "MIT" 1229 | ], 1230 | "authors": [ 1231 | { 1232 | "name": "Paragon Initiative Enterprises", 1233 | "email": "security@paragonie.com", 1234 | "homepage": "https://paragonie.com" 1235 | } 1236 | ], 1237 | "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", 1238 | "keywords": [ 1239 | "csprng", 1240 | "polyfill", 1241 | "pseudorandom", 1242 | "random" 1243 | ], 1244 | "time": "2018-07-02T15:55:56+00:00" 1245 | }, 1246 | { 1247 | "name": "php-di/invoker", 1248 | "version": "2.0.0", 1249 | "source": { 1250 | "type": "git", 1251 | "url": "https://github.com/PHP-DI/Invoker.git", 1252 | "reference": "540c27c86f663e20fe39a24cd72fa76cdb21d41a" 1253 | }, 1254 | "dist": { 1255 | "type": "zip", 1256 | "url": "https://api.github.com/repos/PHP-DI/Invoker/zipball/540c27c86f663e20fe39a24cd72fa76cdb21d41a", 1257 | "reference": "540c27c86f663e20fe39a24cd72fa76cdb21d41a", 1258 | "shasum": "" 1259 | }, 1260 | "require": { 1261 | "psr/container": "~1.0" 1262 | }, 1263 | "require-dev": { 1264 | "athletic/athletic": "~0.1.8", 1265 | "phpunit/phpunit": "~4.5" 1266 | }, 1267 | "type": "library", 1268 | "autoload": { 1269 | "psr-4": { 1270 | "Invoker\\": "src/" 1271 | } 1272 | }, 1273 | "notification-url": "https://packagist.org/downloads/", 1274 | "license": [ 1275 | "MIT" 1276 | ], 1277 | "description": "Generic and extensible callable invoker", 1278 | "homepage": "https://github.com/PHP-DI/Invoker", 1279 | "keywords": [ 1280 | "callable", 1281 | "dependency", 1282 | "dependency-injection", 1283 | "injection", 1284 | "invoke", 1285 | "invoker" 1286 | ], 1287 | "time": "2017-03-20T19:28:22+00:00" 1288 | }, 1289 | { 1290 | "name": "php-di/php-di", 1291 | "version": "6.1.0", 1292 | "source": { 1293 | "type": "git", 1294 | "url": "https://github.com/PHP-DI/PHP-DI.git", 1295 | "reference": "69238bd49acc0eb6a967029311eeadc3f7c5d538" 1296 | }, 1297 | "dist": { 1298 | "type": "zip", 1299 | "url": "https://api.github.com/repos/PHP-DI/PHP-DI/zipball/69238bd49acc0eb6a967029311eeadc3f7c5d538", 1300 | "reference": "69238bd49acc0eb6a967029311eeadc3f7c5d538", 1301 | "shasum": "" 1302 | }, 1303 | "require": { 1304 | "jeremeamia/superclosure": "^2.0", 1305 | "nikic/php-parser": "^2.0|^3.0|^4.0", 1306 | "php": ">=7.2.0", 1307 | "php-di/invoker": "^2.0", 1308 | "php-di/phpdoc-reader": "^2.0.1", 1309 | "psr/container": "^1.0" 1310 | }, 1311 | "provide": { 1312 | "psr/container-implementation": "^1.0" 1313 | }, 1314 | "require-dev": { 1315 | "doctrine/annotations": "~1.2", 1316 | "friendsofphp/php-cs-fixer": "^2.4", 1317 | "mnapoli/phpunit-easymock": "^1.2", 1318 | "ocramius/proxy-manager": "~2.0.2", 1319 | "phpstan/phpstan": "^0.12", 1320 | "phpunit/phpunit": "^8.5" 1321 | }, 1322 | "suggest": { 1323 | "doctrine/annotations": "Install it if you want to use annotations (version ~1.2)", 1324 | "ocramius/proxy-manager": "Install it if you want to use lazy injection (version ~2.0)" 1325 | }, 1326 | "type": "library", 1327 | "autoload": { 1328 | "psr-4": { 1329 | "DI\\": "src/" 1330 | }, 1331 | "files": [ 1332 | "src/functions.php" 1333 | ] 1334 | }, 1335 | "notification-url": "https://packagist.org/downloads/", 1336 | "license": [ 1337 | "MIT" 1338 | ], 1339 | "description": "The dependency injection container for humans", 1340 | "homepage": "http://php-di.org/", 1341 | "keywords": [ 1342 | "PSR-11", 1343 | "container", 1344 | "container-interop", 1345 | "dependency injection", 1346 | "di", 1347 | "ioc", 1348 | "psr11" 1349 | ], 1350 | "time": "2020-04-06T09:54:49+00:00" 1351 | }, 1352 | { 1353 | "name": "php-di/phpdoc-reader", 1354 | "version": "2.1.1", 1355 | "source": { 1356 | "type": "git", 1357 | "url": "https://github.com/PHP-DI/PhpDocReader.git", 1358 | "reference": "15678f7451c020226807f520efb867ad26fbbfcf" 1359 | }, 1360 | "dist": { 1361 | "type": "zip", 1362 | "url": "https://api.github.com/repos/PHP-DI/PhpDocReader/zipball/15678f7451c020226807f520efb867ad26fbbfcf", 1363 | "reference": "15678f7451c020226807f520efb867ad26fbbfcf", 1364 | "shasum": "" 1365 | }, 1366 | "require": { 1367 | "php": ">=5.4.0" 1368 | }, 1369 | "require-dev": { 1370 | "phpunit/phpunit": "~4.6" 1371 | }, 1372 | "type": "library", 1373 | "autoload": { 1374 | "psr-4": { 1375 | "PhpDocReader\\": "src/PhpDocReader" 1376 | } 1377 | }, 1378 | "notification-url": "https://packagist.org/downloads/", 1379 | "license": [ 1380 | "MIT" 1381 | ], 1382 | "description": "PhpDocReader parses @var and @param values in PHP docblocks (supports namespaced class names with the same resolution rules as PHP)", 1383 | "keywords": [ 1384 | "phpdoc", 1385 | "reflection" 1386 | ], 1387 | "time": "2019-09-26T11:24:58+00:00" 1388 | }, 1389 | { 1390 | "name": "psr/container", 1391 | "version": "1.0.0", 1392 | "source": { 1393 | "type": "git", 1394 | "url": "https://github.com/php-fig/container.git", 1395 | "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f" 1396 | }, 1397 | "dist": { 1398 | "type": "zip", 1399 | "url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f", 1400 | "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f", 1401 | "shasum": "" 1402 | }, 1403 | "require": { 1404 | "php": ">=5.3.0" 1405 | }, 1406 | "type": "library", 1407 | "extra": { 1408 | "branch-alias": { 1409 | "dev-master": "1.0.x-dev" 1410 | } 1411 | }, 1412 | "autoload": { 1413 | "psr-4": { 1414 | "Psr\\Container\\": "src/" 1415 | } 1416 | }, 1417 | "notification-url": "https://packagist.org/downloads/", 1418 | "license": [ 1419 | "MIT" 1420 | ], 1421 | "authors": [ 1422 | { 1423 | "name": "PHP-FIG", 1424 | "homepage": "http://www.php-fig.org/" 1425 | } 1426 | ], 1427 | "description": "Common Container Interface (PHP FIG PSR-11)", 1428 | "homepage": "https://github.com/php-fig/container", 1429 | "keywords": [ 1430 | "PSR-11", 1431 | "container", 1432 | "container-interface", 1433 | "container-interop", 1434 | "psr" 1435 | ], 1436 | "time": "2017-02-14T16:28:37+00:00" 1437 | }, 1438 | { 1439 | "name": "psr/http-factory", 1440 | "version": "1.0.1", 1441 | "source": { 1442 | "type": "git", 1443 | "url": "https://github.com/php-fig/http-factory.git", 1444 | "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be" 1445 | }, 1446 | "dist": { 1447 | "type": "zip", 1448 | "url": "https://api.github.com/repos/php-fig/http-factory/zipball/12ac7fcd07e5b077433f5f2bee95b3a771bf61be", 1449 | "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be", 1450 | "shasum": "" 1451 | }, 1452 | "require": { 1453 | "php": ">=7.0.0", 1454 | "psr/http-message": "^1.0" 1455 | }, 1456 | "type": "library", 1457 | "extra": { 1458 | "branch-alias": { 1459 | "dev-master": "1.0.x-dev" 1460 | } 1461 | }, 1462 | "autoload": { 1463 | "psr-4": { 1464 | "Psr\\Http\\Message\\": "src/" 1465 | } 1466 | }, 1467 | "notification-url": "https://packagist.org/downloads/", 1468 | "license": [ 1469 | "MIT" 1470 | ], 1471 | "authors": [ 1472 | { 1473 | "name": "PHP-FIG", 1474 | "homepage": "http://www.php-fig.org/" 1475 | } 1476 | ], 1477 | "description": "Common interfaces for PSR-7 HTTP message factories", 1478 | "keywords": [ 1479 | "factory", 1480 | "http", 1481 | "message", 1482 | "psr", 1483 | "psr-17", 1484 | "psr-7", 1485 | "request", 1486 | "response" 1487 | ], 1488 | "time": "2019-04-30T12:38:16+00:00" 1489 | }, 1490 | { 1491 | "name": "psr/http-message", 1492 | "version": "1.0.1", 1493 | "source": { 1494 | "type": "git", 1495 | "url": "https://github.com/php-fig/http-message.git", 1496 | "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" 1497 | }, 1498 | "dist": { 1499 | "type": "zip", 1500 | "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", 1501 | "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", 1502 | "shasum": "" 1503 | }, 1504 | "require": { 1505 | "php": ">=5.3.0" 1506 | }, 1507 | "type": "library", 1508 | "extra": { 1509 | "branch-alias": { 1510 | "dev-master": "1.0.x-dev" 1511 | } 1512 | }, 1513 | "autoload": { 1514 | "psr-4": { 1515 | "Psr\\Http\\Message\\": "src/" 1516 | } 1517 | }, 1518 | "notification-url": "https://packagist.org/downloads/", 1519 | "license": [ 1520 | "MIT" 1521 | ], 1522 | "authors": [ 1523 | { 1524 | "name": "PHP-FIG", 1525 | "homepage": "http://www.php-fig.org/" 1526 | } 1527 | ], 1528 | "description": "Common interface for HTTP messages", 1529 | "homepage": "https://github.com/php-fig/http-message", 1530 | "keywords": [ 1531 | "http", 1532 | "http-message", 1533 | "psr", 1534 | "psr-7", 1535 | "request", 1536 | "response" 1537 | ], 1538 | "time": "2016-08-06T14:39:51+00:00" 1539 | }, 1540 | { 1541 | "name": "psr/http-server-handler", 1542 | "version": "1.0.1", 1543 | "source": { 1544 | "type": "git", 1545 | "url": "https://github.com/php-fig/http-server-handler.git", 1546 | "reference": "aff2f80e33b7f026ec96bb42f63242dc50ffcae7" 1547 | }, 1548 | "dist": { 1549 | "type": "zip", 1550 | "url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/aff2f80e33b7f026ec96bb42f63242dc50ffcae7", 1551 | "reference": "aff2f80e33b7f026ec96bb42f63242dc50ffcae7", 1552 | "shasum": "" 1553 | }, 1554 | "require": { 1555 | "php": ">=7.0", 1556 | "psr/http-message": "^1.0" 1557 | }, 1558 | "type": "library", 1559 | "extra": { 1560 | "branch-alias": { 1561 | "dev-master": "1.0.x-dev" 1562 | } 1563 | }, 1564 | "autoload": { 1565 | "psr-4": { 1566 | "Psr\\Http\\Server\\": "src/" 1567 | } 1568 | }, 1569 | "notification-url": "https://packagist.org/downloads/", 1570 | "license": [ 1571 | "MIT" 1572 | ], 1573 | "authors": [ 1574 | { 1575 | "name": "PHP-FIG", 1576 | "homepage": "http://www.php-fig.org/" 1577 | } 1578 | ], 1579 | "description": "Common interface for HTTP server-side request handler", 1580 | "keywords": [ 1581 | "handler", 1582 | "http", 1583 | "http-interop", 1584 | "psr", 1585 | "psr-15", 1586 | "psr-7", 1587 | "request", 1588 | "response", 1589 | "server" 1590 | ], 1591 | "time": "2018-10-30T16:46:14+00:00" 1592 | }, 1593 | { 1594 | "name": "psr/http-server-middleware", 1595 | "version": "1.0.1", 1596 | "source": { 1597 | "type": "git", 1598 | "url": "https://github.com/php-fig/http-server-middleware.git", 1599 | "reference": "2296f45510945530b9dceb8bcedb5cb84d40c5f5" 1600 | }, 1601 | "dist": { 1602 | "type": "zip", 1603 | "url": "https://api.github.com/repos/php-fig/http-server-middleware/zipball/2296f45510945530b9dceb8bcedb5cb84d40c5f5", 1604 | "reference": "2296f45510945530b9dceb8bcedb5cb84d40c5f5", 1605 | "shasum": "" 1606 | }, 1607 | "require": { 1608 | "php": ">=7.0", 1609 | "psr/http-message": "^1.0", 1610 | "psr/http-server-handler": "^1.0" 1611 | }, 1612 | "type": "library", 1613 | "extra": { 1614 | "branch-alias": { 1615 | "dev-master": "1.0.x-dev" 1616 | } 1617 | }, 1618 | "autoload": { 1619 | "psr-4": { 1620 | "Psr\\Http\\Server\\": "src/" 1621 | } 1622 | }, 1623 | "notification-url": "https://packagist.org/downloads/", 1624 | "license": [ 1625 | "MIT" 1626 | ], 1627 | "authors": [ 1628 | { 1629 | "name": "PHP-FIG", 1630 | "homepage": "http://www.php-fig.org/" 1631 | } 1632 | ], 1633 | "description": "Common interface for HTTP server-side middleware", 1634 | "keywords": [ 1635 | "http", 1636 | "http-interop", 1637 | "middleware", 1638 | "psr", 1639 | "psr-15", 1640 | "psr-7", 1641 | "request", 1642 | "response" 1643 | ], 1644 | "time": "2018-10-30T17:12:04+00:00" 1645 | }, 1646 | { 1647 | "name": "psr/log", 1648 | "version": "1.1.3", 1649 | "source": { 1650 | "type": "git", 1651 | "url": "https://github.com/php-fig/log.git", 1652 | "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc" 1653 | }, 1654 | "dist": { 1655 | "type": "zip", 1656 | "url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc", 1657 | "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc", 1658 | "shasum": "" 1659 | }, 1660 | "require": { 1661 | "php": ">=5.3.0" 1662 | }, 1663 | "type": "library", 1664 | "extra": { 1665 | "branch-alias": { 1666 | "dev-master": "1.1.x-dev" 1667 | } 1668 | }, 1669 | "autoload": { 1670 | "psr-4": { 1671 | "Psr\\Log\\": "Psr/Log/" 1672 | } 1673 | }, 1674 | "notification-url": "https://packagist.org/downloads/", 1675 | "license": [ 1676 | "MIT" 1677 | ], 1678 | "authors": [ 1679 | { 1680 | "name": "PHP-FIG", 1681 | "homepage": "http://www.php-fig.org/" 1682 | } 1683 | ], 1684 | "description": "Common interface for logging libraries", 1685 | "homepage": "https://github.com/php-fig/log", 1686 | "keywords": [ 1687 | "log", 1688 | "psr", 1689 | "psr-3" 1690 | ], 1691 | "time": "2020-03-23T09:12:05+00:00" 1692 | }, 1693 | { 1694 | "name": "ramsey/uuid", 1695 | "version": "3.9.3", 1696 | "source": { 1697 | "type": "git", 1698 | "url": "https://github.com/ramsey/uuid.git", 1699 | "reference": "7e1633a6964b48589b142d60542f9ed31bd37a92" 1700 | }, 1701 | "dist": { 1702 | "type": "zip", 1703 | "url": "https://api.github.com/repos/ramsey/uuid/zipball/7e1633a6964b48589b142d60542f9ed31bd37a92", 1704 | "reference": "7e1633a6964b48589b142d60542f9ed31bd37a92", 1705 | "shasum": "" 1706 | }, 1707 | "require": { 1708 | "ext-json": "*", 1709 | "paragonie/random_compat": "^1 | ^2 | 9.99.99", 1710 | "php": "^5.4 | ^7 | ^8", 1711 | "symfony/polyfill-ctype": "^1.8" 1712 | }, 1713 | "replace": { 1714 | "rhumsaa/uuid": "self.version" 1715 | }, 1716 | "require-dev": { 1717 | "codeception/aspect-mock": "^1 | ^2", 1718 | "doctrine/annotations": "^1.2", 1719 | "goaop/framework": "1.0.0-alpha.2 | ^1 | ^2.1", 1720 | "jakub-onderka/php-parallel-lint": "^1", 1721 | "mockery/mockery": "^0.9.11 | ^1", 1722 | "moontoast/math": "^1.1", 1723 | "paragonie/random-lib": "^2", 1724 | "php-mock/php-mock-phpunit": "^0.3 | ^1.1", 1725 | "phpunit/phpunit": "^4.8 | ^5.4 | ^6.5", 1726 | "squizlabs/php_codesniffer": "^3.5" 1727 | }, 1728 | "suggest": { 1729 | "ext-ctype": "Provides support for PHP Ctype functions", 1730 | "ext-libsodium": "Provides the PECL libsodium extension for use with the SodiumRandomGenerator", 1731 | "ext-openssl": "Provides the OpenSSL extension for use with the OpenSslGenerator", 1732 | "ext-uuid": "Provides the PECL UUID extension for use with the PeclUuidTimeGenerator and PeclUuidRandomGenerator", 1733 | "moontoast/math": "Provides support for converting UUID to 128-bit integer (in string form).", 1734 | "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", 1735 | "ramsey/uuid-console": "A console application for generating UUIDs with ramsey/uuid", 1736 | "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." 1737 | }, 1738 | "type": "library", 1739 | "extra": { 1740 | "branch-alias": { 1741 | "dev-master": "3.x-dev" 1742 | } 1743 | }, 1744 | "autoload": { 1745 | "psr-4": { 1746 | "Ramsey\\Uuid\\": "src/" 1747 | }, 1748 | "files": [ 1749 | "src/functions.php" 1750 | ] 1751 | }, 1752 | "notification-url": "https://packagist.org/downloads/", 1753 | "license": [ 1754 | "MIT" 1755 | ], 1756 | "authors": [ 1757 | { 1758 | "name": "Ben Ramsey", 1759 | "email": "ben@benramsey.com", 1760 | "homepage": "https://benramsey.com" 1761 | }, 1762 | { 1763 | "name": "Marijn Huizendveld", 1764 | "email": "marijn.huizendveld@gmail.com" 1765 | }, 1766 | { 1767 | "name": "Thibaud Fabre", 1768 | "email": "thibaud@aztech.io" 1769 | } 1770 | ], 1771 | "description": "Formerly rhumsaa/uuid. A PHP 5.4+ library for generating RFC 4122 version 1, 3, 4, and 5 universally unique identifiers (UUID).", 1772 | "homepage": "https://github.com/ramsey/uuid", 1773 | "keywords": [ 1774 | "guid", 1775 | "identifier", 1776 | "uuid" 1777 | ], 1778 | "time": "2020-02-21T04:36:14+00:00" 1779 | }, 1780 | { 1781 | "name": "symfony/polyfill-ctype", 1782 | "version": "v1.15.0", 1783 | "source": { 1784 | "type": "git", 1785 | "url": "https://github.com/symfony/polyfill-ctype.git", 1786 | "reference": "4719fa9c18b0464d399f1a63bf624b42b6fa8d14" 1787 | }, 1788 | "dist": { 1789 | "type": "zip", 1790 | "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/4719fa9c18b0464d399f1a63bf624b42b6fa8d14", 1791 | "reference": "4719fa9c18b0464d399f1a63bf624b42b6fa8d14", 1792 | "shasum": "" 1793 | }, 1794 | "require": { 1795 | "php": ">=5.3.3" 1796 | }, 1797 | "suggest": { 1798 | "ext-ctype": "For best performance" 1799 | }, 1800 | "type": "library", 1801 | "extra": { 1802 | "branch-alias": { 1803 | "dev-master": "1.15-dev" 1804 | } 1805 | }, 1806 | "autoload": { 1807 | "psr-4": { 1808 | "Symfony\\Polyfill\\Ctype\\": "" 1809 | }, 1810 | "files": [ 1811 | "bootstrap.php" 1812 | ] 1813 | }, 1814 | "notification-url": "https://packagist.org/downloads/", 1815 | "license": [ 1816 | "MIT" 1817 | ], 1818 | "authors": [ 1819 | { 1820 | "name": "Gert de Pagter", 1821 | "email": "BackEndTea@gmail.com" 1822 | }, 1823 | { 1824 | "name": "Symfony Community", 1825 | "homepage": "https://symfony.com/contributors" 1826 | } 1827 | ], 1828 | "description": "Symfony polyfill for ctype functions", 1829 | "homepage": "https://symfony.com", 1830 | "keywords": [ 1831 | "compatibility", 1832 | "ctype", 1833 | "polyfill", 1834 | "portable" 1835 | ], 1836 | "time": "2020-02-27T09:26:54+00:00" 1837 | }, 1838 | { 1839 | "name": "symfony/polyfill-php56", 1840 | "version": "v1.15.0", 1841 | "source": { 1842 | "type": "git", 1843 | "url": "https://github.com/symfony/polyfill-php56.git", 1844 | "reference": "d51ec491c8ddceae7dca8dd6c7e30428f543f37d" 1845 | }, 1846 | "dist": { 1847 | "type": "zip", 1848 | "url": "https://api.github.com/repos/symfony/polyfill-php56/zipball/d51ec491c8ddceae7dca8dd6c7e30428f543f37d", 1849 | "reference": "d51ec491c8ddceae7dca8dd6c7e30428f543f37d", 1850 | "shasum": "" 1851 | }, 1852 | "require": { 1853 | "php": ">=5.3.3", 1854 | "symfony/polyfill-util": "~1.0" 1855 | }, 1856 | "type": "library", 1857 | "extra": { 1858 | "branch-alias": { 1859 | "dev-master": "1.15-dev" 1860 | } 1861 | }, 1862 | "autoload": { 1863 | "psr-4": { 1864 | "Symfony\\Polyfill\\Php56\\": "" 1865 | }, 1866 | "files": [ 1867 | "bootstrap.php" 1868 | ] 1869 | }, 1870 | "notification-url": "https://packagist.org/downloads/", 1871 | "license": [ 1872 | "MIT" 1873 | ], 1874 | "authors": [ 1875 | { 1876 | "name": "Nicolas Grekas", 1877 | "email": "p@tchwork.com" 1878 | }, 1879 | { 1880 | "name": "Symfony Community", 1881 | "homepage": "https://symfony.com/contributors" 1882 | } 1883 | ], 1884 | "description": "Symfony polyfill backporting some PHP 5.6+ features to lower PHP versions", 1885 | "homepage": "https://symfony.com", 1886 | "keywords": [ 1887 | "compatibility", 1888 | "polyfill", 1889 | "portable", 1890 | "shim" 1891 | ], 1892 | "time": "2020-03-09T19:04:49+00:00" 1893 | }, 1894 | { 1895 | "name": "symfony/polyfill-util", 1896 | "version": "v1.15.0", 1897 | "source": { 1898 | "type": "git", 1899 | "url": "https://github.com/symfony/polyfill-util.git", 1900 | "reference": "d8e76c104127675d0ea3df3be0f2ae24a8619027" 1901 | }, 1902 | "dist": { 1903 | "type": "zip", 1904 | "url": "https://api.github.com/repos/symfony/polyfill-util/zipball/d8e76c104127675d0ea3df3be0f2ae24a8619027", 1905 | "reference": "d8e76c104127675d0ea3df3be0f2ae24a8619027", 1906 | "shasum": "" 1907 | }, 1908 | "require": { 1909 | "php": ">=5.3.3" 1910 | }, 1911 | "type": "library", 1912 | "extra": { 1913 | "branch-alias": { 1914 | "dev-master": "1.15-dev" 1915 | } 1916 | }, 1917 | "autoload": { 1918 | "psr-4": { 1919 | "Symfony\\Polyfill\\Util\\": "" 1920 | } 1921 | }, 1922 | "notification-url": "https://packagist.org/downloads/", 1923 | "license": [ 1924 | "MIT" 1925 | ], 1926 | "authors": [ 1927 | { 1928 | "name": "Nicolas Grekas", 1929 | "email": "p@tchwork.com" 1930 | }, 1931 | { 1932 | "name": "Symfony Community", 1933 | "homepage": "https://symfony.com/contributors" 1934 | } 1935 | ], 1936 | "description": "Symfony utilities for portability of PHP codes", 1937 | "homepage": "https://symfony.com", 1938 | "keywords": [ 1939 | "compat", 1940 | "compatibility", 1941 | "polyfill", 1942 | "shim" 1943 | ], 1944 | "time": "2020-03-02T11:55:35+00:00" 1945 | }, 1946 | { 1947 | "name": "webimpress/safe-writer", 1948 | "version": "2.0.1", 1949 | "source": { 1950 | "type": "git", 1951 | "url": "https://github.com/webimpress/safe-writer.git", 1952 | "reference": "d6e879960febb307c112538997316371f1e95b12" 1953 | }, 1954 | "dist": { 1955 | "type": "zip", 1956 | "url": "https://api.github.com/repos/webimpress/safe-writer/zipball/d6e879960febb307c112538997316371f1e95b12", 1957 | "reference": "d6e879960febb307c112538997316371f1e95b12", 1958 | "shasum": "" 1959 | }, 1960 | "require": { 1961 | "php": "^7.2" 1962 | }, 1963 | "require-dev": { 1964 | "phpunit/phpunit": "^8.5.2 || ^9.0.1", 1965 | "webimpress/coding-standard": "^1.1.4" 1966 | }, 1967 | "type": "library", 1968 | "extra": { 1969 | "branch-alias": { 1970 | "dev-master": "2.0.x-dev", 1971 | "dev-develop": "2.1.x-dev", 1972 | "dev-release-1.0": "1.0.x-dev" 1973 | } 1974 | }, 1975 | "autoload": { 1976 | "psr-4": { 1977 | "Webimpress\\SafeWriter\\": "src/" 1978 | } 1979 | }, 1980 | "notification-url": "https://packagist.org/downloads/", 1981 | "license": [ 1982 | "BSD-2-Clause" 1983 | ], 1984 | "description": "Tool to write files safely, to avoid race conditions", 1985 | "keywords": [ 1986 | "concurrent write", 1987 | "file writer", 1988 | "race condition", 1989 | "safe writer", 1990 | "webimpress" 1991 | ], 1992 | "funding": [ 1993 | { 1994 | "url": "https://github.com/michalbundyra", 1995 | "type": "github" 1996 | } 1997 | ], 1998 | "time": "2020-03-21T15:49:08+00:00" 1999 | }, 2000 | { 2001 | "name": "willdurand/negotiation", 2002 | "version": "v2.3.1", 2003 | "source": { 2004 | "type": "git", 2005 | "url": "https://github.com/willdurand/Negotiation.git", 2006 | "reference": "03436ededa67c6e83b9b12defac15384cb399dc9" 2007 | }, 2008 | "dist": { 2009 | "type": "zip", 2010 | "url": "https://api.github.com/repos/willdurand/Negotiation/zipball/03436ededa67c6e83b9b12defac15384cb399dc9", 2011 | "reference": "03436ededa67c6e83b9b12defac15384cb399dc9", 2012 | "shasum": "" 2013 | }, 2014 | "require": { 2015 | "php": ">=5.4.0" 2016 | }, 2017 | "require-dev": { 2018 | "phpunit/phpunit": "~4.5" 2019 | }, 2020 | "type": "library", 2021 | "extra": { 2022 | "branch-alias": { 2023 | "dev-master": "2.3-dev" 2024 | } 2025 | }, 2026 | "autoload": { 2027 | "psr-4": { 2028 | "Negotiation\\": "src/Negotiation" 2029 | } 2030 | }, 2031 | "notification-url": "https://packagist.org/downloads/", 2032 | "license": [ 2033 | "MIT" 2034 | ], 2035 | "authors": [ 2036 | { 2037 | "name": "William Durand", 2038 | "email": "will+git@drnd.me" 2039 | } 2040 | ], 2041 | "description": "Content Negotiation tools for PHP provided as a standalone library.", 2042 | "homepage": "http://williamdurand.fr/Negotiation/", 2043 | "keywords": [ 2044 | "accept", 2045 | "content", 2046 | "format", 2047 | "header", 2048 | "negotiation" 2049 | ], 2050 | "time": "2017-05-14T17:21:12+00:00" 2051 | } 2052 | ], 2053 | "packages-dev": [ 2054 | { 2055 | "name": "myclabs/deep-copy", 2056 | "version": "1.9.5", 2057 | "source": { 2058 | "type": "git", 2059 | "url": "https://github.com/myclabs/DeepCopy.git", 2060 | "reference": "b2c28789e80a97badd14145fda39b545d83ca3ef" 2061 | }, 2062 | "dist": { 2063 | "type": "zip", 2064 | "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/b2c28789e80a97badd14145fda39b545d83ca3ef", 2065 | "reference": "b2c28789e80a97badd14145fda39b545d83ca3ef", 2066 | "shasum": "" 2067 | }, 2068 | "require": { 2069 | "php": "^7.1" 2070 | }, 2071 | "replace": { 2072 | "myclabs/deep-copy": "self.version" 2073 | }, 2074 | "require-dev": { 2075 | "doctrine/collections": "^1.0", 2076 | "doctrine/common": "^2.6", 2077 | "phpunit/phpunit": "^7.1" 2078 | }, 2079 | "type": "library", 2080 | "autoload": { 2081 | "psr-4": { 2082 | "DeepCopy\\": "src/DeepCopy/" 2083 | }, 2084 | "files": [ 2085 | "src/DeepCopy/deep_copy.php" 2086 | ] 2087 | }, 2088 | "notification-url": "https://packagist.org/downloads/", 2089 | "license": [ 2090 | "MIT" 2091 | ], 2092 | "description": "Create deep copies (clones) of your objects", 2093 | "keywords": [ 2094 | "clone", 2095 | "copy", 2096 | "duplicate", 2097 | "object", 2098 | "object graph" 2099 | ], 2100 | "time": "2020-01-17T21:11:47+00:00" 2101 | }, 2102 | { 2103 | "name": "phar-io/manifest", 2104 | "version": "1.0.3", 2105 | "source": { 2106 | "type": "git", 2107 | "url": "https://github.com/phar-io/manifest.git", 2108 | "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4" 2109 | }, 2110 | "dist": { 2111 | "type": "zip", 2112 | "url": "https://api.github.com/repos/phar-io/manifest/zipball/7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", 2113 | "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", 2114 | "shasum": "" 2115 | }, 2116 | "require": { 2117 | "ext-dom": "*", 2118 | "ext-phar": "*", 2119 | "phar-io/version": "^2.0", 2120 | "php": "^5.6 || ^7.0" 2121 | }, 2122 | "type": "library", 2123 | "extra": { 2124 | "branch-alias": { 2125 | "dev-master": "1.0.x-dev" 2126 | } 2127 | }, 2128 | "autoload": { 2129 | "classmap": [ 2130 | "src/" 2131 | ] 2132 | }, 2133 | "notification-url": "https://packagist.org/downloads/", 2134 | "license": [ 2135 | "BSD-3-Clause" 2136 | ], 2137 | "authors": [ 2138 | { 2139 | "name": "Arne Blankerts", 2140 | "email": "arne@blankerts.de", 2141 | "role": "Developer" 2142 | }, 2143 | { 2144 | "name": "Sebastian Heuer", 2145 | "email": "sebastian@phpeople.de", 2146 | "role": "Developer" 2147 | }, 2148 | { 2149 | "name": "Sebastian Bergmann", 2150 | "email": "sebastian@phpunit.de", 2151 | "role": "Developer" 2152 | } 2153 | ], 2154 | "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", 2155 | "time": "2018-07-08T19:23:20+00:00" 2156 | }, 2157 | { 2158 | "name": "phar-io/version", 2159 | "version": "2.0.1", 2160 | "source": { 2161 | "type": "git", 2162 | "url": "https://github.com/phar-io/version.git", 2163 | "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6" 2164 | }, 2165 | "dist": { 2166 | "type": "zip", 2167 | "url": "https://api.github.com/repos/phar-io/version/zipball/45a2ec53a73c70ce41d55cedef9063630abaf1b6", 2168 | "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6", 2169 | "shasum": "" 2170 | }, 2171 | "require": { 2172 | "php": "^5.6 || ^7.0" 2173 | }, 2174 | "type": "library", 2175 | "autoload": { 2176 | "classmap": [ 2177 | "src/" 2178 | ] 2179 | }, 2180 | "notification-url": "https://packagist.org/downloads/", 2181 | "license": [ 2182 | "BSD-3-Clause" 2183 | ], 2184 | "authors": [ 2185 | { 2186 | "name": "Arne Blankerts", 2187 | "email": "arne@blankerts.de", 2188 | "role": "Developer" 2189 | }, 2190 | { 2191 | "name": "Sebastian Heuer", 2192 | "email": "sebastian@phpeople.de", 2193 | "role": "Developer" 2194 | }, 2195 | { 2196 | "name": "Sebastian Bergmann", 2197 | "email": "sebastian@phpunit.de", 2198 | "role": "Developer" 2199 | } 2200 | ], 2201 | "description": "Library for handling version information and constraints", 2202 | "time": "2018-07-08T19:19:57+00:00" 2203 | }, 2204 | { 2205 | "name": "phpdocumentor/reflection-common", 2206 | "version": "2.1.0", 2207 | "source": { 2208 | "type": "git", 2209 | "url": "https://github.com/phpDocumentor/ReflectionCommon.git", 2210 | "reference": "6568f4687e5b41b054365f9ae03fcb1ed5f2069b" 2211 | }, 2212 | "dist": { 2213 | "type": "zip", 2214 | "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/6568f4687e5b41b054365f9ae03fcb1ed5f2069b", 2215 | "reference": "6568f4687e5b41b054365f9ae03fcb1ed5f2069b", 2216 | "shasum": "" 2217 | }, 2218 | "require": { 2219 | "php": ">=7.1" 2220 | }, 2221 | "type": "library", 2222 | "extra": { 2223 | "branch-alias": { 2224 | "dev-master": "2.x-dev" 2225 | } 2226 | }, 2227 | "autoload": { 2228 | "psr-4": { 2229 | "phpDocumentor\\Reflection\\": "src/" 2230 | } 2231 | }, 2232 | "notification-url": "https://packagist.org/downloads/", 2233 | "license": [ 2234 | "MIT" 2235 | ], 2236 | "authors": [ 2237 | { 2238 | "name": "Jaap van Otterdijk", 2239 | "email": "opensource@ijaap.nl" 2240 | } 2241 | ], 2242 | "description": "Common reflection classes used by phpdocumentor to reflect the code structure", 2243 | "homepage": "http://www.phpdoc.org", 2244 | "keywords": [ 2245 | "FQSEN", 2246 | "phpDocumentor", 2247 | "phpdoc", 2248 | "reflection", 2249 | "static analysis" 2250 | ], 2251 | "time": "2020-04-27T09:25:28+00:00" 2252 | }, 2253 | { 2254 | "name": "phpdocumentor/reflection-docblock", 2255 | "version": "5.1.0", 2256 | "source": { 2257 | "type": "git", 2258 | "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", 2259 | "reference": "cd72d394ca794d3466a3b2fc09d5a6c1dc86b47e" 2260 | }, 2261 | "dist": { 2262 | "type": "zip", 2263 | "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/cd72d394ca794d3466a3b2fc09d5a6c1dc86b47e", 2264 | "reference": "cd72d394ca794d3466a3b2fc09d5a6c1dc86b47e", 2265 | "shasum": "" 2266 | }, 2267 | "require": { 2268 | "ext-filter": "^7.1", 2269 | "php": "^7.2", 2270 | "phpdocumentor/reflection-common": "^2.0", 2271 | "phpdocumentor/type-resolver": "^1.0", 2272 | "webmozart/assert": "^1" 2273 | }, 2274 | "require-dev": { 2275 | "doctrine/instantiator": "^1", 2276 | "mockery/mockery": "^1" 2277 | }, 2278 | "type": "library", 2279 | "extra": { 2280 | "branch-alias": { 2281 | "dev-master": "5.x-dev" 2282 | } 2283 | }, 2284 | "autoload": { 2285 | "psr-4": { 2286 | "phpDocumentor\\Reflection\\": "src" 2287 | } 2288 | }, 2289 | "notification-url": "https://packagist.org/downloads/", 2290 | "license": [ 2291 | "MIT" 2292 | ], 2293 | "authors": [ 2294 | { 2295 | "name": "Mike van Riel", 2296 | "email": "me@mikevanriel.com" 2297 | }, 2298 | { 2299 | "name": "Jaap van Otterdijk", 2300 | "email": "account@ijaap.nl" 2301 | } 2302 | ], 2303 | "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", 2304 | "time": "2020-02-22T12:28:44+00:00" 2305 | }, 2306 | { 2307 | "name": "phpdocumentor/type-resolver", 2308 | "version": "1.1.0", 2309 | "source": { 2310 | "type": "git", 2311 | "url": "https://github.com/phpDocumentor/TypeResolver.git", 2312 | "reference": "7462d5f123dfc080dfdf26897032a6513644fc95" 2313 | }, 2314 | "dist": { 2315 | "type": "zip", 2316 | "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/7462d5f123dfc080dfdf26897032a6513644fc95", 2317 | "reference": "7462d5f123dfc080dfdf26897032a6513644fc95", 2318 | "shasum": "" 2319 | }, 2320 | "require": { 2321 | "php": "^7.2", 2322 | "phpdocumentor/reflection-common": "^2.0" 2323 | }, 2324 | "require-dev": { 2325 | "ext-tokenizer": "^7.2", 2326 | "mockery/mockery": "~1" 2327 | }, 2328 | "type": "library", 2329 | "extra": { 2330 | "branch-alias": { 2331 | "dev-master": "1.x-dev" 2332 | } 2333 | }, 2334 | "autoload": { 2335 | "psr-4": { 2336 | "phpDocumentor\\Reflection\\": "src" 2337 | } 2338 | }, 2339 | "notification-url": "https://packagist.org/downloads/", 2340 | "license": [ 2341 | "MIT" 2342 | ], 2343 | "authors": [ 2344 | { 2345 | "name": "Mike van Riel", 2346 | "email": "me@mikevanriel.com" 2347 | } 2348 | ], 2349 | "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", 2350 | "time": "2020-02-18T18:59:58+00:00" 2351 | }, 2352 | { 2353 | "name": "phpspec/prophecy", 2354 | "version": "v1.10.3", 2355 | "source": { 2356 | "type": "git", 2357 | "url": "https://github.com/phpspec/prophecy.git", 2358 | "reference": "451c3cd1418cf640de218914901e51b064abb093" 2359 | }, 2360 | "dist": { 2361 | "type": "zip", 2362 | "url": "https://api.github.com/repos/phpspec/prophecy/zipball/451c3cd1418cf640de218914901e51b064abb093", 2363 | "reference": "451c3cd1418cf640de218914901e51b064abb093", 2364 | "shasum": "" 2365 | }, 2366 | "require": { 2367 | "doctrine/instantiator": "^1.0.2", 2368 | "php": "^5.3|^7.0", 2369 | "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0|^5.0", 2370 | "sebastian/comparator": "^1.2.3|^2.0|^3.0|^4.0", 2371 | "sebastian/recursion-context": "^1.0|^2.0|^3.0|^4.0" 2372 | }, 2373 | "require-dev": { 2374 | "phpspec/phpspec": "^2.5 || ^3.2", 2375 | "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1" 2376 | }, 2377 | "type": "library", 2378 | "extra": { 2379 | "branch-alias": { 2380 | "dev-master": "1.10.x-dev" 2381 | } 2382 | }, 2383 | "autoload": { 2384 | "psr-4": { 2385 | "Prophecy\\": "src/Prophecy" 2386 | } 2387 | }, 2388 | "notification-url": "https://packagist.org/downloads/", 2389 | "license": [ 2390 | "MIT" 2391 | ], 2392 | "authors": [ 2393 | { 2394 | "name": "Konstantin Kudryashov", 2395 | "email": "ever.zet@gmail.com", 2396 | "homepage": "http://everzet.com" 2397 | }, 2398 | { 2399 | "name": "Marcello Duarte", 2400 | "email": "marcello.duarte@gmail.com" 2401 | } 2402 | ], 2403 | "description": "Highly opinionated mocking framework for PHP 5.3+", 2404 | "homepage": "https://github.com/phpspec/prophecy", 2405 | "keywords": [ 2406 | "Double", 2407 | "Dummy", 2408 | "fake", 2409 | "mock", 2410 | "spy", 2411 | "stub" 2412 | ], 2413 | "time": "2020-03-05T15:02:03+00:00" 2414 | }, 2415 | { 2416 | "name": "phpunit/php-code-coverage", 2417 | "version": "7.0.10", 2418 | "source": { 2419 | "type": "git", 2420 | "url": "https://github.com/sebastianbergmann/php-code-coverage.git", 2421 | "reference": "f1884187926fbb755a9aaf0b3836ad3165b478bf" 2422 | }, 2423 | "dist": { 2424 | "type": "zip", 2425 | "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/f1884187926fbb755a9aaf0b3836ad3165b478bf", 2426 | "reference": "f1884187926fbb755a9aaf0b3836ad3165b478bf", 2427 | "shasum": "" 2428 | }, 2429 | "require": { 2430 | "ext-dom": "*", 2431 | "ext-xmlwriter": "*", 2432 | "php": "^7.2", 2433 | "phpunit/php-file-iterator": "^2.0.2", 2434 | "phpunit/php-text-template": "^1.2.1", 2435 | "phpunit/php-token-stream": "^3.1.1", 2436 | "sebastian/code-unit-reverse-lookup": "^1.0.1", 2437 | "sebastian/environment": "^4.2.2", 2438 | "sebastian/version": "^2.0.1", 2439 | "theseer/tokenizer": "^1.1.3" 2440 | }, 2441 | "require-dev": { 2442 | "phpunit/phpunit": "^8.2.2" 2443 | }, 2444 | "suggest": { 2445 | "ext-xdebug": "^2.7.2" 2446 | }, 2447 | "type": "library", 2448 | "extra": { 2449 | "branch-alias": { 2450 | "dev-master": "7.0-dev" 2451 | } 2452 | }, 2453 | "autoload": { 2454 | "classmap": [ 2455 | "src/" 2456 | ] 2457 | }, 2458 | "notification-url": "https://packagist.org/downloads/", 2459 | "license": [ 2460 | "BSD-3-Clause" 2461 | ], 2462 | "authors": [ 2463 | { 2464 | "name": "Sebastian Bergmann", 2465 | "email": "sebastian@phpunit.de", 2466 | "role": "lead" 2467 | } 2468 | ], 2469 | "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", 2470 | "homepage": "https://github.com/sebastianbergmann/php-code-coverage", 2471 | "keywords": [ 2472 | "coverage", 2473 | "testing", 2474 | "xunit" 2475 | ], 2476 | "time": "2019-11-20T13:55:58+00:00" 2477 | }, 2478 | { 2479 | "name": "phpunit/php-file-iterator", 2480 | "version": "2.0.2", 2481 | "source": { 2482 | "type": "git", 2483 | "url": "https://github.com/sebastianbergmann/php-file-iterator.git", 2484 | "reference": "050bedf145a257b1ff02746c31894800e5122946" 2485 | }, 2486 | "dist": { 2487 | "type": "zip", 2488 | "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/050bedf145a257b1ff02746c31894800e5122946", 2489 | "reference": "050bedf145a257b1ff02746c31894800e5122946", 2490 | "shasum": "" 2491 | }, 2492 | "require": { 2493 | "php": "^7.1" 2494 | }, 2495 | "require-dev": { 2496 | "phpunit/phpunit": "^7.1" 2497 | }, 2498 | "type": "library", 2499 | "extra": { 2500 | "branch-alias": { 2501 | "dev-master": "2.0.x-dev" 2502 | } 2503 | }, 2504 | "autoload": { 2505 | "classmap": [ 2506 | "src/" 2507 | ] 2508 | }, 2509 | "notification-url": "https://packagist.org/downloads/", 2510 | "license": [ 2511 | "BSD-3-Clause" 2512 | ], 2513 | "authors": [ 2514 | { 2515 | "name": "Sebastian Bergmann", 2516 | "email": "sebastian@phpunit.de", 2517 | "role": "lead" 2518 | } 2519 | ], 2520 | "description": "FilterIterator implementation that filters files based on a list of suffixes.", 2521 | "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", 2522 | "keywords": [ 2523 | "filesystem", 2524 | "iterator" 2525 | ], 2526 | "time": "2018-09-13T20:33:42+00:00" 2527 | }, 2528 | { 2529 | "name": "phpunit/php-text-template", 2530 | "version": "1.2.1", 2531 | "source": { 2532 | "type": "git", 2533 | "url": "https://github.com/sebastianbergmann/php-text-template.git", 2534 | "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" 2535 | }, 2536 | "dist": { 2537 | "type": "zip", 2538 | "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", 2539 | "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", 2540 | "shasum": "" 2541 | }, 2542 | "require": { 2543 | "php": ">=5.3.3" 2544 | }, 2545 | "type": "library", 2546 | "autoload": { 2547 | "classmap": [ 2548 | "src/" 2549 | ] 2550 | }, 2551 | "notification-url": "https://packagist.org/downloads/", 2552 | "license": [ 2553 | "BSD-3-Clause" 2554 | ], 2555 | "authors": [ 2556 | { 2557 | "name": "Sebastian Bergmann", 2558 | "email": "sebastian@phpunit.de", 2559 | "role": "lead" 2560 | } 2561 | ], 2562 | "description": "Simple template engine.", 2563 | "homepage": "https://github.com/sebastianbergmann/php-text-template/", 2564 | "keywords": [ 2565 | "template" 2566 | ], 2567 | "time": "2015-06-21T13:50:34+00:00" 2568 | }, 2569 | { 2570 | "name": "phpunit/php-timer", 2571 | "version": "2.1.2", 2572 | "source": { 2573 | "type": "git", 2574 | "url": "https://github.com/sebastianbergmann/php-timer.git", 2575 | "reference": "1038454804406b0b5f5f520358e78c1c2f71501e" 2576 | }, 2577 | "dist": { 2578 | "type": "zip", 2579 | "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/1038454804406b0b5f5f520358e78c1c2f71501e", 2580 | "reference": "1038454804406b0b5f5f520358e78c1c2f71501e", 2581 | "shasum": "" 2582 | }, 2583 | "require": { 2584 | "php": "^7.1" 2585 | }, 2586 | "require-dev": { 2587 | "phpunit/phpunit": "^7.0" 2588 | }, 2589 | "type": "library", 2590 | "extra": { 2591 | "branch-alias": { 2592 | "dev-master": "2.1-dev" 2593 | } 2594 | }, 2595 | "autoload": { 2596 | "classmap": [ 2597 | "src/" 2598 | ] 2599 | }, 2600 | "notification-url": "https://packagist.org/downloads/", 2601 | "license": [ 2602 | "BSD-3-Clause" 2603 | ], 2604 | "authors": [ 2605 | { 2606 | "name": "Sebastian Bergmann", 2607 | "email": "sebastian@phpunit.de", 2608 | "role": "lead" 2609 | } 2610 | ], 2611 | "description": "Utility class for timing", 2612 | "homepage": "https://github.com/sebastianbergmann/php-timer/", 2613 | "keywords": [ 2614 | "timer" 2615 | ], 2616 | "time": "2019-06-07T04:22:29+00:00" 2617 | }, 2618 | { 2619 | "name": "phpunit/php-token-stream", 2620 | "version": "3.1.1", 2621 | "source": { 2622 | "type": "git", 2623 | "url": "https://github.com/sebastianbergmann/php-token-stream.git", 2624 | "reference": "995192df77f63a59e47f025390d2d1fdf8f425ff" 2625 | }, 2626 | "dist": { 2627 | "type": "zip", 2628 | "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/995192df77f63a59e47f025390d2d1fdf8f425ff", 2629 | "reference": "995192df77f63a59e47f025390d2d1fdf8f425ff", 2630 | "shasum": "" 2631 | }, 2632 | "require": { 2633 | "ext-tokenizer": "*", 2634 | "php": "^7.1" 2635 | }, 2636 | "require-dev": { 2637 | "phpunit/phpunit": "^7.0" 2638 | }, 2639 | "type": "library", 2640 | "extra": { 2641 | "branch-alias": { 2642 | "dev-master": "3.1-dev" 2643 | } 2644 | }, 2645 | "autoload": { 2646 | "classmap": [ 2647 | "src/" 2648 | ] 2649 | }, 2650 | "notification-url": "https://packagist.org/downloads/", 2651 | "license": [ 2652 | "BSD-3-Clause" 2653 | ], 2654 | "authors": [ 2655 | { 2656 | "name": "Sebastian Bergmann", 2657 | "email": "sebastian@phpunit.de" 2658 | } 2659 | ], 2660 | "description": "Wrapper around PHP's tokenizer extension.", 2661 | "homepage": "https://github.com/sebastianbergmann/php-token-stream/", 2662 | "keywords": [ 2663 | "tokenizer" 2664 | ], 2665 | "time": "2019-09-17T06:23:10+00:00" 2666 | }, 2667 | { 2668 | "name": "phpunit/phpunit", 2669 | "version": "8.5.4", 2670 | "source": { 2671 | "type": "git", 2672 | "url": "https://github.com/sebastianbergmann/phpunit.git", 2673 | "reference": "8474e22d7d642f665084ba5ec780626cbd1efd23" 2674 | }, 2675 | "dist": { 2676 | "type": "zip", 2677 | "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/8474e22d7d642f665084ba5ec780626cbd1efd23", 2678 | "reference": "8474e22d7d642f665084ba5ec780626cbd1efd23", 2679 | "shasum": "" 2680 | }, 2681 | "require": { 2682 | "doctrine/instantiator": "^1.2.0", 2683 | "ext-dom": "*", 2684 | "ext-json": "*", 2685 | "ext-libxml": "*", 2686 | "ext-mbstring": "*", 2687 | "ext-xml": "*", 2688 | "ext-xmlwriter": "*", 2689 | "myclabs/deep-copy": "^1.9.1", 2690 | "phar-io/manifest": "^1.0.3", 2691 | "phar-io/version": "^2.0.1", 2692 | "php": "^7.2", 2693 | "phpspec/prophecy": "^1.8.1", 2694 | "phpunit/php-code-coverage": "^7.0.7", 2695 | "phpunit/php-file-iterator": "^2.0.2", 2696 | "phpunit/php-text-template": "^1.2.1", 2697 | "phpunit/php-timer": "^2.1.2", 2698 | "sebastian/comparator": "^3.0.2", 2699 | "sebastian/diff": "^3.0.2", 2700 | "sebastian/environment": "^4.2.2", 2701 | "sebastian/exporter": "^3.1.1", 2702 | "sebastian/global-state": "^3.0.0", 2703 | "sebastian/object-enumerator": "^3.0.3", 2704 | "sebastian/resource-operations": "^2.0.1", 2705 | "sebastian/type": "^1.1.3", 2706 | "sebastian/version": "^2.0.1" 2707 | }, 2708 | "require-dev": { 2709 | "ext-pdo": "*" 2710 | }, 2711 | "suggest": { 2712 | "ext-soap": "*", 2713 | "ext-xdebug": "*", 2714 | "phpunit/php-invoker": "^2.0.0" 2715 | }, 2716 | "bin": [ 2717 | "phpunit" 2718 | ], 2719 | "type": "library", 2720 | "extra": { 2721 | "branch-alias": { 2722 | "dev-master": "8.5-dev" 2723 | } 2724 | }, 2725 | "autoload": { 2726 | "classmap": [ 2727 | "src/" 2728 | ] 2729 | }, 2730 | "notification-url": "https://packagist.org/downloads/", 2731 | "license": [ 2732 | "BSD-3-Clause" 2733 | ], 2734 | "authors": [ 2735 | { 2736 | "name": "Sebastian Bergmann", 2737 | "email": "sebastian@phpunit.de", 2738 | "role": "lead" 2739 | } 2740 | ], 2741 | "description": "The PHP Unit Testing framework.", 2742 | "homepage": "https://phpunit.de/", 2743 | "keywords": [ 2744 | "phpunit", 2745 | "testing", 2746 | "xunit" 2747 | ], 2748 | "time": "2020-04-23T04:39:42+00:00" 2749 | }, 2750 | { 2751 | "name": "sebastian/code-unit-reverse-lookup", 2752 | "version": "1.0.1", 2753 | "source": { 2754 | "type": "git", 2755 | "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", 2756 | "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18" 2757 | }, 2758 | "dist": { 2759 | "type": "zip", 2760 | "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", 2761 | "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", 2762 | "shasum": "" 2763 | }, 2764 | "require": { 2765 | "php": "^5.6 || ^7.0" 2766 | }, 2767 | "require-dev": { 2768 | "phpunit/phpunit": "^5.7 || ^6.0" 2769 | }, 2770 | "type": "library", 2771 | "extra": { 2772 | "branch-alias": { 2773 | "dev-master": "1.0.x-dev" 2774 | } 2775 | }, 2776 | "autoload": { 2777 | "classmap": [ 2778 | "src/" 2779 | ] 2780 | }, 2781 | "notification-url": "https://packagist.org/downloads/", 2782 | "license": [ 2783 | "BSD-3-Clause" 2784 | ], 2785 | "authors": [ 2786 | { 2787 | "name": "Sebastian Bergmann", 2788 | "email": "sebastian@phpunit.de" 2789 | } 2790 | ], 2791 | "description": "Looks up which function or method a line of code belongs to", 2792 | "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", 2793 | "time": "2017-03-04T06:30:41+00:00" 2794 | }, 2795 | { 2796 | "name": "sebastian/comparator", 2797 | "version": "3.0.2", 2798 | "source": { 2799 | "type": "git", 2800 | "url": "https://github.com/sebastianbergmann/comparator.git", 2801 | "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da" 2802 | }, 2803 | "dist": { 2804 | "type": "zip", 2805 | "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/5de4fc177adf9bce8df98d8d141a7559d7ccf6da", 2806 | "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da", 2807 | "shasum": "" 2808 | }, 2809 | "require": { 2810 | "php": "^7.1", 2811 | "sebastian/diff": "^3.0", 2812 | "sebastian/exporter": "^3.1" 2813 | }, 2814 | "require-dev": { 2815 | "phpunit/phpunit": "^7.1" 2816 | }, 2817 | "type": "library", 2818 | "extra": { 2819 | "branch-alias": { 2820 | "dev-master": "3.0-dev" 2821 | } 2822 | }, 2823 | "autoload": { 2824 | "classmap": [ 2825 | "src/" 2826 | ] 2827 | }, 2828 | "notification-url": "https://packagist.org/downloads/", 2829 | "license": [ 2830 | "BSD-3-Clause" 2831 | ], 2832 | "authors": [ 2833 | { 2834 | "name": "Jeff Welch", 2835 | "email": "whatthejeff@gmail.com" 2836 | }, 2837 | { 2838 | "name": "Volker Dusch", 2839 | "email": "github@wallbash.com" 2840 | }, 2841 | { 2842 | "name": "Bernhard Schussek", 2843 | "email": "bschussek@2bepublished.at" 2844 | }, 2845 | { 2846 | "name": "Sebastian Bergmann", 2847 | "email": "sebastian@phpunit.de" 2848 | } 2849 | ], 2850 | "description": "Provides the functionality to compare PHP values for equality", 2851 | "homepage": "https://github.com/sebastianbergmann/comparator", 2852 | "keywords": [ 2853 | "comparator", 2854 | "compare", 2855 | "equality" 2856 | ], 2857 | "time": "2018-07-12T15:12:46+00:00" 2858 | }, 2859 | { 2860 | "name": "sebastian/diff", 2861 | "version": "3.0.2", 2862 | "source": { 2863 | "type": "git", 2864 | "url": "https://github.com/sebastianbergmann/diff.git", 2865 | "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29" 2866 | }, 2867 | "dist": { 2868 | "type": "zip", 2869 | "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/720fcc7e9b5cf384ea68d9d930d480907a0c1a29", 2870 | "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29", 2871 | "shasum": "" 2872 | }, 2873 | "require": { 2874 | "php": "^7.1" 2875 | }, 2876 | "require-dev": { 2877 | "phpunit/phpunit": "^7.5 || ^8.0", 2878 | "symfony/process": "^2 || ^3.3 || ^4" 2879 | }, 2880 | "type": "library", 2881 | "extra": { 2882 | "branch-alias": { 2883 | "dev-master": "3.0-dev" 2884 | } 2885 | }, 2886 | "autoload": { 2887 | "classmap": [ 2888 | "src/" 2889 | ] 2890 | }, 2891 | "notification-url": "https://packagist.org/downloads/", 2892 | "license": [ 2893 | "BSD-3-Clause" 2894 | ], 2895 | "authors": [ 2896 | { 2897 | "name": "Kore Nordmann", 2898 | "email": "mail@kore-nordmann.de" 2899 | }, 2900 | { 2901 | "name": "Sebastian Bergmann", 2902 | "email": "sebastian@phpunit.de" 2903 | } 2904 | ], 2905 | "description": "Diff implementation", 2906 | "homepage": "https://github.com/sebastianbergmann/diff", 2907 | "keywords": [ 2908 | "diff", 2909 | "udiff", 2910 | "unidiff", 2911 | "unified diff" 2912 | ], 2913 | "time": "2019-02-04T06:01:07+00:00" 2914 | }, 2915 | { 2916 | "name": "sebastian/environment", 2917 | "version": "4.2.3", 2918 | "source": { 2919 | "type": "git", 2920 | "url": "https://github.com/sebastianbergmann/environment.git", 2921 | "reference": "464c90d7bdf5ad4e8a6aea15c091fec0603d4368" 2922 | }, 2923 | "dist": { 2924 | "type": "zip", 2925 | "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/464c90d7bdf5ad4e8a6aea15c091fec0603d4368", 2926 | "reference": "464c90d7bdf5ad4e8a6aea15c091fec0603d4368", 2927 | "shasum": "" 2928 | }, 2929 | "require": { 2930 | "php": "^7.1" 2931 | }, 2932 | "require-dev": { 2933 | "phpunit/phpunit": "^7.5" 2934 | }, 2935 | "suggest": { 2936 | "ext-posix": "*" 2937 | }, 2938 | "type": "library", 2939 | "extra": { 2940 | "branch-alias": { 2941 | "dev-master": "4.2-dev" 2942 | } 2943 | }, 2944 | "autoload": { 2945 | "classmap": [ 2946 | "src/" 2947 | ] 2948 | }, 2949 | "notification-url": "https://packagist.org/downloads/", 2950 | "license": [ 2951 | "BSD-3-Clause" 2952 | ], 2953 | "authors": [ 2954 | { 2955 | "name": "Sebastian Bergmann", 2956 | "email": "sebastian@phpunit.de" 2957 | } 2958 | ], 2959 | "description": "Provides functionality to handle HHVM/PHP environments", 2960 | "homepage": "http://www.github.com/sebastianbergmann/environment", 2961 | "keywords": [ 2962 | "Xdebug", 2963 | "environment", 2964 | "hhvm" 2965 | ], 2966 | "time": "2019-11-20T08:46:58+00:00" 2967 | }, 2968 | { 2969 | "name": "sebastian/exporter", 2970 | "version": "3.1.2", 2971 | "source": { 2972 | "type": "git", 2973 | "url": "https://github.com/sebastianbergmann/exporter.git", 2974 | "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e" 2975 | }, 2976 | "dist": { 2977 | "type": "zip", 2978 | "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/68609e1261d215ea5b21b7987539cbfbe156ec3e", 2979 | "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e", 2980 | "shasum": "" 2981 | }, 2982 | "require": { 2983 | "php": "^7.0", 2984 | "sebastian/recursion-context": "^3.0" 2985 | }, 2986 | "require-dev": { 2987 | "ext-mbstring": "*", 2988 | "phpunit/phpunit": "^6.0" 2989 | }, 2990 | "type": "library", 2991 | "extra": { 2992 | "branch-alias": { 2993 | "dev-master": "3.1.x-dev" 2994 | } 2995 | }, 2996 | "autoload": { 2997 | "classmap": [ 2998 | "src/" 2999 | ] 3000 | }, 3001 | "notification-url": "https://packagist.org/downloads/", 3002 | "license": [ 3003 | "BSD-3-Clause" 3004 | ], 3005 | "authors": [ 3006 | { 3007 | "name": "Sebastian Bergmann", 3008 | "email": "sebastian@phpunit.de" 3009 | }, 3010 | { 3011 | "name": "Jeff Welch", 3012 | "email": "whatthejeff@gmail.com" 3013 | }, 3014 | { 3015 | "name": "Volker Dusch", 3016 | "email": "github@wallbash.com" 3017 | }, 3018 | { 3019 | "name": "Adam Harvey", 3020 | "email": "aharvey@php.net" 3021 | }, 3022 | { 3023 | "name": "Bernhard Schussek", 3024 | "email": "bschussek@gmail.com" 3025 | } 3026 | ], 3027 | "description": "Provides the functionality to export PHP variables for visualization", 3028 | "homepage": "http://www.github.com/sebastianbergmann/exporter", 3029 | "keywords": [ 3030 | "export", 3031 | "exporter" 3032 | ], 3033 | "time": "2019-09-14T09:02:43+00:00" 3034 | }, 3035 | { 3036 | "name": "sebastian/global-state", 3037 | "version": "3.0.0", 3038 | "source": { 3039 | "type": "git", 3040 | "url": "https://github.com/sebastianbergmann/global-state.git", 3041 | "reference": "edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4" 3042 | }, 3043 | "dist": { 3044 | "type": "zip", 3045 | "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4", 3046 | "reference": "edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4", 3047 | "shasum": "" 3048 | }, 3049 | "require": { 3050 | "php": "^7.2", 3051 | "sebastian/object-reflector": "^1.1.1", 3052 | "sebastian/recursion-context": "^3.0" 3053 | }, 3054 | "require-dev": { 3055 | "ext-dom": "*", 3056 | "phpunit/phpunit": "^8.0" 3057 | }, 3058 | "suggest": { 3059 | "ext-uopz": "*" 3060 | }, 3061 | "type": "library", 3062 | "extra": { 3063 | "branch-alias": { 3064 | "dev-master": "3.0-dev" 3065 | } 3066 | }, 3067 | "autoload": { 3068 | "classmap": [ 3069 | "src/" 3070 | ] 3071 | }, 3072 | "notification-url": "https://packagist.org/downloads/", 3073 | "license": [ 3074 | "BSD-3-Clause" 3075 | ], 3076 | "authors": [ 3077 | { 3078 | "name": "Sebastian Bergmann", 3079 | "email": "sebastian@phpunit.de" 3080 | } 3081 | ], 3082 | "description": "Snapshotting of global state", 3083 | "homepage": "http://www.github.com/sebastianbergmann/global-state", 3084 | "keywords": [ 3085 | "global state" 3086 | ], 3087 | "time": "2019-02-01T05:30:01+00:00" 3088 | }, 3089 | { 3090 | "name": "sebastian/object-enumerator", 3091 | "version": "3.0.3", 3092 | "source": { 3093 | "type": "git", 3094 | "url": "https://github.com/sebastianbergmann/object-enumerator.git", 3095 | "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5" 3096 | }, 3097 | "dist": { 3098 | "type": "zip", 3099 | "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/7cfd9e65d11ffb5af41198476395774d4c8a84c5", 3100 | "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5", 3101 | "shasum": "" 3102 | }, 3103 | "require": { 3104 | "php": "^7.0", 3105 | "sebastian/object-reflector": "^1.1.1", 3106 | "sebastian/recursion-context": "^3.0" 3107 | }, 3108 | "require-dev": { 3109 | "phpunit/phpunit": "^6.0" 3110 | }, 3111 | "type": "library", 3112 | "extra": { 3113 | "branch-alias": { 3114 | "dev-master": "3.0.x-dev" 3115 | } 3116 | }, 3117 | "autoload": { 3118 | "classmap": [ 3119 | "src/" 3120 | ] 3121 | }, 3122 | "notification-url": "https://packagist.org/downloads/", 3123 | "license": [ 3124 | "BSD-3-Clause" 3125 | ], 3126 | "authors": [ 3127 | { 3128 | "name": "Sebastian Bergmann", 3129 | "email": "sebastian@phpunit.de" 3130 | } 3131 | ], 3132 | "description": "Traverses array structures and object graphs to enumerate all referenced objects", 3133 | "homepage": "https://github.com/sebastianbergmann/object-enumerator/", 3134 | "time": "2017-08-03T12:35:26+00:00" 3135 | }, 3136 | { 3137 | "name": "sebastian/object-reflector", 3138 | "version": "1.1.1", 3139 | "source": { 3140 | "type": "git", 3141 | "url": "https://github.com/sebastianbergmann/object-reflector.git", 3142 | "reference": "773f97c67f28de00d397be301821b06708fca0be" 3143 | }, 3144 | "dist": { 3145 | "type": "zip", 3146 | "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/773f97c67f28de00d397be301821b06708fca0be", 3147 | "reference": "773f97c67f28de00d397be301821b06708fca0be", 3148 | "shasum": "" 3149 | }, 3150 | "require": { 3151 | "php": "^7.0" 3152 | }, 3153 | "require-dev": { 3154 | "phpunit/phpunit": "^6.0" 3155 | }, 3156 | "type": "library", 3157 | "extra": { 3158 | "branch-alias": { 3159 | "dev-master": "1.1-dev" 3160 | } 3161 | }, 3162 | "autoload": { 3163 | "classmap": [ 3164 | "src/" 3165 | ] 3166 | }, 3167 | "notification-url": "https://packagist.org/downloads/", 3168 | "license": [ 3169 | "BSD-3-Clause" 3170 | ], 3171 | "authors": [ 3172 | { 3173 | "name": "Sebastian Bergmann", 3174 | "email": "sebastian@phpunit.de" 3175 | } 3176 | ], 3177 | "description": "Allows reflection of object attributes, including inherited and non-public ones", 3178 | "homepage": "https://github.com/sebastianbergmann/object-reflector/", 3179 | "time": "2017-03-29T09:07:27+00:00" 3180 | }, 3181 | { 3182 | "name": "sebastian/recursion-context", 3183 | "version": "3.0.0", 3184 | "source": { 3185 | "type": "git", 3186 | "url": "https://github.com/sebastianbergmann/recursion-context.git", 3187 | "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8" 3188 | }, 3189 | "dist": { 3190 | "type": "zip", 3191 | "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", 3192 | "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", 3193 | "shasum": "" 3194 | }, 3195 | "require": { 3196 | "php": "^7.0" 3197 | }, 3198 | "require-dev": { 3199 | "phpunit/phpunit": "^6.0" 3200 | }, 3201 | "type": "library", 3202 | "extra": { 3203 | "branch-alias": { 3204 | "dev-master": "3.0.x-dev" 3205 | } 3206 | }, 3207 | "autoload": { 3208 | "classmap": [ 3209 | "src/" 3210 | ] 3211 | }, 3212 | "notification-url": "https://packagist.org/downloads/", 3213 | "license": [ 3214 | "BSD-3-Clause" 3215 | ], 3216 | "authors": [ 3217 | { 3218 | "name": "Jeff Welch", 3219 | "email": "whatthejeff@gmail.com" 3220 | }, 3221 | { 3222 | "name": "Sebastian Bergmann", 3223 | "email": "sebastian@phpunit.de" 3224 | }, 3225 | { 3226 | "name": "Adam Harvey", 3227 | "email": "aharvey@php.net" 3228 | } 3229 | ], 3230 | "description": "Provides functionality to recursively process PHP variables", 3231 | "homepage": "http://www.github.com/sebastianbergmann/recursion-context", 3232 | "time": "2017-03-03T06:23:57+00:00" 3233 | }, 3234 | { 3235 | "name": "sebastian/resource-operations", 3236 | "version": "2.0.1", 3237 | "source": { 3238 | "type": "git", 3239 | "url": "https://github.com/sebastianbergmann/resource-operations.git", 3240 | "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9" 3241 | }, 3242 | "dist": { 3243 | "type": "zip", 3244 | "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/4d7a795d35b889bf80a0cc04e08d77cedfa917a9", 3245 | "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9", 3246 | "shasum": "" 3247 | }, 3248 | "require": { 3249 | "php": "^7.1" 3250 | }, 3251 | "type": "library", 3252 | "extra": { 3253 | "branch-alias": { 3254 | "dev-master": "2.0-dev" 3255 | } 3256 | }, 3257 | "autoload": { 3258 | "classmap": [ 3259 | "src/" 3260 | ] 3261 | }, 3262 | "notification-url": "https://packagist.org/downloads/", 3263 | "license": [ 3264 | "BSD-3-Clause" 3265 | ], 3266 | "authors": [ 3267 | { 3268 | "name": "Sebastian Bergmann", 3269 | "email": "sebastian@phpunit.de" 3270 | } 3271 | ], 3272 | "description": "Provides a list of PHP built-in functions that operate on resources", 3273 | "homepage": "https://www.github.com/sebastianbergmann/resource-operations", 3274 | "time": "2018-10-04T04:07:39+00:00" 3275 | }, 3276 | { 3277 | "name": "sebastian/type", 3278 | "version": "1.1.3", 3279 | "source": { 3280 | "type": "git", 3281 | "url": "https://github.com/sebastianbergmann/type.git", 3282 | "reference": "3aaaa15fa71d27650d62a948be022fe3b48541a3" 3283 | }, 3284 | "dist": { 3285 | "type": "zip", 3286 | "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/3aaaa15fa71d27650d62a948be022fe3b48541a3", 3287 | "reference": "3aaaa15fa71d27650d62a948be022fe3b48541a3", 3288 | "shasum": "" 3289 | }, 3290 | "require": { 3291 | "php": "^7.2" 3292 | }, 3293 | "require-dev": { 3294 | "phpunit/phpunit": "^8.2" 3295 | }, 3296 | "type": "library", 3297 | "extra": { 3298 | "branch-alias": { 3299 | "dev-master": "1.1-dev" 3300 | } 3301 | }, 3302 | "autoload": { 3303 | "classmap": [ 3304 | "src/" 3305 | ] 3306 | }, 3307 | "notification-url": "https://packagist.org/downloads/", 3308 | "license": [ 3309 | "BSD-3-Clause" 3310 | ], 3311 | "authors": [ 3312 | { 3313 | "name": "Sebastian Bergmann", 3314 | "email": "sebastian@phpunit.de", 3315 | "role": "lead" 3316 | } 3317 | ], 3318 | "description": "Collection of value objects that represent the types of the PHP type system", 3319 | "homepage": "https://github.com/sebastianbergmann/type", 3320 | "time": "2019-07-02T08:10:15+00:00" 3321 | }, 3322 | { 3323 | "name": "sebastian/version", 3324 | "version": "2.0.1", 3325 | "source": { 3326 | "type": "git", 3327 | "url": "https://github.com/sebastianbergmann/version.git", 3328 | "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" 3329 | }, 3330 | "dist": { 3331 | "type": "zip", 3332 | "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", 3333 | "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", 3334 | "shasum": "" 3335 | }, 3336 | "require": { 3337 | "php": ">=5.6" 3338 | }, 3339 | "type": "library", 3340 | "extra": { 3341 | "branch-alias": { 3342 | "dev-master": "2.0.x-dev" 3343 | } 3344 | }, 3345 | "autoload": { 3346 | "classmap": [ 3347 | "src/" 3348 | ] 3349 | }, 3350 | "notification-url": "https://packagist.org/downloads/", 3351 | "license": [ 3352 | "BSD-3-Clause" 3353 | ], 3354 | "authors": [ 3355 | { 3356 | "name": "Sebastian Bergmann", 3357 | "email": "sebastian@phpunit.de", 3358 | "role": "lead" 3359 | } 3360 | ], 3361 | "description": "Library that helps with managing the version number of Git-hosted PHP projects", 3362 | "homepage": "https://github.com/sebastianbergmann/version", 3363 | "time": "2016-10-03T07:35:21+00:00" 3364 | }, 3365 | { 3366 | "name": "theseer/tokenizer", 3367 | "version": "1.1.3", 3368 | "source": { 3369 | "type": "git", 3370 | "url": "https://github.com/theseer/tokenizer.git", 3371 | "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9" 3372 | }, 3373 | "dist": { 3374 | "type": "zip", 3375 | "url": "https://api.github.com/repos/theseer/tokenizer/zipball/11336f6f84e16a720dae9d8e6ed5019efa85a0f9", 3376 | "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9", 3377 | "shasum": "" 3378 | }, 3379 | "require": { 3380 | "ext-dom": "*", 3381 | "ext-tokenizer": "*", 3382 | "ext-xmlwriter": "*", 3383 | "php": "^7.0" 3384 | }, 3385 | "type": "library", 3386 | "autoload": { 3387 | "classmap": [ 3388 | "src/" 3389 | ] 3390 | }, 3391 | "notification-url": "https://packagist.org/downloads/", 3392 | "license": [ 3393 | "BSD-3-Clause" 3394 | ], 3395 | "authors": [ 3396 | { 3397 | "name": "Arne Blankerts", 3398 | "email": "arne@blankerts.de", 3399 | "role": "Developer" 3400 | } 3401 | ], 3402 | "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", 3403 | "time": "2019-06-13T22:48:21+00:00" 3404 | }, 3405 | { 3406 | "name": "webmozart/assert", 3407 | "version": "1.8.0", 3408 | "source": { 3409 | "type": "git", 3410 | "url": "https://github.com/webmozart/assert.git", 3411 | "reference": "ab2cb0b3b559010b75981b1bdce728da3ee90ad6" 3412 | }, 3413 | "dist": { 3414 | "type": "zip", 3415 | "url": "https://api.github.com/repos/webmozart/assert/zipball/ab2cb0b3b559010b75981b1bdce728da3ee90ad6", 3416 | "reference": "ab2cb0b3b559010b75981b1bdce728da3ee90ad6", 3417 | "shasum": "" 3418 | }, 3419 | "require": { 3420 | "php": "^5.3.3 || ^7.0", 3421 | "symfony/polyfill-ctype": "^1.8" 3422 | }, 3423 | "conflict": { 3424 | "vimeo/psalm": "<3.9.1" 3425 | }, 3426 | "require-dev": { 3427 | "phpunit/phpunit": "^4.8.36 || ^7.5.13" 3428 | }, 3429 | "type": "library", 3430 | "autoload": { 3431 | "psr-4": { 3432 | "Webmozart\\Assert\\": "src/" 3433 | } 3434 | }, 3435 | "notification-url": "https://packagist.org/downloads/", 3436 | "license": [ 3437 | "MIT" 3438 | ], 3439 | "authors": [ 3440 | { 3441 | "name": "Bernhard Schussek", 3442 | "email": "bschussek@gmail.com" 3443 | } 3444 | ], 3445 | "description": "Assertions to validate method input/output with nice error messages.", 3446 | "keywords": [ 3447 | "assert", 3448 | "check", 3449 | "validate" 3450 | ], 3451 | "time": "2020-04-18T12:12:48+00:00" 3452 | } 3453 | ], 3454 | "aliases": [], 3455 | "minimum-stability": "stable", 3456 | "stability-flags": [], 3457 | "prefer-stable": false, 3458 | "prefer-lowest": false, 3459 | "platform": { 3460 | "php": "^7.4", 3461 | "ext-json": "*", 3462 | "ext-pdo": "*", 3463 | "ext-pdo_pgsql": "*" 3464 | }, 3465 | "platform-dev": [], 3466 | "plugin-api-version": "1.1.0" 3467 | } 3468 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | services: 3 | 4 | http-proxy: 5 | build: 6 | context: infra/nginx 7 | restart: unless-stopped 8 | depends_on: 9 | - app 10 | - docs 11 | ports: 12 | - "80:80" 13 | 14 | app: 15 | build: 16 | context: . 17 | target: dev 18 | restart: unless-stopped 19 | volumes: 20 | - .:/app 21 | depends_on: 22 | - database 23 | environment: 24 | - COMPOSER_CACHE_DIR=/app/var/composer 25 | env_file: 26 | - .env.example 27 | 28 | docs: 29 | image: swaggerapi/swagger-ui 30 | volumes: 31 | - ./docs/todos.openapi.yml:/app/todos.openapi.yml 32 | environment: 33 | - LAYOUT=BaseLayout 34 | - SWAGGER_JSON=/app/todos.openapi.yml 35 | 36 | database: 37 | image: postgres:11-alpine 38 | restart: unless-stopped 39 | ports: 40 | - "5432:5432" 41 | environment: 42 | LC_ALL: C.UTF-8 43 | POSTGRES_USER: app 44 | POSTGRES_PASSWORD: password 45 | POSTGRES_DB: app 46 | 47 | -------------------------------------------------------------------------------- /docs/todos.openapi.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.2 2 | info: 3 | title: ToDos API 4 | description: A simple CRUD API 5 | version: 0.0.1 6 | 7 | tags: 8 | - name: ToDos 9 | description: the ToDo resource 10 | 11 | paths: 12 | /todos: 13 | get: 14 | tags: [ ToDos ] 15 | summary: List all ToDos 16 | operationId: listToDos 17 | parameters: 18 | - $ref: '#/components/parameters/search' 19 | - $ref: '#/components/parameters/page' 20 | - $ref: '#/components/parameters/pageSize' 21 | responses: 22 | '200': 23 | $ref: '#/components/responses/ToDoList' 24 | post: 25 | tags: [ ToDos ] 26 | summary: Add a new ToDo 27 | operationId: createToDo 28 | requestBody: 29 | $ref: '#/components/requestBodies/ToDo' 30 | responses: 31 | '201': 32 | $ref: '#/components/responses/ToDo' 33 | '400': 34 | $ref: '#/components/responses/BadRequest' 35 | '401': 36 | $ref: '#/components/responses/Unauthorized' 37 | security: 38 | - basicAuth: [] 39 | /todos/{id}: 40 | parameters: 41 | - $ref: '#/components/parameters/id' 42 | get: 43 | tags: [ ToDos ] 44 | summary: Get a ToDo 45 | operationId: getToDo 46 | responses: 47 | '200': 48 | $ref: '#/components/responses/ToDo' 49 | '404': 50 | $ref: '#/components/responses/NotFound' 51 | '400': 52 | $ref: '#/components/responses/BadRequest' 53 | patch: 54 | tags: [ ToDos ] 55 | summary: Update a ToDo 56 | operationId: patchToDo 57 | requestBody: 58 | $ref: '#/components/requestBodies/ToDo' 59 | responses: 60 | '200': 61 | $ref: '#/components/responses/ToDo' 62 | '404': 63 | $ref: '#/components/responses/NotFound' 64 | '400': 65 | $ref: '#/components/responses/BadRequest' 66 | '401': 67 | $ref: '#/components/responses/Unauthorized' 68 | security: 69 | - basicAuth: [] 70 | delete: 71 | tags: [ ToDos ] 72 | summary: Delete a ToDo 73 | operationId: deleteToDo 74 | responses: 75 | '204': 76 | description: Empty response 77 | '404': 78 | $ref: '#/components/responses/NotFound' 79 | '400': 80 | $ref: '#/components/responses/BadRequest' 81 | '401': 82 | $ref: '#/components/responses/Unauthorized' 83 | security: 84 | - basicAuth: [] 85 | 86 | components: 87 | schemas: 88 | ToDo: 89 | properties: 90 | id: 91 | type: string 92 | readOnly: true 93 | example: "fd1a8610-8aa2-485c-880a-90e43ec189c3" 94 | createdAt: 95 | type: string 96 | readOnly: true 97 | example: "4020-04-20T16:20:00.000000+0000" 98 | format: date-time 99 | name: 100 | type: string 101 | example: "Pay the bills" 102 | dueFor: 103 | type: string 104 | example: "4020-04-20T16:20:00.000000+0000" 105 | format: date-time 106 | doneAt: 107 | type: string 108 | example: "4020-04-20T16:20:00.000000+0000" 109 | format: date-time 110 | isDone: 111 | type: bool 112 | readOnly: true 113 | required: 114 | - id 115 | - name 116 | GenericError: 117 | properties: 118 | error: 119 | type: object 120 | properties: 121 | message: 122 | type: string 123 | code: 124 | type: integer 125 | required: 126 | - message 127 | - code 128 | InvalidDataError: 129 | properties: 130 | error: 131 | type: object 132 | properties: 133 | message: 134 | type: string 135 | code: 136 | type: integer 137 | details: 138 | type: object 139 | example: 140 | messageIdentifier: detailed message 141 | required: 142 | - message 143 | - code 144 | 145 | requestBodies: 146 | ToDo: 147 | required: true 148 | content: 149 | application/json: 150 | schema: 151 | $ref: '#/components/schemas/ToDo' 152 | ToDoRating: 153 | required: true 154 | content: 155 | application/json: 156 | schema: 157 | $ref: '#/components/schemas/ToDoRating' 158 | 159 | responses: 160 | ToDo: 161 | description: A single ToDo 162 | content: 163 | application/json: 164 | schema: 165 | $ref: '#/components/schemas/ToDo' 166 | ToDoList: 167 | description: A list of ToDos 168 | content: 169 | application/json: 170 | schema: 171 | properties: 172 | items: 173 | type: array 174 | items: 175 | $ref: '#/components/schemas/ToDo' 176 | nextPage: 177 | type: integer 178 | example: 2 179 | prevPage: 180 | type: integer 181 | example: null 182 | totalPages: 183 | type: integer 184 | example: 2 185 | NotFound: 186 | description: Resource not found 187 | content: 188 | application/json: 189 | schema: 190 | $ref: '#/components/schemas/GenericError' 191 | BadRequest: 192 | description: Invalid request 193 | content: 194 | application/json: 195 | schema: 196 | $ref: '#/components/schemas/InvalidDataError' 197 | Unauthorized: 198 | description: Authentication failed 199 | content: {} 200 | headers: 201 | WWW-Authenticate: 202 | schema: 203 | type: string 204 | description: Supported authentication schema 205 | 206 | parameters: 207 | id: 208 | in: path 209 | name: id 210 | description: A string representation of a UUID 211 | required: true 212 | schema: 213 | type: string 214 | format: uuid 215 | example: 'fd1a8610-8aa2-485c-880a-90e43ec189c3' 216 | search: 217 | name: search 218 | in: query 219 | schema: 220 | type: string 221 | example: 'pay' 222 | description: Full text search phrase 223 | page: 224 | name: page 225 | in: query 226 | schema: 227 | type: integer 228 | default: 1 229 | example: 1 230 | description: The page number 231 | pageSize: 232 | name: pageSize 233 | in: query 234 | schema: 235 | type: integer 236 | default: 20 237 | example: 20 238 | description: The maximum page size 239 | securitySchemes: 240 | basicAuth: 241 | type: http 242 | scheme: basic 243 | 244 | security: 245 | - basicAuth: [] 246 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | handle($request); 20 | } catch (Throwable $e) { 21 | logger()->error($e, ['exception' => $e, 'request' => $request ?? null]); 22 | $response = new JsonResponse([ 'error' => exception_to_array($e) ], 500); 23 | } 24 | 25 | (new SapiStreamEmitter())->emit($response); 26 | })(); 27 | -------------------------------------------------------------------------------- /infra/docker-compose.ci.override.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | 3 | services: 4 | 5 | app: 6 | image: ${APP_DEV_IMAGE}:${CI_COMMIT_REF_SLUG} 7 | 8 | reverse-proxy: 9 | image: ${HTTP_PROXY_IMAGE}:${CI_COMMIT_REF_SLUG} 10 | -------------------------------------------------------------------------------- /infra/nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:alpine 2 | 3 | ENV FASTCGI_TARGET='app:9000' 4 | ENV DOCS_TARGET='http://docs:8080/' 5 | 6 | COPY nginx.conf /etc/nginx/default.tmpl 7 | 8 | RUN apk add --no-cache curl 9 | 10 | HEALTHCHECK --interval=30s --timeout=2s CMD curl -f localhost/nginx_status || exit 1 11 | 12 | CMD [ "/bin/sh", "-c", "envsubst '${FASTCGI_TARGET} ${DOCS_TARGET}' < /etc/nginx/default.tmpl > /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;' || cat /etc/nginx/conf.d/default.conf" ] 13 | 14 | -------------------------------------------------------------------------------- /infra/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name localhost; 4 | root /usr/share/nginx/html; 5 | error_page 500 502 503 504 /50x.html; 6 | 7 | client_max_body_size 100m; 8 | 9 | location / { 10 | try_files $uri /index.php$is_args$args; 11 | } 12 | 13 | location ~ \.php$ { 14 | fastcgi_split_path_info ^(.+\.php)(/.+)$; 15 | fastcgi_pass ${FASTCGI_TARGET}; 16 | fastcgi_index index.php; 17 | include fastcgi_params; 18 | fastcgi_param DOCUMENT_ROOT /app; 19 | fastcgi_param SCRIPT_FILENAME /app/$fastcgi_script_name; 20 | } 21 | 22 | location /docs/ { 23 | proxy_pass ${DOCS_TARGET}; 24 | } 25 | 26 | # add trailing slash 27 | location = /docs { 28 | return 302 $scheme://$host$request_uri/; 29 | } 30 | 31 | location ~* ^/(robots\.txt|favicon\.ico)$ { 32 | access_log off; 33 | log_not_found off; 34 | return 404; 35 | } 36 | 37 | location = /nginx_status { 38 | stub_status on; 39 | access_log off; 40 | allow 127.0.0.1; 41 | deny all; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /infra/php/php-fpm.conf: -------------------------------------------------------------------------------- 1 | [www] 2 | pm.status_path = /status 3 | -------------------------------------------------------------------------------- /infra/php/php.ini: -------------------------------------------------------------------------------- 1 | date.timezone = UTC 2 | memory_limit = -1 3 | log_errors = 1 4 | error_reporting = E_ALL 5 | upload_max_filesize = 32M 6 | post_max_size = 32M 7 | expose_php = off 8 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: max 3 | ignoreErrors: 4 | - message: "#factory expects callable#" 5 | path: src/Config/DependencyInjection.php 6 | includes: 7 | - /opt/composer/vendor/phpstan/phpstan-beberlei-assert/extension.neon 8 | - /opt/composer/vendor/phpstan/phpstan-phpunit/extension.neon 9 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | test 11 | 12 | 13 | test 14 | 15 | 16 | 17 | 18 | 19 | src 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/App.php: -------------------------------------------------------------------------------- 1 | get(__CLASS__); 33 | $app->container = $container; 34 | 35 | return $app; 36 | } 37 | 38 | public function __construct(Router $router) 39 | { 40 | $this->router = $router; 41 | } 42 | 43 | public function handle(Request $request): Response 44 | { 45 | try { 46 | $response = $this->router->dispatch($request); 47 | } catch (InvalidDataException $e) { 48 | return new JsonResponse([ 'error' => exception_to_array($e) ], 400); 49 | } catch (HttpException $e) { 50 | return new JsonResponse([ 'error' => exception_to_array($e) ], $e->getStatusCode()); 51 | } 52 | 53 | return $response; 54 | } 55 | 56 | public function get($id) 57 | { 58 | return $this->container->get($id); 59 | } 60 | 61 | public function has($id) 62 | { 63 | return $this->container->has($id); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Config/DependencyInjection.php: -------------------------------------------------------------------------------- 1 | addDefinitions( 22 | [ 23 | LoggerInterface::class => DI\factory('\Acme\ToDo\logger'), 24 | Router::class => DI\factory(RouterFactory::class), 25 | PDO::class => DI\factory(PdoFactory::class), 26 | ToDoDataMapper::class => DI\autowire()->lazy(), 27 | BasicAuthentication::class => DI\create()->constructor( 28 | json_decode(env('AUTH_USERS'), true, 512, JSON_THROW_ON_ERROR) 29 | ), 30 | ContentType::class => DI\create() 31 | ->constructor( 32 | array_filter( 33 | ContentType::getDefaultFormats(), 34 | function ($key) { 35 | return $key === 'json'; 36 | }, 37 | ARRAY_FILTER_USE_KEY 38 | ) 39 | ) 40 | ->method('useDefault', false), 41 | ] 42 | ); 43 | 44 | if (env('CACHE')) { 45 | $builder->enableCompilation(App::CACHE_DIR); 46 | } 47 | 48 | return $builder->build(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Config/LoggerFactory.php: -------------------------------------------------------------------------------- 1 | includeStacktraces(); 17 | 18 | $streamHandler = new MonologStreamHandler('php://stderr', env('DEBUG') ? Monolog::DEBUG : Monolog::INFO); 19 | $streamHandler->setFormatter($formatter); 20 | $logger->pushHandler($streamHandler); 21 | 22 | return $logger; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Config/PdoFactory.php: -------------------------------------------------------------------------------- 1 | setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); 23 | 24 | return $pdo; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Config/RouterFactory.php: -------------------------------------------------------------------------------- 1 | setContainer($container); 22 | $router = new LeagueRouter(); 23 | $router->setStrategy($strategy); 24 | 25 | $authMiddleware = $container->get(BasicAuthentication::class); 26 | $contentNegotiationMiddleware = $container->get(ContentType::class); 27 | 28 | Assert::thatAll([ $authMiddleware, $contentNegotiationMiddleware ])->isInstanceOf(Middleware::class); 29 | 30 | $router->middleware($contentNegotiationMiddleware); 31 | $router->middleware(new JsonPayload()); 32 | 33 | $router->map('GET', '/', RouteHandler\Home::class); 34 | 35 | $router->group('/todos', function (RouteGroup $route) use ($authMiddleware): void { 36 | $route->map('GET', '/', RouteHandler\ToDoList::class); 37 | $route->map('POST', '/', RouteHandler\ToDoCreate::class)->middleware($authMiddleware); 38 | $route->map('GET', '/{id}', RouteHandler\ToDoRead::class); 39 | $route->map('PATCH', '/{id}', RouteHandler\ToDoUpdate::class)->middleware($authMiddleware); 40 | $route->map('DELETE', '/{id}', RouteHandler\ToDoDelete::class)->middleware($authMiddleware); 41 | }); 42 | 43 | return $router; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Http/RouteHandler.php: -------------------------------------------------------------------------------- 1 | [ 22 | 'todos' => $request->getUri() . 'todos', 23 | 'docs' => $request->getUri() . 'docs', 24 | ], 25 | ]); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Http/RouteHandler/ToDoCreate.php: -------------------------------------------------------------------------------- 1 | todoDataMapper = $todoDataMapper; 24 | } 25 | 26 | /** 27 | * @param Request $request 28 | * @param string[] $args 29 | * 30 | * @return Response 31 | * 32 | * @throws BadRequestException 33 | * @throws InvalidDataException 34 | */ 35 | public function __invoke(Request $request, array $args): Response 36 | { 37 | $requestBody = $request->getParsedBody(); 38 | 39 | if (! is_array($requestBody)) { 40 | throw new BadRequestException('Invalid request body'); 41 | } 42 | 43 | $item = ToDo::createFromArray($requestBody); 44 | 45 | $this->todoDataMapper->insert($item); 46 | 47 | return new JsonResponse($item, 201); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Http/RouteHandler/ToDoDelete.php: -------------------------------------------------------------------------------- 1 | todoDataMapper = $todoDataMapper; 23 | } 24 | 25 | /** 26 | * @param Request $request 27 | * @param string[] $args 28 | * 29 | * @return Response 30 | * 31 | * @throws Http\Exception\BadRequestException 32 | * @throws Http\Exception\NotFoundException 33 | */ 34 | public function __invoke(Request $request, array $args): Response 35 | { 36 | if (! Uuid::isValid($args['id'])) { 37 | throw new Http\Exception\BadRequestException('Invalid UUID'); 38 | } 39 | 40 | $item = $this->todoDataMapper->find($args['id']); 41 | 42 | if ($item === null) { 43 | throw new Http\Exception\NotFoundException('Resource not found'); 44 | } 45 | 46 | $this->todoDataMapper->delete($args['id']); 47 | 48 | return new EmptyResponse(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Http/RouteHandler/ToDoList.php: -------------------------------------------------------------------------------- 1 | todoDataMapper = $todoDataMapper; 24 | } 25 | 26 | /** 27 | * @param Request $request 28 | * @param string[] $args 29 | * 30 | * @return Response 31 | * 32 | * @throws BadRequestException 33 | */ 34 | public function __invoke(Request $request, array $args): Response 35 | { 36 | $query = $request->getQueryParams(); 37 | $search = $query['search'] ?? ''; 38 | 39 | $page = (int) ($query['page'] ?? 1); 40 | if ($page < 1) { 41 | throw new BadRequestException('Page must be greater than 0'); 42 | } 43 | 44 | $pageSize = (int) ($query['pageSize'] ?? ToDoDataMapper::DEFAULT_PAGE_SIZE); 45 | 46 | if ($pageSize < 1 || $pageSize > self::MAX_PAGE_SIZE) { 47 | throw new BadRequestException(sprintf('Page size must be between 1 and %s', self::MAX_PAGE_SIZE)); 48 | } 49 | 50 | $totalPages = $this->todoDataMapper->countPages($search, $pageSize); 51 | $items = $this->todoDataMapper->getAll($search, $page, $pageSize); 52 | 53 | $prev = $page > 1 ? min($page - 1, $totalPages) : null; 54 | $next = $page < $totalPages ? $page + 1 : null; 55 | 56 | return new JsonResponse(compact('items', 'totalPages', 'prev', 'next')); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Http/RouteHandler/ToDoRead.php: -------------------------------------------------------------------------------- 1 | todoDataMapper = $todoDataMapper; 23 | } 24 | 25 | /** 26 | * @param Request $request 27 | * @param string[] $args 28 | * 29 | * @return Response 30 | * 31 | * @throws Http\Exception\BadRequestException 32 | * @throws Http\Exception\NotFoundException 33 | */ 34 | public function __invoke(Request $request, array $args): Response 35 | { 36 | if (! Uuid::isValid($args['id'])) { 37 | throw new Http\Exception\BadRequestException('Invalid UUID'); 38 | } 39 | 40 | $item = $this->todoDataMapper->find($args['id']); 41 | 42 | if ($item === null) { 43 | throw new Http\Exception\NotFoundException('Resource not found'); 44 | } 45 | 46 | return new JsonResponse($item); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Http/RouteHandler/ToDoUpdate.php: -------------------------------------------------------------------------------- 1 | todoDataMapper = $todoDataMapper; 25 | } 26 | 27 | /** 28 | * @param Request $request 29 | * @param string[] $args 30 | * 31 | * @return Response 32 | * 33 | * @throws BadRequestException 34 | * @throws InvalidDataException 35 | * @throws NotFoundException 36 | */ 37 | public function __invoke(Request $request, array $args): Response 38 | { 39 | if (! Uuid::isValid($args['id'])) { 40 | throw new BadRequestException('Invalid UUID'); 41 | } 42 | 43 | $item = $this->todoDataMapper->find($args['id']); 44 | 45 | if ($item === null) { 46 | throw new NotFoundException('Resource not found'); 47 | } 48 | 49 | $requestBody = $request->getParsedBody(); 50 | 51 | if (! is_array($requestBody)) { 52 | throw new BadRequestException('Invalid request body'); 53 | } 54 | 55 | $item->updateFromArray($requestBody); 56 | 57 | $this->todoDataMapper->update($item); 58 | 59 | return new JsonResponse($item, 200); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Model/InvalidDataException.php: -------------------------------------------------------------------------------- 1 | getErrorExceptions() as $error) { 21 | $new->details[$error->getPropertyPath()] = $error->getMessage(); 22 | } 23 | 24 | return $new; 25 | } 26 | 27 | public static function fromAssertInvalidArgumentException(InvalidArgumentException $e): self 28 | { 29 | $new = new static('Invalid data', 0, $e); 30 | $new->details[$e->getPropertyPath()] = $e->getMessage(); 31 | return $new; 32 | } 33 | 34 | /** 35 | * @return string[] 36 | */ 37 | public function getDetails(): array 38 | { 39 | return $this->details; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Model/ToDo.php: -------------------------------------------------------------------------------- 1 | id = Uuid::uuid4(); 34 | $this->createdAt = now(); 35 | 36 | $this->validate(compact('name')); 37 | 38 | $this->name = $name; 39 | $this->dueFor = $dueFor; 40 | $this->doneAt = $doneAt; 41 | } 42 | 43 | public function getId(): string 44 | { 45 | return $this->id->toString(); 46 | } 47 | 48 | public function withId(string $id): self 49 | { 50 | $new = clone $this; 51 | $new->id = Uuid::fromString($id); 52 | 53 | return $new; 54 | } 55 | 56 | public function withCreatedAt(string $createdAt): self 57 | { 58 | $new = clone $this; 59 | $new->createdAt = datetime_from_string($createdAt); 60 | 61 | return $new; 62 | } 63 | 64 | public function getName(): string 65 | { 66 | return $this->name; 67 | } 68 | 69 | public function getCreatedAt(): DateTimeImmutable 70 | { 71 | return $this->createdAt; 72 | } 73 | 74 | public function getCreatedAtAsString(): string 75 | { 76 | return $this->createdAt->format(DATE_FORMAT); 77 | } 78 | 79 | public function getDueFor(): ?DateTimeImmutable 80 | { 81 | return $this->dueFor; 82 | } 83 | 84 | public function getDueForAsString(): string 85 | { 86 | return $this->dueFor ? $this->dueFor->format(DATE_FORMAT) : ''; 87 | } 88 | 89 | public function markDone(): void 90 | { 91 | $this->doneAt = now(); 92 | } 93 | 94 | public function getDoneAt(): ?DateTimeImmutable 95 | { 96 | return $this->doneAt; 97 | } 98 | 99 | public function getDoneAtAsString(): string 100 | { 101 | return $this->doneAt ? $this->doneAt->format(DATE_FORMAT): ''; 102 | } 103 | 104 | public function isDone(): bool 105 | { 106 | return $this->doneAt !== null; 107 | } 108 | 109 | /** 110 | * @return mixed[] 111 | */ 112 | public function jsonSerialize(): array 113 | { 114 | return [ 115 | 'id' => $this->getId(), 116 | 'name' => $this->name, 117 | 'createdAt' => $this->getCreatedAtAsString(), 118 | 'dueFor' => $this->getDueForAsString() ?: null, 119 | 'doneAt' => $this->getDoneAtAsString() ?: null, 120 | 'isDone' => $this->isDone(), 121 | ]; 122 | } 123 | 124 | /** 125 | * This method is intended as a type-unsafe alternative to the constructor 126 | * 127 | * @param mixed[] $data 128 | * 129 | * @throws InvalidDataException 130 | */ 131 | public static function createFromArray(array $data): self 132 | { 133 | /** 134 | * we use this to avoid double validation. 135 | * @var self $new 136 | */ 137 | $new = (new Instantiator)->instantiate(__CLASS__); 138 | 139 | $new->id = Uuid::uuid4(); 140 | $new->createdAt = now(); 141 | $new->dueFor = null; 142 | $new->doneAt = null; 143 | $new->updateFromArray($data); 144 | 145 | return $new; 146 | } 147 | 148 | /** 149 | * @param mixed[] $data 150 | * @throws InvalidDataException 151 | */ 152 | public function updateFromArray(array $data): void 153 | { 154 | $this->validate($data); 155 | 156 | $this->name = $data['name'] ?? $this->name; 157 | $this->dueFor = isset($data['dueFor']) ? datetime_from_string($data['dueFor']) : $this->dueFor; 158 | $this->doneAt = isset($data['doneAt']) ? datetime_from_string($data['doneAt']) : $this->doneAt; 159 | } 160 | 161 | /** 162 | * @param mixed[] $data 163 | * 164 | * @throws InvalidDataException 165 | */ 166 | private function validate(array $data): void 167 | { 168 | $assert = Assert::lazy()->tryAll(); 169 | 170 | if (isset($data['name'])) { 171 | $assert->that($data['name'], 'name')->string()->notBlank(); 172 | } 173 | 174 | if (isset($data['dueFor'])) { 175 | $assert->that($data['dueFor'], 'dueFor')->nullOr()->date(DATE_FORMAT); 176 | } 177 | 178 | if (isset($data['doneAt'])) { 179 | $assert->that($data['doneAt'], 'doneAt')->nullOr()->date(DATE_FORMAT); 180 | } 181 | 182 | try { 183 | $assert->verifyNow(); 184 | } catch (LazyAssertionException $e) { 185 | throw InvalidDataException::fromLazyAssertionException($e); 186 | } 187 | } 188 | 189 | 190 | } 191 | -------------------------------------------------------------------------------- /src/Model/ToDoDataMapper.php: -------------------------------------------------------------------------------- 1 | pdo = $pdo; 23 | } 24 | 25 | public function insert(ToDo $item): void 26 | { 27 | $stmt = $this->pdo->prepare( 28 | <<execute(); 45 | 46 | if ($result === false) { 47 | throw new RuntimeException('PDO failed to execute a statement'); 48 | } 49 | } 50 | 51 | public function update(ToDo $item): void 52 | { 53 | $stmt = $this->pdo->prepare( 54 | <<execute(); 69 | 70 | if ($result === false) { 71 | throw new RuntimeException('PDO failed to execute a statement'); 72 | } 73 | } 74 | 75 | /** 76 | * @return ToDo[] 77 | */ 78 | public function getAll(string $search = '', int $page = 1, int $pageSize = self::DEFAULT_PAGE_SIZE): array 79 | { 80 | Assert::that($page)->greaterThan(0); 81 | Assert::that($pageSize)->greaterThan(0); 82 | $isSearch = $search !== ''; 83 | $where = $isSearch ? 'WHERE "searchVector" @@ plainto_tsquery(:search_query)' : ''; 84 | 85 | $offset = ($page - 1) * $pageSize; 86 | $limit = $pageSize; 87 | 88 | $stmt = $this->pdo->prepare( 89 | <<bindValue('search_query', $search); 104 | } 105 | 106 | $result = $stmt->execute(); 107 | 108 | if ($result === false) { 109 | throw new RuntimeException('PDO failed to execute a statement'); 110 | } 111 | 112 | $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); 113 | 114 | if ($rows === false) { 115 | throw new RuntimeException('PDO failed to fetch rows'); 116 | } 117 | 118 | $toDoFactory = Closure::fromCallable([ $this, 'createToDoFromRow' ]); 119 | 120 | return array_map($toDoFactory, $rows); 121 | } 122 | 123 | public function countPages(string $search = '', int $pageSize = self::DEFAULT_PAGE_SIZE): int 124 | { 125 | Assert::that($pageSize)->greaterThan(0); 126 | 127 | $isSearch = $search !== ''; 128 | $where = $isSearch ? 'WHERE "searchVector" @@ plainto_tsquery(:search_query)' : ''; 129 | 130 | $stmt = $this->pdo->prepare( 131 | <<bindValue('search_query', $search); 138 | } 139 | 140 | $result = $stmt->execute(); 141 | 142 | if ($result === false) { 143 | throw new RuntimeException('PDO failed to execute a statement'); 144 | } 145 | 146 | $count = $stmt->fetchColumn(); 147 | 148 | if ($count === false) { 149 | throw new RuntimeException('PDO failed to fetch a row'); 150 | } 151 | 152 | if ($count <= $pageSize) { 153 | return 1; 154 | } 155 | 156 | return (int) ceil($count / $pageSize); 157 | } 158 | 159 | public function find(string $id): ?ToDo 160 | { 161 | $stmt = $this->pdo->prepare( 162 | <<execute(compact('id')); 174 | 175 | if ($result === false) { 176 | throw new RuntimeException('PDO failed to execute a statement'); 177 | } 178 | 179 | $row = $stmt->fetch(PDO::FETCH_ASSOC); 180 | 181 | if ($row === false) { 182 | return null; 183 | } 184 | 185 | return $this->createToDoFromRow($row); 186 | } 187 | 188 | public function delete(string $id): void 189 | { 190 | $stmt = $this->pdo->prepare('DELETE FROM "todos" WHERE "id" = :id;'); 191 | $result = $stmt->execute(compact('id')); 192 | 193 | if ($result === false) { 194 | throw new RuntimeException('PDO failed to execute a statement'); 195 | } 196 | } 197 | 198 | public function initSchema(): void 199 | { 200 | $this->pdo->exec(static::getSchema()); 201 | } 202 | 203 | public function dropSchema(): void 204 | { 205 | $this->pdo->exec(sprintf('DROP TABLE IF EXISTS "%s";', 'todos')); 206 | } 207 | 208 | /** 209 | * @param ToDo $item 210 | * @param PDOStatement $stmt 211 | */ 212 | private static function bindParams(ToDo $item, PDOStatement $stmt): void 213 | { 214 | $stmt->bindValue('id', $item->getId()); 215 | $stmt->bindValue('name', $item->getName()); 216 | $stmt->bindValue('createdAt', $item->getCreatedAtAsString()); 217 | $stmt->bindValue('dueFor', $item->getDoneAtAsString() ?: null); 218 | $stmt->bindValue('doneAt', $item->getDoneAtAsString() ?: null); 219 | $stmt->bindValue('searchVector', $item->getName()); 220 | } 221 | 222 | private static function getSchema(): string 223 | { 224 | return <<withId($row['id'])->withCreatedAt($row['createdAt']); 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /src/_functions.php: -------------------------------------------------------------------------------- 1 | $exception->getMessage(), 22 | 'code' => $exception->getCode(), 23 | ]; 24 | 25 | if ($exception instanceof InvalidDataException) { 26 | $output['details'] = $exception->getDetails(); 27 | } 28 | 29 | if (env('DEBUG') === true) { 30 | $output = array_merge($output, [ 31 | 'type' => get_class($exception), 32 | 'file' => $exception->getFile(), 33 | 'line' => $exception->getLine(), 34 | 'trace' => explode("\n", $exception->getTraceAsString()), 35 | 'previous' => [], 36 | ]); 37 | } 38 | 39 | return $output; 40 | }; 41 | 42 | $result = $singleToArray($exception); 43 | $last = $exception; 44 | 45 | while ($last = $last->getPrevious()) { 46 | $result['previous'][] = $singleToArray($last); 47 | } 48 | 49 | return $result; 50 | } 51 | 52 | function php_error_handler(int $errno, string $errstr, string $errfile, int $errline): void 53 | { 54 | if (! (error_reporting() & $errno)) { 55 | return; // error_reporting does not include this error 56 | } 57 | 58 | throw new ErrorException($errstr, 0, $errno, $errfile, $errline); 59 | } 60 | 61 | function logger(Logger $newLogger = null): Logger 62 | { 63 | static $logger; 64 | 65 | if ($newLogger) { 66 | $logger = $newLogger; 67 | } 68 | 69 | if (! $logger) { 70 | $logger = (new Config\LoggerFactory())(); 71 | } 72 | 73 | return $logger; 74 | } 75 | 76 | function now(Clock $newClock = null): DateTimeImmutable 77 | { 78 | static $clock; 79 | 80 | if ($newClock) { 81 | $clock = $newClock; 82 | } 83 | 84 | if (! $clock) { 85 | $clock = new SystemClock(); 86 | } 87 | 88 | return $clock->now(); 89 | } 90 | 91 | function datetime_from_string(string $dateTime): DateTimeImmutable 92 | { 93 | $dateTime = DateTimeImmutable::createFromFormat(DATE_FORMAT, $dateTime); 94 | 95 | Assert::that($dateTime)->notSame(false); 96 | 97 | return $dateTime; 98 | } 99 | 100 | const DATE_FORMAT = "Y-m-d\TH:i:s.uO"; // ISO8601 with milliseconds -------------------------------------------------------------------------------- /test/AppIntegrationTest.php: -------------------------------------------------------------------------------- 1 | expectNotToPerformAssertions(); 15 | 16 | App::bootstrap(); 17 | } 18 | 19 | public function testRootRequest(): void 20 | { 21 | $app = App::bootstrap(); 22 | $request = (new ServerRequestFactory())->createServerRequest('GET', '/')->withHeader('accept', '*/*'); 23 | $response = $app->handle($request); 24 | assertSame(200, $response->getStatusCode()); 25 | } 26 | 27 | public function testContentNegotiation(): void 28 | { 29 | $app = App::bootstrap(); 30 | $request = (new ServerRequestFactory())->createServerRequest('GET', '/')->withHeader('accept', 'text/html'); 31 | $response = $app->handle($request); 32 | assertSame(406, $response->getStatusCode()); 33 | } 34 | 35 | /** 36 | * @dataProvider authRequiredRequestProvider 37 | */ 38 | public function testAuthenticationFailure(Request $request): void 39 | { 40 | $app = App::bootstrap(); 41 | $response = $app->handle($request); 42 | assertSame(401, $response->getStatusCode()); 43 | } 44 | 45 | /** 46 | * @dataProvider authRequiredRequestProvider 47 | */ 48 | public function testAuthenticationSuccess(Request $request): void 49 | { 50 | Env::$options |= Env::USE_ENV_ARRAY; 51 | $_ENV['AUTH_USERS'] = '{ "john": "doe" }'; 52 | $app = App::bootstrap(); 53 | $request = $request->withHeader('Authorization', 'Basic ' . base64_encode('john:doe')); 54 | $response = $app->handle($request); 55 | assertNotSame(401, $response->getStatusCode()); 56 | } 57 | 58 | /** 59 | * @return array[] 60 | */ 61 | public function authRequiredRequestProvider(): array 62 | { 63 | $serverRequestFactory = new ServerRequestFactory(); 64 | 65 | return [ 66 | [ $serverRequestFactory->createServerRequest('POST', '/todos') 67 | ->withHeader('accept', '*/*') ], 68 | [ $serverRequestFactory->createServerRequest('PATCH', '/todos/foo') 69 | ->withHeader('accept', '*/*') ], 70 | [ $serverRequestFactory->createServerRequest('DELETE', '/todos/foo') 71 | ->withHeader('accept', '*/*') ], 72 | ]; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /test/AppUnitTest.php: -------------------------------------------------------------------------------- 1 | router = $this->createMock(Router::class); 29 | $this->SUT = new App($this->router); 30 | } 31 | 32 | /** 33 | * @dataProvider exceptionProvider 34 | */ 35 | public function testItHandlesHttpAndDomainExceptions(Exception $exception): void 36 | { 37 | $request = new ServerRequest(); 38 | 39 | $this->router 40 | ->method('dispatch') 41 | ->willThrowException($exception) 42 | ; 43 | 44 | $response = $this->SUT->handle($request); 45 | $expectedStatusCode = $exception instanceof HttpException ? $exception->getStatusCode(): 400; 46 | assertSame($expectedStatusCode, $response->getStatusCode()); 47 | assertInstanceOf(JsonResponse::class, $response); /** @var JsonResponse $response */ 48 | $payload = $response->getPayload(); 49 | assertArrayHasKey('error', $payload); 50 | assertEquals(exception_to_array($exception), $payload['error']); 51 | } 52 | 53 | /** 54 | * @return array[] 55 | */ 56 | public function exceptionProvider(): array 57 | { 58 | return [ 59 | [ new HttpException(401, 'foo') ], 60 | [ new InvalidDataException() ], 61 | ]; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /test/Http/RouteHandler/ToDoCreateUnitTest.php: -------------------------------------------------------------------------------- 1 | todoDataMapper = $this->createMock(ToDoDataMapper::class); 30 | $this->SUT = new ToDoCreate($this->todoDataMapper); 31 | } 32 | 33 | public function testSuccess(): void 34 | { 35 | $data = [ 36 | 'name' => 'foo', 37 | 'dueFor' => (new DateTimeImmutable())->format(DATE_FORMAT), 38 | 'doneAt' => (new DateTimeImmutable())->format(DATE_FORMAT), 39 | ]; 40 | $request = (new ServerRequest())->withParsedBody($data); 41 | 42 | $this->todoDataMapper->expects(once())->method('insert'); 43 | 44 | $response = $this->SUT->__invoke($request, []); 45 | 46 | assertSame(201, $response->getStatusCode()); 47 | assertInstanceOf(JsonResponse::class, $response); /** @var JsonResponse $response */ 48 | assertInstanceOf(ToDo::class, $response->getPayload()); 49 | /** @var ToDo $item */ 50 | $item = $response->getPayload(); 51 | assertSame($data['name'], $item->getName()); 52 | assertSame($data['dueFor'], $item->getDueForAsString()); 53 | assertSame($data['doneAt'], $item->getDoneAtAsString()); 54 | } 55 | 56 | public function testInvalidRequestBody(): void 57 | { 58 | $request = new ServerRequest(); 59 | 60 | $this->expectException(BadRequestException::class); 61 | $this->expectExceptionMessage('Invalid request body'); 62 | 63 | $this->SUT->__invoke($request, []); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /test/Http/RouteHandler/ToDoDeleteUnitTest.php: -------------------------------------------------------------------------------- 1 | todoDataMapper = $this->createMock(ToDoDataMapper::class); 29 | $this->SUT = new ToDoDelete($this->todoDataMapper); 30 | } 31 | 32 | public function testSuccess(): void 33 | { 34 | $item = new ToDo('foo'); 35 | $request = new ServerRequest(); 36 | $args = ['id' => Uuid::NIL]; 37 | 38 | $this->todoDataMapper 39 | ->method('find') 40 | ->with($args['id']) 41 | ->willReturn($item) 42 | ; 43 | 44 | $this->todoDataMapper->expects(once())->method('delete')->with($args['id']); 45 | 46 | $response = $this->SUT->__invoke($request, $args); 47 | 48 | assertSame(204, $response->getStatusCode()); 49 | } 50 | 51 | public function testInvalidUUID(): void 52 | { 53 | $request = new ServerRequest(); 54 | $args = ['id' => 'foo']; 55 | 56 | $this->expectException(BadRequestException::class); 57 | $this->expectExceptionMessage('Invalid UUID'); 58 | 59 | $this->SUT->__invoke($request, $args); 60 | } 61 | 62 | public function testNotFound(): void 63 | { 64 | $request = new ServerRequest(); 65 | $args = ['id' => Uuid::NIL]; 66 | 67 | $this->todoDataMapper 68 | ->method('find') 69 | ->with($args['id']) 70 | ->willReturn(null) 71 | ; 72 | 73 | $this->expectException(NotFoundException::class); 74 | 75 | $this->SUT->__invoke($request, $args); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /test/Http/RouteHandler/ToDoListUnitTest.php: -------------------------------------------------------------------------------- 1 | todoDataMapper = $this->createMock(ToDoDataMapper::class); 28 | $this->SUT = new ToDoList($this->todoDataMapper); 29 | } 30 | 31 | public function testSuccess(): void 32 | { 33 | $item1 = new ToDo('foo'); 34 | $item2 = new ToDo('bar'); 35 | $request = new ServerRequest(); 36 | 37 | $records = [ $item1, $item2 ]; 38 | 39 | $this->todoDataMapper 40 | ->expects(once()) 41 | ->method('getAll') 42 | ->willReturn($records) 43 | ; 44 | 45 | $response = $this->SUT->__invoke($request, []); 46 | 47 | assertSame(200, $response->getStatusCode()); 48 | assertInstanceOf(JsonResponse::class, $response); /** @var JsonResponse $response */ 49 | $payload = $response->getPayload(); 50 | assertArrayHasKey('items', $payload); 51 | assertEquals($records, $payload['items']); 52 | } 53 | 54 | public function testPagination(): void 55 | { 56 | $query = [ 57 | 'search' => 'foo', 58 | 'page' => 2, 59 | 'pageSize' => 10, 60 | ]; 61 | $request = (new ServerRequest())->withQueryParams($query); 62 | 63 | $this->todoDataMapper 64 | ->expects(once()) 65 | ->method('getAll') 66 | ->with($query['search'], $query['page'], $query['pageSize']) 67 | ; 68 | 69 | $this->todoDataMapper 70 | ->expects(once()) 71 | ->method('countPages') 72 | ->with($query['search'], $query['pageSize']) 73 | ->willReturn(3) 74 | ; 75 | 76 | $response = $this->SUT->__invoke($request, []); 77 | assertSame(200, $response->getStatusCode()); 78 | assertInstanceOf(JsonResponse::class, $response); /** @var JsonResponse $response */ 79 | $payload = $response->getPayload(); 80 | assertArrayHasKey('prev', $payload); 81 | assertArrayHasKey('next', $payload); 82 | assertArrayHasKey('totalPages', $payload); 83 | assertSame(1, $payload['prev']); 84 | assertSame(3, $payload['next']); 85 | assertSame(3, $payload['totalPages']); 86 | } 87 | 88 | public function testInvalidPageNumber(): void 89 | { 90 | $request = (new ServerRequest())->withQueryParams([ 'page' => 0 ]); 91 | 92 | $this->expectException(BadRequestException::class); 93 | $this->expectExceptionMessage('Page must be greater than 0'); 94 | 95 | $this->SUT->__invoke($request, []); 96 | } 97 | 98 | public function testInvalidPageSize(): void 99 | { 100 | $request = (new ServerRequest())->withQueryParams([ 'pageSize' => 0 ]); 101 | 102 | $this->expectException(BadRequestException::class); 103 | $this->expectExceptionMessage('Page size must be between 1 and 100'); 104 | 105 | $this->SUT->__invoke($request, []); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /test/Http/RouteHandler/ToDoReadUnitTest.php: -------------------------------------------------------------------------------- 1 | todoDataMapper = $this->createMock(ToDoDataMapper::class); 30 | $this->SUT = new ToDoRead($this->todoDataMapper); 31 | } 32 | 33 | public function testSuccess(): void 34 | { 35 | $item = new ToDo('foo'); 36 | $request = new ServerRequest(); 37 | $args = ['id' => Uuid::NIL]; 38 | 39 | $this->todoDataMapper 40 | ->method('find') 41 | ->with($args['id']) 42 | ->willReturn($item) 43 | ; 44 | 45 | $response = $this->SUT->__invoke($request, $args); 46 | 47 | assertSame(200, $response->getStatusCode()); 48 | assertInstanceOf(JsonResponse::class, $response); /** @var JsonResponse $response */ 49 | assertEquals($item, $response->getPayload()); 50 | } 51 | 52 | public function testInvalidUUID(): void 53 | { 54 | $request = new ServerRequest(); 55 | $args = ['id' => 'foo']; 56 | 57 | $this->expectException(BadRequestException::class); 58 | $this->expectExceptionMessage('Invalid UUID'); 59 | 60 | $this->SUT->__invoke($request, $args); 61 | } 62 | 63 | public function testNotFound(): void 64 | { 65 | $request = new ServerRequest(); 66 | $args = ['id' => Uuid::NIL]; 67 | 68 | $this->todoDataMapper 69 | ->method('find') 70 | ->with($args['id']) 71 | ->willReturn(null) 72 | ; 73 | 74 | $this->expectException(NotFoundException::class); 75 | 76 | $this->SUT->__invoke($request, $args); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /test/Http/RouteHandler/ToDoUpdateUnitTest.php: -------------------------------------------------------------------------------- 1 | todoDataMapper = $this->createMock(ToDoDataMapper::class); 32 | $this->SUT = new ToDoUpdate($this->todoDataMapper); 33 | } 34 | 35 | public function testSuccess(): void 36 | { 37 | $item = new ToDo('foo'); 38 | $data = [ 39 | 'name' => 'bar', 40 | 'dueFor' => (new DateTimeImmutable())->format(DATE_FORMAT), 41 | 'doneAt' => (new DateTimeImmutable())->format(DATE_FORMAT), 42 | ]; 43 | $request = (new ServerRequest())->withParsedBody($data); 44 | $args = ['id' => Uuid::NIL]; 45 | 46 | $this->todoDataMapper 47 | ->method('find') 48 | ->with($args['id']) 49 | ->willReturn($item) 50 | ; 51 | 52 | $this->todoDataMapper 53 | ->expects(once()) 54 | ->method('update') 55 | ->with($item) 56 | ; 57 | 58 | $response = $this->SUT->__invoke($request, $args); 59 | 60 | assertSame(200, $response->getStatusCode()); 61 | assertInstanceOf(JsonResponse::class, $response); /** @var JsonResponse $response */ 62 | assertEquals($item, $response->getPayload()); 63 | assertSame($data['name'], $item->getName()); 64 | assertSame($data['dueFor'], $item->getDueForAsString()); 65 | assertSame($data['doneAt'], $item->getDoneAtAsString()); 66 | } 67 | 68 | public function testInvalidUUID(): void 69 | { 70 | $request = new ServerRequest(); 71 | $args = ['id' => 'foo']; 72 | 73 | $this->expectException(BadRequestException::class); 74 | $this->expectExceptionMessage('Invalid UUID'); 75 | 76 | $this->SUT->__invoke($request, $args); 77 | } 78 | 79 | public function testNotFound(): void 80 | { 81 | $request = new ServerRequest(); 82 | $args = ['id' => Uuid::NIL]; 83 | 84 | $this->todoDataMapper 85 | ->method('find') 86 | ->with($args['id']) 87 | ->willReturn(null) 88 | ; 89 | 90 | $this->expectException(NotFoundException::class); 91 | 92 | $this->SUT->__invoke($request, $args); 93 | } 94 | 95 | public function testInvalidRequestBody(): void 96 | { 97 | $item = new ToDo('foo'); 98 | $request = new ServerRequest(); 99 | $args = ['id' => Uuid::NIL]; 100 | 101 | $this->todoDataMapper 102 | ->method('find') 103 | ->with($args['id']) 104 | ->willReturn($item) 105 | ; 106 | 107 | $this->expectException(BadRequestException::class); 108 | $this->expectExceptionMessage('Invalid request body'); 109 | 110 | $this->SUT->__invoke($request, $args); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /test/Model/ToDoDataMapperIntegrationTest.php: -------------------------------------------------------------------------------- 1 | SUT = $app->get(ToDoDataMapper::class); 19 | $this->SUT->dropSchema(); 20 | $this->SUT->initSchema(); 21 | } 22 | 23 | public function testAddAndFind(): void 24 | { 25 | $item = new ToDo('foo'); 26 | 27 | assertNull($this->SUT->find($item->getId())); 28 | 29 | $this->SUT->insert($item); 30 | 31 | assertEquals($item, $this->SUT->find($item->getId())); 32 | } 33 | 34 | public function testAddAndGetall(): void 35 | { 36 | $item1 = new ToDo('foo'); 37 | $item2 = new ToDo('bar'); 38 | 39 | $this->SUT->insert($item1); 40 | $this->SUT->insert($item2); 41 | 42 | $items = $this->SUT->getAll(); 43 | assertEquals([ $item1, $item2 ], $items); 44 | } 45 | 46 | public function testAddUpdateAndFind(): void 47 | { 48 | $item = new ToDo('foo'); 49 | 50 | $this->SUT->insert($item); 51 | 52 | $item->updateFromArray([ 53 | 'name' => 'bar', 54 | 'prep_time_mins' => 6, 55 | 'difficulty' => 2, 56 | 'vegetarian' => true 57 | ]); 58 | 59 | $this->SUT->update($item); 60 | 61 | assertEquals($item, $this->SUT->find($item->getId())); 62 | } 63 | 64 | public function testAddAndDelete(): void 65 | { 66 | $item = new ToDo('foo'); 67 | 68 | $this->SUT->insert($item); 69 | 70 | assertEquals($item, $this->SUT->find($item->getId())); 71 | 72 | $this->SUT->delete($item->getId()); 73 | 74 | assertNull($this->SUT->find($item->getId())); 75 | } 76 | 77 | public function testFullTextSearch(): void 78 | { 79 | $item1 = new ToDo('foo'); 80 | $item2 = new ToDo('bar'); 81 | $item3 = new ToDo('bar baz'); 82 | 83 | $this->SUT->insert($item1); 84 | $this->SUT->insert($item2); 85 | $this->SUT->insert($item3); 86 | 87 | assertEquals([$item1], $this->SUT->getAll('foo')); 88 | assertEquals([$item2, $item3], $this->SUT->getAll('bar')); 89 | assertEquals([$item3], $this->SUT->getAll('baz')); 90 | } 91 | 92 | public function testPagination(): void 93 | { 94 | $item1 = new ToDo('foo'); 95 | $item2 = new ToDo('bar'); 96 | 97 | $this->SUT->insert($item1); 98 | $this->SUT->insert($item2); 99 | 100 | assertEquals([$item1, $item2], $this->SUT->getAll()); 101 | assertEquals([$item2], $this->SUT->getAll('', $page=2, $pageSize=1)); 102 | assertEquals([], $this->SUT->getAll('', $page=2)); 103 | } 104 | 105 | public function testCountPages(): void 106 | { 107 | $item1 = new ToDo('foo'); 108 | $item2 = new ToDo('bar'); 109 | $item3 = new ToDo('bar baz'); 110 | $item4 = new ToDo('bat'); 111 | 112 | $this->SUT->insert($item1); 113 | $this->SUT->insert($item2); 114 | $this->SUT->insert($item3); 115 | $this->SUT->insert($item4); 116 | 117 | assertSame(1, $this->SUT->countPages()); 118 | assertSame(2, $this->SUT->countPages('bar', $pageSize=1)); 119 | assertSame(2, $this->SUT->countPages('', $pageSize=2)); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /test/Model/ToDoUnitTest.php: -------------------------------------------------------------------------------- 1 | getCreatedAt()); 26 | } 27 | 28 | /** 29 | * @dataProvider typeSafeInvalidDataProvider 30 | * 31 | * @param mixed $name 32 | * @param mixed $dueFor 33 | * @param mixed $doneAt 34 | * @param string[] $invalidProperties 35 | */ 36 | public function testValidation($name, $dueFor, $doneAt, array $invalidProperties): void 37 | { 38 | try { 39 | new ToDo($name, $dueFor, $doneAt); 40 | } catch (InvalidDataException $e) { 41 | } 42 | 43 | assertTrue(isset($e)); 44 | assertEquals($invalidProperties, array_keys($e->getDetails()), 'Expected invalid properties don\'t match'); 45 | } 46 | 47 | /** 48 | * @dataProvider invalidDataProvider 49 | * 50 | * @param mixed $name 51 | * @param mixed $dueFor 52 | * @param mixed $doneAt 53 | * @param string[] $invalidProperties 54 | */ 55 | public function testValidationFromArray($name, $dueFor, $doneAt, array $invalidProperties): void 56 | { 57 | try { 58 | ToDo::createFromArray(compact('name', 'dueFor', 'doneAt')); 59 | } catch (InvalidDataException $e) { 60 | } 61 | 62 | assertTrue(isset($e)); 63 | assertEquals($invalidProperties, array_keys($e->getDetails()), 'Expected invalid properties don\'t match'); 64 | } 65 | 66 | /** 67 | * @dataProvider invalidDataProvider 68 | * 69 | * @param mixed $name 70 | * @param mixed $dueFor 71 | * @param mixed $doneAt 72 | * @param string[] $invalidProperties 73 | */ 74 | public function testValidationOnUpdate($name, $dueFor, $doneAt, array $invalidProperties): void 75 | { 76 | $item = new ToDo('foo'); 77 | 78 | try { 79 | $item->updateFromArray(compact('name', 'dueFor', 'doneAt')); 80 | } catch (InvalidDataException $e) { 81 | } 82 | 83 | assertTrue(isset($e)); 84 | assertEquals($invalidProperties, array_keys($e->getDetails()), 'Expected invalid properties don\'t match'); 85 | } 86 | 87 | /** 88 | * @return array[] 89 | */ 90 | public function invalidDataProvider(): array 91 | { 92 | return [ 93 | [ '', '', '', [ 'name', 'dueFor', 'doneAt' ] ], 94 | [ '', $this->createDateTimeString(), $this->createDateTimeString(), [ 'name' ] ], 95 | [ 'foo', '', $this->createDateTimeString(), [ 'dueFor' ] ], 96 | [ 'foo', '1970-01-01', $this->createDateTimeString(), [ 'dueFor' ] ], 97 | [ 'foo', $this->createDateTimeString(), '', [ 'doneAt' ] ], 98 | [ 'foo', $this->createDateTimeString(), '1970-01-01', [ 'doneAt' ] ], 99 | ]; 100 | } 101 | 102 | /** 103 | * @return array[] 104 | */ 105 | public function typeSafeInvalidDataProvider(): array 106 | { 107 | return [ 108 | [ '', new DateTimeImmutable('@0'), new DateTimeImmutable('@0'), [ 'name'] ], 109 | [ ' ', new DateTimeImmutable('@0'), new DateTimeImmutable('@0'), [ 'name'] ], 110 | ]; 111 | } 112 | 113 | public function testMarkDone(): void 114 | { 115 | $createdAt = new DateTimeImmutable('@0'); 116 | now(new FrozenClock($createdAt)); 117 | $item = new ToDo('foo'); 118 | 119 | $doneAt = new DateTimeImmutable('@1'); 120 | now(new FrozenClock($doneAt)); 121 | $item->markDone(); 122 | assertEquals($doneAt, $item->getDoneAt()); 123 | } 124 | 125 | private function createDateTimeString(int $timestamp = 0): string 126 | { 127 | return (new DateTimeImmutable("@$timestamp"))->format(DATE_FORMAT); 128 | } 129 | } 130 | --------------------------------------------------------------------------------