diff --git a/.coveralls.yml b/.coveralls.yml index ddb84f67..2fcbcc66 100644 --- a/.coveralls.yml +++ b/.coveralls.yml @@ -1,4 +1,4 @@ service_name: travis-ci coverage_clover: tests/_output/coverage.xml -json_path: tests/_output/unit.json \ No newline at end of file +json_path: tests/_output/unit.json diff --git a/.gitignore b/.gitignore index 087c1786..b26078e7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,11 @@ -/vendor +composer.lock + +/tests/config/*.local.php + +/tests/runtime/cache/ +!/tests/runtime/cache/.gitkeep + /tests/runtime/web/assets/ !/tests/runtime/web/assets/.gitkeep + +/vendor diff --git a/.travis.yml b/.travis.yml index ba523905..dbee4ae9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,9 @@ dist: trusty # faster builds on new travis setup not using sudo sudo: false +services: + - mysql + # cache vendor dirs cache: directories: @@ -18,13 +21,21 @@ cache: env: matrix: - - COMPOSER_OPTIONS="--prefer-lowest --prefer-stable" - - COMPOSER_OPTIONS="" + - COMPOSER_OPTIONS="--prefer-lowest --prefer-stable" + - COMPOSER_OPTIONS="" + global: + - DB_TEST_DSN="mysql:host=localhost;dbname=database" + - DB_USERNAME=travis + - DB_PASSWORD= + +before_install: + - mysql -e 'CREATE DATABASE IF NOT EXISTS `database`;' install: - travis_retry composer self-update && composer --version - export PATH="$HOME/.composer/vendor/bin:$PATH" -- travis_retry composer install --prefer-dist --no-interaction +- travis_retry composer update --prefer-dist --no-interaction $COMPOSER_OPTIONS +- php tests/bin/yii migrate --interactive=0 script: - sh ./phpcs.sh diff --git a/README.md b/README.md index 52541793..986624cd 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,13 @@ DDD Classes for Yii 2.0 ======================= Classes for a Domain-Driven Design inspired workflow with Yii 2.0 Framework +Summary +-------- + - develop with DDD metodology, but you can use ActiveRecord classes + - decouple business logic from ActiveRecord models into an Entity class + - decouple database queries into a repository class + - encapsulate business logic for different scenarios into a dedicated form and a service class + Installation ------------ @@ -21,72 +28,170 @@ to the require section of your `composer.json` file. Usage ----- +`TL;DR; check the tests folder for live examples` + Lets see an example with a standard `App` model which is an implementation of `\yii\db\ActiveRecord` and generated via `gii`. -I recommend to do not make any modification with this class, but make it `abstract` to prevent direct usages. +I recommend to do not make any modification with this class. -Then create a business model which extends our abstract active record class and implements `BusinessObject` interface. +Then create an entity which extends `\albertborsos\ddd\models\AbstractEntity` class and implements `\albertborsos\ddd\interfaces\EntityInterface` interface. Every business logic will be implemented in this class. ```php CustomerLanguage::class, + ]; + } } ``` +Now this class is fully decoupled from the underlying storage. + +For every entity you need to define a repository which handles the communication between the application and the storage. +For `ActiveRecord` usage you can use the `AbstractActiveRecordRepository` class, which has fully functional methods and you only need to implement the following way: + +```php + [ + 'definitions' => [ + \application\domains\customer\interfaces\CustomerActiveRepositoryInterface::class => \application\domains\customer\mysql\CustomerActiveRepository::class, + \application\domains\customer\interfaces\CustomerLanguageActiveRepositoryInterface::class => \application\domains\customer\mysql\CustomerLanguageActiveRepository::class, + ], + ], + + ... + +]; +``` + #### Lets create a new record! We will need a new `FormObject` which will be responsible for the data validation. And we will need a `service` model, which handles the business logic with the related models too. - A simple example for a `FormObject`: ```php ['in', 'range' => ['en', 'de', 'hu']]], + [['name'], 'trim'], + [['name'], 'default'], + [['name'], 'required'], + [['contactLanguages'], 'each', 'rule' => ['in', 'range' => ['en', 'de', 'hu']]], ]; } } ``` -And a simple example for a `service`. Services are expecting that the values in the `FormObject` are valid values. +Services are expecting that the values in the `FormObject` are valid values. That is why it is just store the values. The validation will be handled in the controller. ```php load($this->getForm()->attributes, ''); + /** @var CreateCustomerForm $form */ + $form = $this->getForm(); - if ($model->save()) { - $this->assignLanguages($model->id, $this->getForm()->languages); - $this->setId($model->id); + /** @var Customer $entity */ + $entity = $this->getRepository()->hydrate([]); + $entity->setAttributes($form->attributes, false); - return true; - } - } catch(\yii\db\Exception $e) { - $this->getForm()->addErrors(['exception' => $e->getMessage()]); + if ($this->getRepository()->insert($entity)) { + $this->setId($entity->id); + return true; } + + $form->addErrors($entity->getErrors()); + return false; } - private function assignLanguages($appId, $languageIds) + private function assignLanguages($customerId, $languageIds) { foreach ($languageIds as $languageId) { - $form = new CreateAppLanguageForm([ - 'app_id' => $appId, + $form = new CreateCustomerLanguageForm([ + 'customerId' => $customerId, 'language_id' => $languageId, ]); if ($form->validate() === false) { - throw new Exception('Unable to validate language for this app'); + throw new Exception('Unable to validate language for this customer'); } - $service = new CreateAppLanguageService($form); + $service = new CreateCustomerLanguageService($form); if ($service->execute() === false) { - throw new Exception('Unable to save language for this app'); + throw new Exception('Unable to save language for this customer'); } } } @@ -133,7 +239,7 @@ class CreateAppService extends AbstractService ``` -And this is how you can use it in the controller +And this is how you can use it in a web controller: ```php load(Yii::$app->request->post()) && $form->validate()) { - $service = new CreateAppService($form); + $service = new CreateCustomerService($form); if ($service->execute()) { - AlertWidget::addSuccess('App created successfully!'); + AlertWidget::addSuccess('Customer created successfully!'); return $this->redirect(['view', 'id' => $service->getId()]); } } @@ -163,5 +269,49 @@ class AppController extends \yii\web\Controller ]); } } +``` + +And this is how you can use it in a REST controller: + +```php + IndexAction::class, + 'view' => ViewAction::class, + 'create' => [ + 'class' => CreateAction::class, + 'formClass' => CreateCustomerForm::class, + 'serviceClass' => CreateCustomerService::class, + ], + 'update' => [ + 'class' => UpdateAction::class, + 'formClass' => UpdateCustomerForm::class, + 'serviceClass' => UpdateCustomerService::class, + ], + 'delete' => [ + 'class' => DeleteAction::class, + 'formClass' => DeleteCustomerForm::class, + 'serviceClass' => DeleteCustomerService::class, + ], + 'options' => OptionsAction::class, + ]; + } +} ``` diff --git a/UPGRADE.md b/UPGRADE.md new file mode 100644 index 00000000..5ce8241d --- /dev/null +++ b/UPGRADE.md @@ -0,0 +1,5 @@ +Upgrading from 1.0.0 +==================== + + - use `\albertborsos\ddd\interfaces\EntityInterface` instead of `\albertborsos\ddd\interfaces\BusinessObject` + diff --git a/codeception.yml b/codeception.yml index c3f75f78..8ccf62ec 100644 --- a/codeception.yml +++ b/codeception.yml @@ -5,12 +5,16 @@ paths: support: tests/_support envs: tests/_envs actor_suffix: Tester -settings: - bootstrap: _bootstrap.php extensions: enabled: - Codeception\Extension\RunFailed +modules: + enabled: + - Yii2: + configFile: 'tests/config/main.php' coverage: enabled: true include: - src/* + exclude: + - src/web/* diff --git a/composer.json b/composer.json index 6baa68fc..ae4dff03 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "albertborsos/yii2-ddd", "description": "Classes for Domain-Driven Development with Yii 2.0 Framework", - "type": "library", + "type": "yii2-extension", "keywords": ["yii2","extension"], "license": "MIT", "authors": [ @@ -12,21 +12,36 @@ ], "require": { "php": ">=7.1.0", - "yiisoft/yii2": "~2.0.0" + "yiisoft/yii2": "~2.0.14", + "samdark/hydrator": "^1.0.2" }, "require-dev": { - "codeception/base": "~3.0", + "codeception/codeception": "~3.0", "codeception/verify": "~1.0", "codeception/specify": "~1.0", "codeception/mockery-module": "^0.3.0", - "satooshi/php-coveralls": "^2.1", - "mito/yii2-coding-standards": "~2.0.0@beta" + "mockery/mockery": "^0.9.9 || ^1.0.0", + "phpunit/phpunit": "^7.0.0", + "hoa/file": "~1.0", + "hoa/consistency": "^1.17.05.02", + "squizlabs/php_codesniffer": "~2.8", + "php-coveralls/php-coveralls": "^2.1", + "mito/yii2-coding-standards": "~2.0.0-beta16" + }, + "suggest": { + "albertborsos/yii2-rest": "If you want to easily use DDD forms and services in REST API endpoints." + }, + "extra": { + "bootstrap": "albertborsos\\ddd\\Bootstrap" }, "autoload": { "psr-4": { "albertborsos\\ddd\\": "src/", "albertborsos\\ddd\\tests\\": "tests/", - "albertborsos\\ddd\\tests\\support\\base\\": "tests/_support/base/" + "albertborsos\\ddd\\tests\\support\\base\\": "tests/_support/base/", + "albertborsos\\ddd\\tests\\support\\base\\domains\\": "tests/_support/base/domains", + "albertborsos\\ddd\\tests\\support\\base\\modules\\": "tests/_support/base/modules", + "albertborsos\\ddd\\tests\\support\\base\\services\\": "tests/_support/base/services" } }, "repositories": [ diff --git a/composer.lock b/composer.lock deleted file mode 100644 index 48558e3b..00000000 --- a/composer.lock +++ /dev/null @@ -1,4465 +0,0 @@ -{ - "_readme": [ - "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", - "This file is @generated automatically" - ], - "content-hash": "4bc534048e49c307bcac3f95a0b50611", - "packages": [ - { - "name": "bower-asset/inputmask", - "version": "3.3.11", - "source": { - "type": "git", - "url": "https://github.com/RobinHerbots/Inputmask.git", - "reference": "5e670ad62f50c738388d4dcec78d2888505ad77b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/RobinHerbots/Inputmask/zipball/5e670ad62f50c738388d4dcec78d2888505ad77b", - "reference": "5e670ad62f50c738388d4dcec78d2888505ad77b" - }, - "require": { - "bower-asset/jquery": ">=1.7" - }, - "type": "bower-asset", - "license": [ - "http://opensource.org/licenses/mit-license.php" - ] - }, - { - "name": "bower-asset/jquery", - "version": "3.4.1", - "source": { - "type": "git", - "url": "https://github.com/jquery/jquery-dist.git", - "reference": "15bc73803f76bc53b654b9fdbbbc096f56d7c03d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/jquery/jquery-dist/zipball/15bc73803f76bc53b654b9fdbbbc096f56d7c03d", - "reference": "15bc73803f76bc53b654b9fdbbbc096f56d7c03d" - }, - "type": "bower-asset", - "license": [ - "MIT" - ] - }, - { - "name": "bower-asset/punycode", - "version": "v1.3.2", - "source": { - "type": "git", - "url": "git@github.com:bestiejs/punycode.js.git", - "reference": "38c8d3131a82567bfef18da09f7f4db68c84f8a3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/bestiejs/punycode.js/zipball/38c8d3131a82567bfef18da09f7f4db68c84f8a3", - "reference": "38c8d3131a82567bfef18da09f7f4db68c84f8a3" - }, - "type": "bower-asset" - }, - { - "name": "bower-asset/yii2-pjax", - "version": "2.0.7.1", - "source": { - "type": "git", - "url": "git@github.com:yiisoft/jquery-pjax.git", - "reference": "aef7b953107264f00234902a3880eb50dafc48be" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/yiisoft/jquery-pjax/zipball/aef7b953107264f00234902a3880eb50dafc48be", - "reference": "aef7b953107264f00234902a3880eb50dafc48be" - }, - "require": { - "bower-asset/jquery": ">=1.8" - }, - "type": "bower-asset", - "license": [ - "MIT" - ] - }, - { - "name": "cebe/markdown", - "version": "1.2.1", - "source": { - "type": "git", - "url": "https://github.com/cebe/markdown.git", - "reference": "9bac5e971dd391e2802dca5400bbeacbaea9eb86" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/cebe/markdown/zipball/9bac5e971dd391e2802dca5400bbeacbaea9eb86", - "reference": "9bac5e971dd391e2802dca5400bbeacbaea9eb86", - "shasum": "" - }, - "require": { - "lib-pcre": "*", - "php": ">=5.4.0" - }, - "require-dev": { - "cebe/indent": "*", - "facebook/xhprof": "*@dev", - "phpunit/phpunit": "4.1.*" - }, - "bin": [ - "bin/markdown" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.2.x-dev" - } - }, - "autoload": { - "psr-4": { - "cebe\\markdown\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Carsten Brandt", - "email": "mail@cebe.cc", - "homepage": "http://cebe.cc/", - "role": "Creator" - } - ], - "description": "A super fast, highly extensible markdown parser for PHP", - "homepage": "https://github.com/cebe/markdown#readme", - "keywords": [ - "extensible", - "fast", - "gfm", - "markdown", - "markdown-extra" - ], - "time": "2018-03-26T11:24:36+00:00" - }, - { - "name": "ezyang/htmlpurifier", - "version": "v4.10.0", - "source": { - "type": "git", - "url": "https://github.com/ezyang/htmlpurifier.git", - "reference": "d85d39da4576a6934b72480be6978fb10c860021" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/d85d39da4576a6934b72480be6978fb10c860021", - "reference": "d85d39da4576a6934b72480be6978fb10c860021", - "shasum": "" - }, - "require": { - "php": ">=5.2" - }, - "require-dev": { - "simpletest/simpletest": "^1.1" - }, - "type": "library", - "autoload": { - "psr-0": { - "HTMLPurifier": "library/" - }, - "files": [ - "library/HTMLPurifier.composer.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "LGPL" - ], - "authors": [ - { - "name": "Edward Z. Yang", - "email": "admin@htmlpurifier.org", - "homepage": "http://ezyang.com" - } - ], - "description": "Standards compliant HTML filter written in PHP", - "homepage": "http://htmlpurifier.org/", - "keywords": [ - "html" - ], - "time": "2018-02-23T01:58:20+00:00" - }, - { - "name": "yiisoft/yii2", - "version": "2.0.21", - "source": { - "type": "git", - "url": "https://github.com/yiisoft/yii2-framework.git", - "reference": "7e4622252c5f272373e1339a47d331e4a55e9591" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/yiisoft/yii2-framework/zipball/7e4622252c5f272373e1339a47d331e4a55e9591", - "reference": "7e4622252c5f272373e1339a47d331e4a55e9591", - "shasum": "" - }, - "require": { - "bower-asset/inputmask": "~3.2.2 | ~3.3.5", - "bower-asset/jquery": "3.4.*@stable | 3.3.*@stable | 3.2.*@stable | 3.1.*@stable | 2.2.*@stable | 2.1.*@stable | 1.11.*@stable | 1.12.*@stable", - "bower-asset/punycode": "1.3.*", - "bower-asset/yii2-pjax": "~2.0.1", - "cebe/markdown": "~1.0.0 | ~1.1.0 | ~1.2.0", - "ext-ctype": "*", - "ext-mbstring": "*", - "ezyang/htmlpurifier": "~4.6", - "lib-pcre": "*", - "php": ">=5.4.0", - "yiisoft/yii2-composer": "~2.0.4" - }, - "bin": [ - "yii" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "yii\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Qiang Xue", - "email": "qiang.xue@gmail.com", - "homepage": "http://www.yiiframework.com/", - "role": "Founder and project lead" - }, - { - "name": "Alexander Makarov", - "email": "sam@rmcreative.ru", - "homepage": "http://rmcreative.ru/", - "role": "Core framework development" - }, - { - "name": "Maurizio Domba", - "homepage": "http://mdomba.info/", - "role": "Core framework development" - }, - { - "name": "Carsten Brandt", - "email": "mail@cebe.cc", - "homepage": "http://cebe.cc/", - "role": "Core framework development" - }, - { - "name": "Timur Ruziev", - "email": "resurtm@gmail.com", - "homepage": "http://resurtm.com/", - "role": "Core framework development" - }, - { - "name": "Paul Klimov", - "email": "klimov.paul@gmail.com", - "role": "Core framework development" - }, - { - "name": "Dmitry Naumenko", - "email": "d.naumenko.a@gmail.com", - "role": "Core framework development" - }, - { - "name": "Boudewijn Vahrmeijer", - "email": "info@dynasource.eu", - "homepage": "http://dynasource.eu", - "role": "Core framework development" - } - ], - "description": "Yii PHP Framework Version 2", - "homepage": "http://www.yiiframework.com/", - "keywords": [ - "framework", - "yii2" - ], - "time": "2019-06-18T14:25:08+00:00" - }, - { - "name": "yiisoft/yii2-composer", - "version": "2.0.7", - "source": { - "type": "git", - "url": "https://github.com/yiisoft/yii2-composer.git", - "reference": "1439e78be1218c492e6cde251ed87d3f128b9534" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/yiisoft/yii2-composer/zipball/1439e78be1218c492e6cde251ed87d3f128b9534", - "reference": "1439e78be1218c492e6cde251ed87d3f128b9534", - "shasum": "" - }, - "require": { - "composer-plugin-api": "^1.0" - }, - "require-dev": { - "composer/composer": "^1.0" - }, - "type": "composer-plugin", - "extra": { - "class": "yii\\composer\\Plugin", - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "yii\\composer\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Qiang Xue", - "email": "qiang.xue@gmail.com" - }, - { - "name": "Carsten Brandt", - "email": "mail@cebe.cc" - } - ], - "description": "The composer plugin for Yii extension installer", - "keywords": [ - "composer", - "extension installer", - "yii2" - ], - "time": "2018-07-05T15:44:47+00:00" - } - ], - "packages-dev": [ - { - "name": "behat/gherkin", - "version": "v4.6.0", - "source": { - "type": "git", - "url": "https://github.com/Behat/Gherkin.git", - "reference": "ab0a02ea14893860bca00f225f5621d351a3ad07" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Behat/Gherkin/zipball/ab0a02ea14893860bca00f225f5621d351a3ad07", - "reference": "ab0a02ea14893860bca00f225f5621d351a3ad07", - "shasum": "" - }, - "require": { - "php": ">=5.3.1" - }, - "require-dev": { - "phpunit/phpunit": "~4.5|~5", - "symfony/phpunit-bridge": "~2.7|~3|~4", - "symfony/yaml": "~2.3|~3|~4" - }, - "suggest": { - "symfony/yaml": "If you want to parse features, represented in YAML files" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.4-dev" - } - }, - "autoload": { - "psr-0": { - "Behat\\Gherkin": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Konstantin Kudryashov", - "email": "ever.zet@gmail.com", - "homepage": "http://everzet.com" - } - ], - "description": "Gherkin DSL parser for PHP 5.3", - "homepage": "http://behat.org/", - "keywords": [ - "BDD", - "Behat", - "Cucumber", - "DSL", - "gherkin", - "parser" - ], - "time": "2019-01-16T14:22:17+00:00" - }, - { - "name": "codeception/base", - "version": "3.0.0", - "source": { - "type": "git", - "url": "https://github.com/Codeception/base.git", - "reference": "86f10d5dcb05895e76711e6d25e5eb8ead354a09" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Codeception/base/zipball/86f10d5dcb05895e76711e6d25e5eb8ead354a09", - "reference": "86f10d5dcb05895e76711e6d25e5eb8ead354a09", - "shasum": "" - }, - "require": { - "behat/gherkin": "^4.4.0", - "codeception/phpunit-wrapper": ">6.0.15 <6.1.0 | ^6.6.1 | ^7.7.1 | ^8.0.3", - "codeception/stub": "^2.0", - "ext-curl": "*", - "ext-json": "*", - "ext-mbstring": "*", - "guzzlehttp/psr7": "~1.4", - "hoa/console": "~3.0", - "php": ">=5.6.0 <8.0", - "symfony/browser-kit": ">=2.7 <5.0", - "symfony/console": ">=2.7 <5.0", - "symfony/css-selector": ">=2.7 <5.0", - "symfony/dom-crawler": ">=2.7 <5.0", - "symfony/event-dispatcher": ">=2.7 <5.0", - "symfony/finder": ">=2.7 <5.0", - "symfony/yaml": ">=2.7 <5.0" - }, - "require-dev": { - "codeception/specify": "~0.3", - "flow/jsonpath": "~0.2", - "monolog/monolog": "~1.8", - "pda/pheanstalk": "~3.0", - "php-amqplib/php-amqplib": "~2.4", - "predis/predis": "^1.0", - "squizlabs/php_codesniffer": "~2.0", - "symfony/process": ">=2.7 <5.0", - "vlucas/phpdotenv": "^3.0" - }, - "suggest": { - "aws/aws-sdk-php": "For using AWS Auth in REST module and Queue module", - "codeception/phpbuiltinserver": "Start and stop PHP built-in web server for your tests", - "codeception/specify": "BDD-style code blocks", - "codeception/verify": "BDD-style assertions", - "flow/jsonpath": "For using JSONPath in REST module", - "league/factory-muffin": "For DataFactory module", - "league/factory-muffin-faker": "For Faker support in DataFactory module", - "phpseclib/phpseclib": "for SFTP option in FTP Module", - "stecman/symfony-console-completion": "For BASH autocompletion", - "symfony/phpunit-bridge": "For phpunit-bridge support" - }, - "bin": [ - "codecept" - ], - "type": "library", - "extra": { - "branch-alias": [] - }, - "autoload": { - "psr-4": { - "Codeception\\": "src/Codeception", - "Codeception\\Extension\\": "ext" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Bodnarchuk", - "email": "davert@mail.ua", - "homepage": "http://codegyre.com" - } - ], - "description": "BDD-style testing framework", - "homepage": "http://codeception.com/", - "keywords": [ - "BDD", - "TDD", - "acceptance testing", - "functional testing", - "unit testing" - ], - "time": "2019-04-24T12:13:51+00:00" - }, - { - "name": "codeception/codeception", - "version": "3.0.1", - "source": { - "type": "git", - "url": "https://github.com/Codeception/Codeception.git", - "reference": "52dfbb5f31b74d042100a8836bbde792326ebb64" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Codeception/Codeception/zipball/52dfbb5f31b74d042100a8836bbde792326ebb64", - "reference": "52dfbb5f31b74d042100a8836bbde792326ebb64", - "shasum": "" - }, - "require": { - "behat/gherkin": "^4.4.0", - "codeception/phpunit-wrapper": ">6.0.15 <6.1.0 | ^6.6.1 | ^7.7.1 | ^8.0.3", - "codeception/stub": "^2.0", - "ext-curl": "*", - "ext-json": "*", - "ext-mbstring": "*", - "facebook/webdriver": "^1.6.0", - "guzzlehttp/guzzle": "^6.3.0", - "guzzlehttp/psr7": "~1.4", - "hoa/console": "~3.0", - "php": ">=5.6.0 <8.0", - "symfony/browser-kit": ">=2.7 <5.0", - "symfony/console": ">=2.7 <5.0", - "symfony/css-selector": ">=2.7 <5.0", - "symfony/dom-crawler": ">=2.7 <5.0", - "symfony/event-dispatcher": ">=2.7 <5.0", - "symfony/finder": ">=2.7 <5.0", - "symfony/yaml": ">=2.7 <5.0" - }, - "require-dev": { - "codeception/specify": "~0.3", - "flow/jsonpath": "~0.2", - "monolog/monolog": "~1.8", - "pda/pheanstalk": "~3.0", - "php-amqplib/php-amqplib": "~2.4", - "predis/predis": "^1.0", - "squizlabs/php_codesniffer": "~2.0", - "symfony/process": ">=2.7 <5.0", - "vlucas/phpdotenv": "^3.0" - }, - "suggest": { - "aws/aws-sdk-php": "For using AWS Auth in REST module and Queue module", - "codeception/phpbuiltinserver": "Start and stop PHP built-in web server for your tests", - "codeception/specify": "BDD-style code blocks", - "codeception/verify": "BDD-style assertions", - "flow/jsonpath": "For using JSONPath in REST module", - "league/factory-muffin": "For DataFactory module", - "league/factory-muffin-faker": "For Faker support in DataFactory module", - "phpseclib/phpseclib": "for SFTP option in FTP Module", - "stecman/symfony-console-completion": "For BASH autocompletion", - "symfony/phpunit-bridge": "For phpunit-bridge support" - }, - "bin": [ - "codecept" - ], - "type": "library", - "extra": { - "branch-alias": [] - }, - "autoload": { - "psr-4": { - "Codeception\\": "src/Codeception", - "Codeception\\Extension\\": "ext" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Bodnarchuk", - "email": "davert@mail.ua", - "homepage": "http://codegyre.com" - } - ], - "description": "BDD-style testing framework", - "homepage": "http://codeception.com/", - "keywords": [ - "BDD", - "TDD", - "acceptance testing", - "functional testing", - "unit testing" - ], - "time": "2019-05-20T17:02:37+00:00" - }, - { - "name": "codeception/mockery-module", - "version": "0.3.0", - "source": { - "type": "git", - "url": "https://github.com/Codeception/MockeryModule.git", - "reference": "19cc51ae4123031d7c93c56a9abbde40697dbc24" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Codeception/MockeryModule/zipball/19cc51ae4123031d7c93c56a9abbde40697dbc24", - "reference": "19cc51ae4123031d7c93c56a9abbde40697dbc24", - "shasum": "" - }, - "require": { - "codeception/codeception": "^2.0|^3.0", - "mockery/mockery": "^0.8|^0.9|^1.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Codeception\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Bodnarchuk", - "email": "davert.php@mailican.com" - }, - { - "name": "Jáchym Toušek", - "email": "enumag@gmail.com" - } - ], - "description": "Mockery Module for Codeception", - "time": "2019-04-26T19:47:46+00:00" - }, - { - "name": "codeception/phpunit-wrapper", - "version": "7.7.1", - "source": { - "type": "git", - "url": "https://github.com/Codeception/phpunit-wrapper.git", - "reference": "ab04a956264291505ea84998f43cf91639b4575d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Codeception/phpunit-wrapper/zipball/ab04a956264291505ea84998f43cf91639b4575d", - "reference": "ab04a956264291505ea84998f43cf91639b4575d", - "shasum": "" - }, - "require": { - "phpunit/php-code-coverage": "^6.0", - "phpunit/phpunit": "7.5.*", - "sebastian/comparator": "^3.0", - "sebastian/diff": "^3.0" - }, - "require-dev": { - "codeception/specify": "*", - "vlucas/phpdotenv": "^3.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Codeception\\PHPUnit\\": "src\\" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Davert", - "email": "davert.php@resend.cc" - } - ], - "description": "PHPUnit classes used by Codeception", - "time": "2019-02-26T20:35:32+00:00" - }, - { - "name": "codeception/specify", - "version": "1.1", - "source": { - "type": "git", - "url": "https://github.com/Codeception/Specify.git", - "reference": "504ac7a882e6f7226b0cff44c72a6c0bbd0bad95" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Codeception/Specify/zipball/504ac7a882e6f7226b0cff44c72a6c0bbd0bad95", - "reference": "504ac7a882e6f7226b0cff44c72a6c0bbd0bad95", - "shasum": "" - }, - "require": { - "myclabs/deep-copy": "~1.1", - "php": ">=7.1.0", - "phpunit/phpunit": "^7.0" - }, - "type": "library", - "autoload": { - "psr-0": { - "Codeception\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Bodnarchuk", - "email": "davert@codeception.com" - } - ], - "description": "BDD code blocks for PHPUnit and Codeception", - "time": "2018-03-12T23:55:10+00:00" - }, - { - "name": "codeception/stub", - "version": "2.1.0", - "source": { - "type": "git", - "url": "https://github.com/Codeception/Stub.git", - "reference": "853657f988942f7afb69becf3fd0059f192c705a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Codeception/Stub/zipball/853657f988942f7afb69becf3fd0059f192c705a", - "reference": "853657f988942f7afb69becf3fd0059f192c705a", - "shasum": "" - }, - "require": { - "codeception/phpunit-wrapper": ">6.0.15 <6.1.0 | ^6.6.1 | ^7.7.1 | ^8.0.3" - }, - "type": "library", - "autoload": { - "psr-4": { - "Codeception\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Flexible Stub wrapper for PHPUnit's Mock Builder", - "time": "2019-03-02T15:35:10+00:00" - }, - { - "name": "codeception/verify", - "version": "1.0.0", - "source": { - "type": "git", - "url": "https://github.com/Codeception/Verify.git", - "reference": "f45b39025b3f5cfd9a9d8fb992432885ff5380c1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Codeception/Verify/zipball/f45b39025b3f5cfd9a9d8fb992432885ff5380c1", - "reference": "f45b39025b3f5cfd9a9d8fb992432885ff5380c1", - "shasum": "" - }, - "require": { - "php": ">= 7.0", - "phpunit/phpunit": "> 6.0" - }, - "type": "library", - "autoload": { - "files": [ - "src/Codeception/function.php" - ], - "psr-4": { - "Codeception\\": "src\\Codeception" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Bodnarchuk", - "email": "davert@codeception.com" - } - ], - "description": "BDD assertion library for PHPUnit", - "time": "2017-11-12T01:51:59+00:00" - }, - { - "name": "doctrine/instantiator", - "version": "1.2.0", - "source": { - "type": "git", - "url": "https://github.com/doctrine/instantiator.git", - "reference": "a2c590166b2133a4633738648b6b064edae0814a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/a2c590166b2133a4633738648b6b064edae0814a", - "reference": "a2c590166b2133a4633738648b6b064edae0814a", - "shasum": "" - }, - "require": { - "php": "^7.1" - }, - "require-dev": { - "doctrine/coding-standard": "^6.0", - "ext-pdo": "*", - "ext-phar": "*", - "phpbench/phpbench": "^0.13", - "phpstan/phpstan-phpunit": "^0.11", - "phpstan/phpstan-shim": "^0.11", - "phpunit/phpunit": "^7.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.2.x-dev" - } - }, - "autoload": { - "psr-4": { - "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Marco Pivetta", - "email": "ocramius@gmail.com", - "homepage": "http://ocramius.github.com/" - } - ], - "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", - "homepage": "https://www.doctrine-project.org/projects/instantiator.html", - "keywords": [ - "constructor", - "instantiate" - ], - "time": "2019-03-17T17:37:11+00:00" - }, - { - "name": "facebook/webdriver", - "version": "1.7.1", - "source": { - "type": "git", - "url": "https://github.com/facebook/php-webdriver.git", - "reference": "e43de70f3c7166169d0f14a374505392734160e5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/facebook/php-webdriver/zipball/e43de70f3c7166169d0f14a374505392734160e5", - "reference": "e43de70f3c7166169d0f14a374505392734160e5", - "shasum": "" - }, - "require": { - "ext-curl": "*", - "ext-json": "*", - "ext-mbstring": "*", - "ext-zip": "*", - "php": "^5.6 || ~7.0", - "symfony/process": "^2.8 || ^3.1 || ^4.0" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^2.0", - "jakub-onderka/php-parallel-lint": "^0.9.2", - "php-coveralls/php-coveralls": "^2.0", - "php-mock/php-mock-phpunit": "^1.1", - "phpunit/phpunit": "^5.7", - "sebastian/environment": "^1.3.4 || ^2.0 || ^3.0", - "squizlabs/php_codesniffer": "^2.6", - "symfony/var-dumper": "^3.3 || ^4.0" - }, - "suggest": { - "ext-SimpleXML": "For Firefox profile creation" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-community": "1.5-dev" - } - }, - "autoload": { - "psr-4": { - "Facebook\\WebDriver\\": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "Apache-2.0" - ], - "description": "A PHP client for Selenium WebDriver", - "homepage": "https://github.com/facebook/php-webdriver", - "keywords": [ - "facebook", - "php", - "selenium", - "webdriver" - ], - "time": "2019-06-13T08:02:18+00:00" - }, - { - "name": "guzzlehttp/guzzle", - "version": "6.3.3", - "source": { - "type": "git", - "url": "https://github.com/guzzle/guzzle.git", - "reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/407b0cb880ace85c9b63c5f9551db498cb2d50ba", - "reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba", - "shasum": "" - }, - "require": { - "guzzlehttp/promises": "^1.0", - "guzzlehttp/psr7": "^1.4", - "php": ">=5.5" - }, - "require-dev": { - "ext-curl": "*", - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0", - "psr/log": "^1.0" - }, - "suggest": { - "psr/log": "Required for using the Log middleware" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "6.3-dev" - } - }, - "autoload": { - "files": [ - "src/functions_include.php" - ], - "psr-4": { - "GuzzleHttp\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - } - ], - "description": "Guzzle is a PHP HTTP client library", - "homepage": "http://guzzlephp.org/", - "keywords": [ - "client", - "curl", - "framework", - "http", - "http client", - "rest", - "web service" - ], - "time": "2018-04-22T15:46:56+00:00" - }, - { - "name": "guzzlehttp/promises", - "version": "v1.3.1", - "source": { - "type": "git", - "url": "https://github.com/guzzle/promises.git", - "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/a59da6cf61d80060647ff4d3eb2c03a2bc694646", - "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646", - "shasum": "" - }, - "require": { - "php": ">=5.5.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.4-dev" - } - }, - "autoload": { - "psr-4": { - "GuzzleHttp\\Promise\\": "src/" - }, - "files": [ - "src/functions_include.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - } - ], - "description": "Guzzle promises library", - "keywords": [ - "promise" - ], - "time": "2016-12-20T10:07:11+00:00" - }, - { - "name": "guzzlehttp/psr7", - "version": "1.5.2", - "source": { - "type": "git", - "url": "https://github.com/guzzle/psr7.git", - "reference": "9f83dded91781a01c63574e387eaa769be769115" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/9f83dded91781a01c63574e387eaa769be769115", - "reference": "9f83dded91781a01c63574e387eaa769be769115", - "shasum": "" - }, - "require": { - "php": ">=5.4.0", - "psr/http-message": "~1.0", - "ralouphie/getallheaders": "^2.0.5" - }, - "provide": { - "psr/http-message-implementation": "1.0" - }, - "require-dev": { - "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.8" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.5-dev" - } - }, - "autoload": { - "psr-4": { - "GuzzleHttp\\Psr7\\": "src/" - }, - "files": [ - "src/functions_include.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - }, - { - "name": "Tobias Schultze", - "homepage": "https://github.com/Tobion" - } - ], - "description": "PSR-7 message implementation that also provides common utility methods", - "keywords": [ - "http", - "message", - "psr-7", - "request", - "response", - "stream", - "uri", - "url" - ], - "time": "2018-12-04T20:46:45+00:00" - }, - { - "name": "hamcrest/hamcrest-php", - "version": "v2.0.0", - "source": { - "type": "git", - "url": "https://github.com/hamcrest/hamcrest-php.git", - "reference": "776503d3a8e85d4f9a1148614f95b7a608b046ad" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/776503d3a8e85d4f9a1148614f95b7a608b046ad", - "reference": "776503d3a8e85d4f9a1148614f95b7a608b046ad", - "shasum": "" - }, - "require": { - "php": "^5.3|^7.0" - }, - "replace": { - "cordoval/hamcrest-php": "*", - "davedevelopment/hamcrest-php": "*", - "kodova/hamcrest-php": "*" - }, - "require-dev": { - "phpunit/php-file-iterator": "1.3.3", - "phpunit/phpunit": "~4.0", - "satooshi/php-coveralls": "^1.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "classmap": [ - "hamcrest" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD" - ], - "description": "This is the PHP port of Hamcrest Matchers", - "keywords": [ - "test" - ], - "time": "2016-01-20T08:20:44+00:00" - }, - { - "name": "hoa/consistency", - "version": "1.17.05.02", - "source": { - "type": "git", - "url": "https://github.com/hoaproject/Consistency.git", - "reference": "fd7d0adc82410507f332516faf655b6ed22e4c2f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/hoaproject/Consistency/zipball/fd7d0adc82410507f332516faf655b6ed22e4c2f", - "reference": "fd7d0adc82410507f332516faf655b6ed22e4c2f", - "shasum": "" - }, - "require": { - "hoa/exception": "~1.0", - "php": ">=5.5.0" - }, - "require-dev": { - "hoa/stream": "~1.0", - "hoa/test": "~2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Hoa\\Consistency\\": "." - }, - "files": [ - "Prelude.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Ivan Enderlin", - "email": "ivan.enderlin@hoa-project.net" - }, - { - "name": "Hoa community", - "homepage": "https://hoa-project.net/" - } - ], - "description": "The Hoa\\Consistency library.", - "homepage": "https://hoa-project.net/", - "keywords": [ - "autoloader", - "callable", - "consistency", - "entity", - "flex", - "keyword", - "library" - ], - "time": "2017-05-02T12:18:12+00:00" - }, - { - "name": "hoa/console", - "version": "3.17.05.02", - "source": { - "type": "git", - "url": "https://github.com/hoaproject/Console.git", - "reference": "e231fd3ea70e6d773576ae78de0bdc1daf331a66" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/hoaproject/Console/zipball/e231fd3ea70e6d773576ae78de0bdc1daf331a66", - "reference": "e231fd3ea70e6d773576ae78de0bdc1daf331a66", - "shasum": "" - }, - "require": { - "hoa/consistency": "~1.0", - "hoa/event": "~1.0", - "hoa/exception": "~1.0", - "hoa/file": "~1.0", - "hoa/protocol": "~1.0", - "hoa/stream": "~1.0", - "hoa/ustring": "~4.0" - }, - "require-dev": { - "hoa/test": "~2.0" - }, - "suggest": { - "ext-pcntl": "To enable hoa://Event/Console/Window:resize.", - "hoa/dispatcher": "To use the console kit.", - "hoa/router": "To use the console kit." - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - } - }, - "autoload": { - "psr-4": { - "Hoa\\Console\\": "." - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Ivan Enderlin", - "email": "ivan.enderlin@hoa-project.net" - }, - { - "name": "Hoa community", - "homepage": "https://hoa-project.net/" - } - ], - "description": "The Hoa\\Console library.", - "homepage": "https://hoa-project.net/", - "keywords": [ - "autocompletion", - "chrome", - "cli", - "console", - "cursor", - "getoption", - "library", - "option", - "parser", - "processus", - "readline", - "terminfo", - "tput", - "window" - ], - "time": "2017-05-02T12:26:19+00:00" - }, - { - "name": "hoa/event", - "version": "1.17.01.13", - "source": { - "type": "git", - "url": "https://github.com/hoaproject/Event.git", - "reference": "6c0060dced212ffa3af0e34bb46624f990b29c54" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/hoaproject/Event/zipball/6c0060dced212ffa3af0e34bb46624f990b29c54", - "reference": "6c0060dced212ffa3af0e34bb46624f990b29c54", - "shasum": "" - }, - "require": { - "hoa/consistency": "~1.0", - "hoa/exception": "~1.0" - }, - "require-dev": { - "hoa/test": "~2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Hoa\\Event\\": "." - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Ivan Enderlin", - "email": "ivan.enderlin@hoa-project.net" - }, - { - "name": "Hoa community", - "homepage": "https://hoa-project.net/" - } - ], - "description": "The Hoa\\Event library.", - "homepage": "https://hoa-project.net/", - "keywords": [ - "event", - "library", - "listener", - "observer" - ], - "time": "2017-01-13T15:30:50+00:00" - }, - { - "name": "hoa/exception", - "version": "1.17.01.16", - "source": { - "type": "git", - "url": "https://github.com/hoaproject/Exception.git", - "reference": "091727d46420a3d7468ef0595651488bfc3a458f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/hoaproject/Exception/zipball/091727d46420a3d7468ef0595651488bfc3a458f", - "reference": "091727d46420a3d7468ef0595651488bfc3a458f", - "shasum": "" - }, - "require": { - "hoa/consistency": "~1.0", - "hoa/event": "~1.0" - }, - "require-dev": { - "hoa/test": "~2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Hoa\\Exception\\": "." - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Ivan Enderlin", - "email": "ivan.enderlin@hoa-project.net" - }, - { - "name": "Hoa community", - "homepage": "https://hoa-project.net/" - } - ], - "description": "The Hoa\\Exception library.", - "homepage": "https://hoa-project.net/", - "keywords": [ - "exception", - "library" - ], - "time": "2017-01-16T07:53:27+00:00" - }, - { - "name": "hoa/file", - "version": "1.17.07.11", - "source": { - "type": "git", - "url": "https://github.com/hoaproject/File.git", - "reference": "35cb979b779bc54918d2f9a4e02ed6c7a1fa67ca" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/hoaproject/File/zipball/35cb979b779bc54918d2f9a4e02ed6c7a1fa67ca", - "reference": "35cb979b779bc54918d2f9a4e02ed6c7a1fa67ca", - "shasum": "" - }, - "require": { - "hoa/consistency": "~1.0", - "hoa/event": "~1.0", - "hoa/exception": "~1.0", - "hoa/iterator": "~2.0", - "hoa/stream": "~1.0" - }, - "require-dev": { - "hoa/test": "~2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Hoa\\File\\": "." - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Ivan Enderlin", - "email": "ivan.enderlin@hoa-project.net" - }, - { - "name": "Hoa community", - "homepage": "https://hoa-project.net/" - } - ], - "description": "The Hoa\\File library.", - "homepage": "https://hoa-project.net/", - "keywords": [ - "Socket", - "directory", - "file", - "finder", - "library", - "link", - "temporary" - ], - "time": "2017-07-11T07:42:15+00:00" - }, - { - "name": "hoa/iterator", - "version": "2.17.01.10", - "source": { - "type": "git", - "url": "https://github.com/hoaproject/Iterator.git", - "reference": "d1120ba09cb4ccd049c86d10058ab94af245f0cc" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/hoaproject/Iterator/zipball/d1120ba09cb4ccd049c86d10058ab94af245f0cc", - "reference": "d1120ba09cb4ccd049c86d10058ab94af245f0cc", - "shasum": "" - }, - "require": { - "hoa/consistency": "~1.0", - "hoa/exception": "~1.0" - }, - "require-dev": { - "hoa/test": "~2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.x-dev" - } - }, - "autoload": { - "psr-4": { - "Hoa\\Iterator\\": "." - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Ivan Enderlin", - "email": "ivan.enderlin@hoa-project.net" - }, - { - "name": "Hoa community", - "homepage": "https://hoa-project.net/" - } - ], - "description": "The Hoa\\Iterator library.", - "homepage": "https://hoa-project.net/", - "keywords": [ - "iterator", - "library" - ], - "time": "2017-01-10T10:34:47+00:00" - }, - { - "name": "hoa/protocol", - "version": "1.17.01.14", - "source": { - "type": "git", - "url": "https://github.com/hoaproject/Protocol.git", - "reference": "5c2cf972151c45f373230da170ea015deecf19e2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/hoaproject/Protocol/zipball/5c2cf972151c45f373230da170ea015deecf19e2", - "reference": "5c2cf972151c45f373230da170ea015deecf19e2", - "shasum": "" - }, - "require": { - "hoa/consistency": "~1.0", - "hoa/exception": "~1.0" - }, - "require-dev": { - "hoa/test": "~2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Hoa\\Protocol\\": "." - }, - "files": [ - "Wrapper.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Ivan Enderlin", - "email": "ivan.enderlin@hoa-project.net" - }, - { - "name": "Hoa community", - "homepage": "https://hoa-project.net/" - } - ], - "description": "The Hoa\\Protocol library.", - "homepage": "https://hoa-project.net/", - "keywords": [ - "library", - "protocol", - "resource", - "stream", - "wrapper" - ], - "time": "2017-01-14T12:26:10+00:00" - }, - { - "name": "hoa/stream", - "version": "1.17.02.21", - "source": { - "type": "git", - "url": "https://github.com/hoaproject/Stream.git", - "reference": "3293cfffca2de10525df51436adf88a559151d82" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/hoaproject/Stream/zipball/3293cfffca2de10525df51436adf88a559151d82", - "reference": "3293cfffca2de10525df51436adf88a559151d82", - "shasum": "" - }, - "require": { - "hoa/consistency": "~1.0", - "hoa/event": "~1.0", - "hoa/exception": "~1.0", - "hoa/protocol": "~1.0" - }, - "require-dev": { - "hoa/test": "~2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Hoa\\Stream\\": "." - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Ivan Enderlin", - "email": "ivan.enderlin@hoa-project.net" - }, - { - "name": "Hoa community", - "homepage": "https://hoa-project.net/" - } - ], - "description": "The Hoa\\Stream library.", - "homepage": "https://hoa-project.net/", - "keywords": [ - "Context", - "bucket", - "composite", - "filter", - "in", - "library", - "out", - "protocol", - "stream", - "wrapper" - ], - "time": "2017-02-21T16:01:06+00:00" - }, - { - "name": "hoa/ustring", - "version": "4.17.01.16", - "source": { - "type": "git", - "url": "https://github.com/hoaproject/Ustring.git", - "reference": "e6326e2739178799b1fe3fdd92029f9517fa17a0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/hoaproject/Ustring/zipball/e6326e2739178799b1fe3fdd92029f9517fa17a0", - "reference": "e6326e2739178799b1fe3fdd92029f9517fa17a0", - "shasum": "" - }, - "require": { - "hoa/consistency": "~1.0", - "hoa/exception": "~1.0" - }, - "require-dev": { - "hoa/test": "~2.0" - }, - "suggest": { - "ext-iconv": "ext/iconv must be present (or a third implementation) to use Hoa\\Ustring::transcode().", - "ext-intl": "To get a better Hoa\\Ustring::toAscii() and Hoa\\Ustring::compareTo()." - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.x-dev" - } - }, - "autoload": { - "psr-4": { - "Hoa\\Ustring\\": "." - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Ivan Enderlin", - "email": "ivan.enderlin@hoa-project.net" - }, - { - "name": "Hoa community", - "homepage": "https://hoa-project.net/" - } - ], - "description": "The Hoa\\Ustring library.", - "homepage": "https://hoa-project.net/", - "keywords": [ - "library", - "search", - "string", - "unicode" - ], - "time": "2017-01-16T07:08:25+00:00" - }, - { - "name": "mito/yii2-coding-standards", - "version": "2.0.0-beta16", - "source": { - "type": "git", - "url": "https://github.com/hellowearemito/yii2-coding-standards.git", - "reference": "41f1fcc48c34722a0cb82b3259e279e580019421" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/hellowearemito/yii2-coding-standards/zipball/41f1fcc48c34722a0cb82b3259e279e580019421", - "reference": "41f1fcc48c34722a0cb82b3259e279e580019421", - "shasum": "" - }, - "require": { - "php": ">=5.4.0", - "squizlabs/php_codesniffer": "~2.7" - }, - "require-dev": { - "phpunit/phpunit": "^5.3", - "satooshi/php-coveralls": "^1.0" - }, - "type": "library", - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Qiang Xue", - "email": "qiang.xue@gmail.com", - "homepage": "http://www.yiiframework.com/", - "role": "Founder and project lead" - }, - { - "name": "Alexander Makarov", - "email": "sam@rmcreative.ru", - "homepage": "http://rmcreative.ru/", - "role": "Core framework development" - }, - { - "name": "Maurizio Domba", - "homepage": "http://mdomba.info/", - "role": "Core framework development" - }, - { - "name": "Carsten Brandt", - "email": "mail@cebe.cc", - "homepage": "http://cebe.cc/", - "role": "Core framework development" - }, - { - "name": "Timur Ruziev", - "email": "resurtm@gmail.com", - "homepage": "http://resurtm.com/", - "role": "Core framework development" - }, - { - "name": "Paul Klimov", - "email": "klimov.paul@gmail.com", - "role": "Core framework development" - }, - { - "name": "Nikola Kovacs", - "email": "nikola.kovacs@gmail.com" - } - ], - "description": "Mito Yii 2 coding standards", - "homepage": "https://mito.hu/", - "keywords": [ - "codesniffer", - "framework", - "yii" - ], - "time": "2017-11-20T15:57:26+00:00" - }, - { - "name": "mockery/mockery", - "version": "1.2.2", - "source": { - "type": "git", - "url": "https://github.com/mockery/mockery.git", - "reference": "0eb0b48c3f07b3b89f5169ce005b7d05b18cf1d2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/mockery/mockery/zipball/0eb0b48c3f07b3b89f5169ce005b7d05b18cf1d2", - "reference": "0eb0b48c3f07b3b89f5169ce005b7d05b18cf1d2", - "shasum": "" - }, - "require": { - "hamcrest/hamcrest-php": "~2.0", - "lib-pcre": ">=7.0", - "php": ">=5.6.0" - }, - "require-dev": { - "phpunit/phpunit": "~5.7.10|~6.5|~7.0|~8.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-0": { - "Mockery": "library/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Pádraic Brady", - "email": "padraic.brady@gmail.com", - "homepage": "http://blog.astrumfutura.com" - }, - { - "name": "Dave Marshall", - "email": "dave.marshall@atstsolutions.co.uk", - "homepage": "http://davedevelopment.co.uk" - } - ], - "description": "Mockery is a simple yet flexible PHP mock object framework", - "homepage": "https://github.com/mockery/mockery", - "keywords": [ - "BDD", - "TDD", - "library", - "mock", - "mock objects", - "mockery", - "stub", - "test", - "test double", - "testing" - ], - "time": "2019-02-13T09:37:52+00:00" - }, - { - "name": "myclabs/deep-copy", - "version": "1.9.1", - "source": { - "type": "git", - "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "e6828efaba2c9b79f4499dae1d66ef8bfa7b2b72" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/e6828efaba2c9b79f4499dae1d66ef8bfa7b2b72", - "reference": "e6828efaba2c9b79f4499dae1d66ef8bfa7b2b72", - "shasum": "" - }, - "require": { - "php": "^7.1" - }, - "replace": { - "myclabs/deep-copy": "self.version" - }, - "require-dev": { - "doctrine/collections": "^1.0", - "doctrine/common": "^2.6", - "phpunit/phpunit": "^7.1" - }, - "type": "library", - "autoload": { - "psr-4": { - "DeepCopy\\": "src/DeepCopy/" - }, - "files": [ - "src/DeepCopy/deep_copy.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Create deep copies (clones) of your objects", - "keywords": [ - "clone", - "copy", - "duplicate", - "object", - "object graph" - ], - "time": "2019-04-07T13:18:21+00:00" - }, - { - "name": "phar-io/manifest", - "version": "1.0.3", - "source": { - "type": "git", - "url": "https://github.com/phar-io/manifest.git", - "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", - "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-phar": "*", - "phar-io/version": "^2.0", - "php": "^5.6 || ^7.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" - }, - { - "name": "Sebastian Heuer", - "email": "sebastian@phpeople.de", - "role": "Developer" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "Developer" - } - ], - "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", - "time": "2018-07-08T19:23:20+00:00" - }, - { - "name": "phar-io/version", - "version": "2.0.1", - "source": { - "type": "git", - "url": "https://github.com/phar-io/version.git", - "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phar-io/version/zipball/45a2ec53a73c70ce41d55cedef9063630abaf1b6", - "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6", - "shasum": "" - }, - "require": { - "php": "^5.6 || ^7.0" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" - }, - { - "name": "Sebastian Heuer", - "email": "sebastian@phpeople.de", - "role": "Developer" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "Developer" - } - ], - "description": "Library for handling version information and constraints", - "time": "2018-07-08T19:19:57+00:00" - }, - { - "name": "phpdocumentor/reflection-common", - "version": "1.0.1", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", - "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", - "shasum": "" - }, - "require": { - "php": ">=5.5" - }, - "require-dev": { - "phpunit/phpunit": "^4.6" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jaap van Otterdijk", - "email": "opensource@ijaap.nl" - } - ], - "description": "Common reflection classes used by phpdocumentor to reflect the code structure", - "homepage": "http://www.phpdoc.org", - "keywords": [ - "FQSEN", - "phpDocumentor", - "phpdoc", - "reflection", - "static analysis" - ], - "time": "2017-09-11T18:02:19+00:00" - }, - { - "name": "phpdocumentor/reflection-docblock", - "version": "4.3.1", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "bdd9f737ebc2a01c06ea7ff4308ec6697db9b53c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/bdd9f737ebc2a01c06ea7ff4308ec6697db9b53c", - "reference": "bdd9f737ebc2a01c06ea7ff4308ec6697db9b53c", - "shasum": "" - }, - "require": { - "php": "^7.0", - "phpdocumentor/reflection-common": "^1.0.0", - "phpdocumentor/type-resolver": "^0.4.0", - "webmozart/assert": "^1.0" - }, - "require-dev": { - "doctrine/instantiator": "~1.0.5", - "mockery/mockery": "^1.0", - "phpunit/phpunit": "^6.4" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - } - ], - "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2019-04-30T17:48:53+00:00" - }, - { - "name": "phpdocumentor/type-resolver", - "version": "0.4.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/9c977708995954784726e25d0cd1dddf4e65b0f7", - "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7", - "shasum": "" - }, - "require": { - "php": "^5.5 || ^7.0", - "phpdocumentor/reflection-common": "^1.0" - }, - "require-dev": { - "mockery/mockery": "^0.9.4", - "phpunit/phpunit": "^5.2||^4.8.24" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - } - ], - "time": "2017-07-14T14:27:02+00:00" - }, - { - "name": "phpspec/prophecy", - "version": "1.8.1", - "source": { - "type": "git", - "url": "https://github.com/phpspec/prophecy.git", - "reference": "1927e75f4ed19131ec9bcc3b002e07fb1173ee76" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/1927e75f4ed19131ec9bcc3b002e07fb1173ee76", - "reference": "1927e75f4ed19131ec9bcc3b002e07fb1173ee76", - "shasum": "" - }, - "require": { - "doctrine/instantiator": "^1.0.2", - "php": "^5.3|^7.0", - "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0", - "sebastian/comparator": "^1.1|^2.0|^3.0", - "sebastian/recursion-context": "^1.0|^2.0|^3.0" - }, - "require-dev": { - "phpspec/phpspec": "^2.5|^3.2", - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.8.x-dev" - } - }, - "autoload": { - "psr-4": { - "Prophecy\\": "src/Prophecy" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Konstantin Kudryashov", - "email": "ever.zet@gmail.com", - "homepage": "http://everzet.com" - }, - { - "name": "Marcello Duarte", - "email": "marcello.duarte@gmail.com" - } - ], - "description": "Highly opinionated mocking framework for PHP 5.3+", - "homepage": "https://github.com/phpspec/prophecy", - "keywords": [ - "Double", - "Dummy", - "fake", - "mock", - "spy", - "stub" - ], - "time": "2019-06-13T12:50:23+00:00" - }, - { - "name": "phpunit/php-code-coverage", - "version": "6.1.4", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "807e6013b00af69b6c5d9ceb4282d0393dbb9d8d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/807e6013b00af69b6c5d9ceb4282d0393dbb9d8d", - "reference": "807e6013b00af69b6c5d9ceb4282d0393dbb9d8d", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-xmlwriter": "*", - "php": "^7.1", - "phpunit/php-file-iterator": "^2.0", - "phpunit/php-text-template": "^1.2.1", - "phpunit/php-token-stream": "^3.0", - "sebastian/code-unit-reverse-lookup": "^1.0.1", - "sebastian/environment": "^3.1 || ^4.0", - "sebastian/version": "^2.0.1", - "theseer/tokenizer": "^1.1" - }, - "require-dev": { - "phpunit/phpunit": "^7.0" - }, - "suggest": { - "ext-xdebug": "^2.6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "6.1-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", - "homepage": "https://github.com/sebastianbergmann/php-code-coverage", - "keywords": [ - "coverage", - "testing", - "xunit" - ], - "time": "2018-10-31T16:06:48+00:00" - }, - { - "name": "phpunit/php-file-iterator", - "version": "2.0.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "050bedf145a257b1ff02746c31894800e5122946" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/050bedf145a257b1ff02746c31894800e5122946", - "reference": "050bedf145a257b1ff02746c31894800e5122946", - "shasum": "" - }, - "require": { - "php": "^7.1" - }, - "require-dev": { - "phpunit/phpunit": "^7.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "FilterIterator implementation that filters files based on a list of suffixes.", - "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", - "keywords": [ - "filesystem", - "iterator" - ], - "time": "2018-09-13T20:33:42+00:00" - }, - { - "name": "phpunit/php-text-template", - "version": "1.2.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", - "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Simple template engine.", - "homepage": "https://github.com/sebastianbergmann/php-text-template/", - "keywords": [ - "template" - ], - "time": "2015-06-21T13:50:34+00:00" - }, - { - "name": "phpunit/php-timer", - "version": "2.1.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "1038454804406b0b5f5f520358e78c1c2f71501e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/1038454804406b0b5f5f520358e78c1c2f71501e", - "reference": "1038454804406b0b5f5f520358e78c1c2f71501e", - "shasum": "" - }, - "require": { - "php": "^7.1" - }, - "require-dev": { - "phpunit/phpunit": "^7.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.1-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Utility class for timing", - "homepage": "https://github.com/sebastianbergmann/php-timer/", - "keywords": [ - "timer" - ], - "time": "2019-06-07T04:22:29+00:00" - }, - { - "name": "phpunit/php-token-stream", - "version": "3.0.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "c99e3be9d3e85f60646f152f9002d46ed7770d18" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/c99e3be9d3e85f60646f152f9002d46ed7770d18", - "reference": "c99e3be9d3e85f60646f152f9002d46ed7770d18", - "shasum": "" - }, - "require": { - "ext-tokenizer": "*", - "php": "^7.1" - }, - "require-dev": { - "phpunit/phpunit": "^7.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Wrapper around PHP's tokenizer extension.", - "homepage": "https://github.com/sebastianbergmann/php-token-stream/", - "keywords": [ - "tokenizer" - ], - "time": "2018-10-30T05:52:18+00:00" - }, - { - "name": "phpunit/phpunit", - "version": "7.5.13", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "b9278591caa8630127f96c63b598712b699e671c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b9278591caa8630127f96c63b598712b699e671c", - "reference": "b9278591caa8630127f96c63b598712b699e671c", - "shasum": "" - }, - "require": { - "doctrine/instantiator": "^1.1", - "ext-dom": "*", - "ext-json": "*", - "ext-libxml": "*", - "ext-mbstring": "*", - "ext-xml": "*", - "myclabs/deep-copy": "^1.7", - "phar-io/manifest": "^1.0.2", - "phar-io/version": "^2.0", - "php": "^7.1", - "phpspec/prophecy": "^1.7", - "phpunit/php-code-coverage": "^6.0.7", - "phpunit/php-file-iterator": "^2.0.1", - "phpunit/php-text-template": "^1.2.1", - "phpunit/php-timer": "^2.1", - "sebastian/comparator": "^3.0", - "sebastian/diff": "^3.0", - "sebastian/environment": "^4.0", - "sebastian/exporter": "^3.1", - "sebastian/global-state": "^2.0", - "sebastian/object-enumerator": "^3.0.3", - "sebastian/resource-operations": "^2.0", - "sebastian/version": "^2.0.1" - }, - "conflict": { - "phpunit/phpunit-mock-objects": "*" - }, - "require-dev": { - "ext-pdo": "*" - }, - "suggest": { - "ext-soap": "*", - "ext-xdebug": "*", - "phpunit/php-invoker": "^2.0" - }, - "bin": [ - "phpunit" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "7.5-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "The PHP Unit Testing framework.", - "homepage": "https://phpunit.de/", - "keywords": [ - "phpunit", - "testing", - "xunit" - ], - "time": "2019-06-19T12:01:51+00:00" - }, - { - "name": "psr/container", - "version": "1.0.0", - "source": { - "type": "git", - "url": "https://github.com/php-fig/container.git", - "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f", - "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Container\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" - } - ], - "description": "Common Container Interface (PHP FIG PSR-11)", - "homepage": "https://github.com/php-fig/container", - "keywords": [ - "PSR-11", - "container", - "container-interface", - "container-interop", - "psr" - ], - "time": "2017-02-14T16:28:37+00:00" - }, - { - "name": "psr/http-message", - "version": "1.0.1", - "source": { - "type": "git", - "url": "https://github.com/php-fig/http-message.git", - "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", - "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Http\\Message\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" - } - ], - "description": "Common interface for HTTP messages", - "homepage": "https://github.com/php-fig/http-message", - "keywords": [ - "http", - "http-message", - "psr", - "psr-7", - "request", - "response" - ], - "time": "2016-08-06T14:39:51+00:00" - }, - { - "name": "psr/log", - "version": "1.1.0", - "source": { - "type": "git", - "url": "https://github.com/php-fig/log.git", - "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd", - "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Log\\": "Psr/Log/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" - } - ], - "description": "Common interface for logging libraries", - "homepage": "https://github.com/php-fig/log", - "keywords": [ - "log", - "psr", - "psr-3" - ], - "time": "2018-11-20T15:27:04+00:00" - }, - { - "name": "ralouphie/getallheaders", - "version": "2.0.5", - "source": { - "type": "git", - "url": "https://github.com/ralouphie/getallheaders.git", - "reference": "5601c8a83fbba7ef674a7369456d12f1e0d0eafa" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/5601c8a83fbba7ef674a7369456d12f1e0d0eafa", - "reference": "5601c8a83fbba7ef674a7369456d12f1e0d0eafa", - "shasum": "" - }, - "require": { - "php": ">=5.3" - }, - "require-dev": { - "phpunit/phpunit": "~3.7.0", - "satooshi/php-coveralls": ">=1.0" - }, - "type": "library", - "autoload": { - "files": [ - "src/getallheaders.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Ralph Khattar", - "email": "ralph.khattar@gmail.com" - } - ], - "description": "A polyfill for getallheaders.", - "time": "2016-02-11T07:05:27+00:00" - }, - { - "name": "satooshi/php-coveralls", - "version": "v2.1.0", - "source": { - "type": "git", - "url": "https://github.com/php-coveralls/php-coveralls.git", - "reference": "3b00c229726f892bfdadeaf01ea430ffd04a939d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-coveralls/php-coveralls/zipball/3b00c229726f892bfdadeaf01ea430ffd04a939d", - "reference": "3b00c229726f892bfdadeaf01ea430ffd04a939d", - "shasum": "" - }, - "require": { - "ext-json": "*", - "ext-simplexml": "*", - "guzzlehttp/guzzle": "^6.0", - "php": "^5.5 || ^7.0", - "psr/log": "^1.0", - "symfony/config": "^2.1 || ^3.0 || ^4.0", - "symfony/console": "^2.1 || ^3.0 || ^4.0", - "symfony/stopwatch": "^2.0 || ^3.0 || ^4.0", - "symfony/yaml": "^2.0 || ^3.0 || ^4.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.4.3 || ^6.0" - }, - "suggest": { - "symfony/http-kernel": "Allows Symfony integration" - }, - "bin": [ - "bin/php-coveralls" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.1-dev" - } - }, - "autoload": { - "psr-4": { - "PhpCoveralls\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Kitamura Satoshi", - "email": "with.no.parachute@gmail.com", - "homepage": "https://www.facebook.com/satooshi.jp", - "role": "Original creator" - }, - { - "name": "Takashi Matsuo", - "email": "tmatsuo@google.com" - }, - { - "name": "Google Inc" - }, - { - "name": "Dariusz Ruminski", - "email": "dariusz.ruminski@gmail.com", - "homepage": "https://github.com/keradus" - }, - { - "name": "Contributors", - "homepage": "https://github.com/php-coveralls/php-coveralls/graphs/contributors" - } - ], - "description": "PHP client library for Coveralls API", - "homepage": "https://github.com/php-coveralls/php-coveralls", - "keywords": [ - "ci", - "coverage", - "github", - "test" - ], - "abandoned": "php-coveralls/php-coveralls", - "time": "2018-05-22T23:11:08+00:00" - }, - { - "name": "sebastian/code-unit-reverse-lookup", - "version": "1.0.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", - "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", - "shasum": "" - }, - "require": { - "php": "^5.6 || ^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^5.7 || ^6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Looks up which function or method a line of code belongs to", - "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", - "time": "2017-03-04T06:30:41+00:00" - }, - { - "name": "sebastian/comparator", - "version": "3.0.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/5de4fc177adf9bce8df98d8d141a7559d7ccf6da", - "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da", - "shasum": "" - }, - "require": { - "php": "^7.1", - "sebastian/diff": "^3.0", - "sebastian/exporter": "^3.1" - }, - "require-dev": { - "phpunit/phpunit": "^7.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Volker Dusch", - "email": "github@wallbash.com" - }, - { - "name": "Bernhard Schussek", - "email": "bschussek@2bepublished.at" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides the functionality to compare PHP values for equality", - "homepage": "https://github.com/sebastianbergmann/comparator", - "keywords": [ - "comparator", - "compare", - "equality" - ], - "time": "2018-07-12T15:12:46+00:00" - }, - { - "name": "sebastian/diff", - "version": "3.0.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/720fcc7e9b5cf384ea68d9d930d480907a0c1a29", - "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29", - "shasum": "" - }, - "require": { - "php": "^7.1" - }, - "require-dev": { - "phpunit/phpunit": "^7.5 || ^8.0", - "symfony/process": "^2 || ^3.3 || ^4" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Kore Nordmann", - "email": "mail@kore-nordmann.de" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Diff implementation", - "homepage": "https://github.com/sebastianbergmann/diff", - "keywords": [ - "diff", - "udiff", - "unidiff", - "unified diff" - ], - "time": "2019-02-04T06:01:07+00:00" - }, - { - "name": "sebastian/environment", - "version": "4.2.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "f2a2c8e1c97c11ace607a7a667d73d47c19fe404" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/f2a2c8e1c97c11ace607a7a667d73d47c19fe404", - "reference": "f2a2c8e1c97c11ace607a7a667d73d47c19fe404", - "shasum": "" - }, - "require": { - "php": "^7.1" - }, - "require-dev": { - "phpunit/phpunit": "^7.5" - }, - "suggest": { - "ext-posix": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.2-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides functionality to handle HHVM/PHP environments", - "homepage": "http://www.github.com/sebastianbergmann/environment", - "keywords": [ - "Xdebug", - "environment", - "hhvm" - ], - "time": "2019-05-05T09:05:15+00:00" - }, - { - "name": "sebastian/exporter", - "version": "3.1.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "234199f4528de6d12aaa58b612e98f7d36adb937" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/234199f4528de6d12aaa58b612e98f7d36adb937", - "reference": "234199f4528de6d12aaa58b612e98f7d36adb937", - "shasum": "" - }, - "require": { - "php": "^7.0", - "sebastian/recursion-context": "^3.0" - }, - "require-dev": { - "ext-mbstring": "*", - "phpunit/phpunit": "^6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.1.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Volker Dusch", - "email": "github@wallbash.com" - }, - { - "name": "Bernhard Schussek", - "email": "bschussek@2bepublished.at" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Adam Harvey", - "email": "aharvey@php.net" - } - ], - "description": "Provides the functionality to export PHP variables for visualization", - "homepage": "http://www.github.com/sebastianbergmann/exporter", - "keywords": [ - "export", - "exporter" - ], - "time": "2017-04-03T13:19:02+00:00" - }, - { - "name": "sebastian/global-state", - "version": "2.0.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", - "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", - "shasum": "" - }, - "require": { - "php": "^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^6.0" - }, - "suggest": { - "ext-uopz": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Snapshotting of global state", - "homepage": "http://www.github.com/sebastianbergmann/global-state", - "keywords": [ - "global state" - ], - "time": "2017-04-27T15:39:26+00:00" - }, - { - "name": "sebastian/object-enumerator", - "version": "3.0.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/7cfd9e65d11ffb5af41198476395774d4c8a84c5", - "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5", - "shasum": "" - }, - "require": { - "php": "^7.0", - "sebastian/object-reflector": "^1.1.1", - "sebastian/recursion-context": "^3.0" - }, - "require-dev": { - "phpunit/phpunit": "^6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Traverses array structures and object graphs to enumerate all referenced objects", - "homepage": "https://github.com/sebastianbergmann/object-enumerator/", - "time": "2017-08-03T12:35:26+00:00" - }, - { - "name": "sebastian/object-reflector", - "version": "1.1.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "773f97c67f28de00d397be301821b06708fca0be" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/773f97c67f28de00d397be301821b06708fca0be", - "reference": "773f97c67f28de00d397be301821b06708fca0be", - "shasum": "" - }, - "require": { - "php": "^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.1-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Allows reflection of object attributes, including inherited and non-public ones", - "homepage": "https://github.com/sebastianbergmann/object-reflector/", - "time": "2017-03-29T09:07:27+00:00" - }, - { - "name": "sebastian/recursion-context", - "version": "3.0.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", - "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", - "shasum": "" - }, - "require": { - "php": "^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Adam Harvey", - "email": "aharvey@php.net" - } - ], - "description": "Provides functionality to recursively process PHP variables", - "homepage": "http://www.github.com/sebastianbergmann/recursion-context", - "time": "2017-03-03T06:23:57+00:00" - }, - { - "name": "sebastian/resource-operations", - "version": "2.0.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/4d7a795d35b889bf80a0cc04e08d77cedfa917a9", - "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9", - "shasum": "" - }, - "require": { - "php": "^7.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides a list of PHP built-in functions that operate on resources", - "homepage": "https://www.github.com/sebastianbergmann/resource-operations", - "time": "2018-10-04T04:07:39+00:00" - }, - { - "name": "sebastian/version", - "version": "2.0.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/version.git", - "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", - "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", - "shasum": "" - }, - "require": { - "php": ">=5.6" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library that helps with managing the version number of Git-hosted PHP projects", - "homepage": "https://github.com/sebastianbergmann/version", - "time": "2016-10-03T07:35:21+00:00" - }, - { - "name": "squizlabs/php_codesniffer", - "version": "2.9.2", - "source": { - "type": "git", - "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "2acf168de78487db620ab4bc524135a13cfe6745" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/2acf168de78487db620ab4bc524135a13cfe6745", - "reference": "2acf168de78487db620ab4bc524135a13cfe6745", - "shasum": "" - }, - "require": { - "ext-simplexml": "*", - "ext-tokenizer": "*", - "ext-xmlwriter": "*", - "php": ">=5.1.2" - }, - "require-dev": { - "phpunit/phpunit": "~4.0" - }, - "bin": [ - "scripts/phpcs", - "scripts/phpcbf" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.x-dev" - } - }, - "autoload": { - "classmap": [ - "CodeSniffer.php", - "CodeSniffer/CLI.php", - "CodeSniffer/Exception.php", - "CodeSniffer/File.php", - "CodeSniffer/Fixer.php", - "CodeSniffer/Report.php", - "CodeSniffer/Reporting.php", - "CodeSniffer/Sniff.php", - "CodeSniffer/Tokens.php", - "CodeSniffer/Reports/", - "CodeSniffer/Tokenizers/", - "CodeSniffer/DocGenerators/", - "CodeSniffer/Standards/AbstractPatternSniff.php", - "CodeSniffer/Standards/AbstractScopeSniff.php", - "CodeSniffer/Standards/AbstractVariableSniff.php", - "CodeSniffer/Standards/IncorrectPatternException.php", - "CodeSniffer/Standards/Generic/Sniffs/", - "CodeSniffer/Standards/MySource/Sniffs/", - "CodeSniffer/Standards/PEAR/Sniffs/", - "CodeSniffer/Standards/PSR1/Sniffs/", - "CodeSniffer/Standards/PSR2/Sniffs/", - "CodeSniffer/Standards/Squiz/Sniffs/", - "CodeSniffer/Standards/Zend/Sniffs/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Greg Sherwood", - "role": "lead" - } - ], - "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", - "homepage": "http://www.squizlabs.com/php-codesniffer", - "keywords": [ - "phpcs", - "standards" - ], - "time": "2018-11-07T22:31:41+00:00" - }, - { - "name": "symfony/browser-kit", - "version": "v4.3.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/browser-kit.git", - "reference": "e07d50e84b8cf489590f22244f4f609579b4a2c4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/browser-kit/zipball/e07d50e84b8cf489590f22244f4f609579b4a2c4", - "reference": "e07d50e84b8cf489590f22244f4f609579b4a2c4", - "shasum": "" - }, - "require": { - "php": "^7.1.3", - "symfony/dom-crawler": "~3.4|~4.0" - }, - "require-dev": { - "symfony/css-selector": "~3.4|~4.0", - "symfony/http-client": "^4.3", - "symfony/mime": "^4.3", - "symfony/process": "~3.4|~4.0" - }, - "suggest": { - "symfony/process": "" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.3-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\BrowserKit\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony BrowserKit Component", - "homepage": "https://symfony.com", - "time": "2019-05-30T16:10:05+00:00" - }, - { - "name": "symfony/config", - "version": "v4.3.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/config.git", - "reference": "6379ee07398643e09e6ed1e87d9c62dfcad7f4eb" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/6379ee07398643e09e6ed1e87d9c62dfcad7f4eb", - "reference": "6379ee07398643e09e6ed1e87d9c62dfcad7f4eb", - "shasum": "" - }, - "require": { - "php": "^7.1.3", - "symfony/filesystem": "~3.4|~4.0", - "symfony/polyfill-ctype": "~1.8" - }, - "conflict": { - "symfony/finder": "<3.4" - }, - "require-dev": { - "symfony/dependency-injection": "~3.4|~4.0", - "symfony/event-dispatcher": "~3.4|~4.0", - "symfony/finder": "~3.4|~4.0", - "symfony/messenger": "~4.1", - "symfony/yaml": "~3.4|~4.0" - }, - "suggest": { - "symfony/yaml": "To use the yaml reference dumper" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.3-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Config\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Config Component", - "homepage": "https://symfony.com", - "time": "2019-05-30T16:10:05+00:00" - }, - { - "name": "symfony/console", - "version": "v4.3.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/console.git", - "reference": "d50bbeeb0e17e6dd4124ea391eff235e932cbf64" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/d50bbeeb0e17e6dd4124ea391eff235e932cbf64", - "reference": "d50bbeeb0e17e6dd4124ea391eff235e932cbf64", - "shasum": "" - }, - "require": { - "php": "^7.1.3", - "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php73": "^1.8", - "symfony/service-contracts": "^1.1" - }, - "conflict": { - "symfony/dependency-injection": "<3.4", - "symfony/event-dispatcher": "<4.3", - "symfony/process": "<3.3" - }, - "provide": { - "psr/log-implementation": "1.0" - }, - "require-dev": { - "psr/log": "~1.0", - "symfony/config": "~3.4|~4.0", - "symfony/dependency-injection": "~3.4|~4.0", - "symfony/event-dispatcher": "^4.3", - "symfony/lock": "~3.4|~4.0", - "symfony/process": "~3.4|~4.0", - "symfony/var-dumper": "^4.3" - }, - "suggest": { - "psr/log": "For using the console logger", - "symfony/event-dispatcher": "", - "symfony/lock": "", - "symfony/process": "" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.3-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Console\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Console Component", - "homepage": "https://symfony.com", - "time": "2019-06-05T13:25:51+00:00" - }, - { - "name": "symfony/css-selector", - "version": "v4.3.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/css-selector.git", - "reference": "105c98bb0c5d8635bea056135304bd8edcc42b4d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/105c98bb0c5d8635bea056135304bd8edcc42b4d", - "reference": "105c98bb0c5d8635bea056135304bd8edcc42b4d", - "shasum": "" - }, - "require": { - "php": "^7.1.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.3-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\CssSelector\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jean-François Simon", - "email": "jeanfrancois.simon@sensiolabs.com" - }, - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony CssSelector Component", - "homepage": "https://symfony.com", - "time": "2019-01-16T21:53:39+00:00" - }, - { - "name": "symfony/dom-crawler", - "version": "v4.3.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/dom-crawler.git", - "reference": "06ee58fbc9a8130f1d35b5280e15235a0515d457" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/06ee58fbc9a8130f1d35b5280e15235a0515d457", - "reference": "06ee58fbc9a8130f1d35b5280e15235a0515d457", - "shasum": "" - }, - "require": { - "php": "^7.1.3", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-mbstring": "~1.0" - }, - "conflict": { - "masterminds/html5": "<2.6" - }, - "require-dev": { - "masterminds/html5": "^2.6", - "symfony/css-selector": "~3.4|~4.0" - }, - "suggest": { - "symfony/css-selector": "" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.3-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\DomCrawler\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony DomCrawler Component", - "homepage": "https://symfony.com", - "time": "2019-05-31T18:55:30+00:00" - }, - { - "name": "symfony/event-dispatcher", - "version": "v4.3.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "4e6c670af81c4fb0b6c08b035530a9915d0b691f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/4e6c670af81c4fb0b6c08b035530a9915d0b691f", - "reference": "4e6c670af81c4fb0b6c08b035530a9915d0b691f", - "shasum": "" - }, - "require": { - "php": "^7.1.3", - "symfony/event-dispatcher-contracts": "^1.1" - }, - "conflict": { - "symfony/dependency-injection": "<3.4" - }, - "provide": { - "psr/event-dispatcher-implementation": "1.0", - "symfony/event-dispatcher-implementation": "1.1" - }, - "require-dev": { - "psr/log": "~1.0", - "symfony/config": "~3.4|~4.0", - "symfony/dependency-injection": "~3.4|~4.0", - "symfony/expression-language": "~3.4|~4.0", - "symfony/http-foundation": "^3.4|^4.0", - "symfony/service-contracts": "^1.1", - "symfony/stopwatch": "~3.4|~4.0" - }, - "suggest": { - "symfony/dependency-injection": "", - "symfony/http-kernel": "" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.3-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\EventDispatcher\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony EventDispatcher Component", - "homepage": "https://symfony.com", - "time": "2019-05-30T16:10:05+00:00" - }, - { - "name": "symfony/event-dispatcher-contracts", - "version": "v1.1.5", - "source": { - "type": "git", - "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "c61766f4440ca687de1084a5c00b08e167a2575c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/c61766f4440ca687de1084a5c00b08e167a2575c", - "reference": "c61766f4440ca687de1084a5c00b08e167a2575c", - "shasum": "" - }, - "require": { - "php": "^7.1.3" - }, - "suggest": { - "psr/event-dispatcher": "", - "symfony/event-dispatcher-implementation": "" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.1-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Contracts\\EventDispatcher\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Generic abstractions related to dispatching event", - "homepage": "https://symfony.com", - "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" - ], - "time": "2019-06-20T06:46:26+00:00" - }, - { - "name": "symfony/filesystem", - "version": "v4.3.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/filesystem.git", - "reference": "bf2af40d738dec5e433faea7b00daa4431d0a4cf" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/bf2af40d738dec5e433faea7b00daa4431d0a4cf", - "reference": "bf2af40d738dec5e433faea7b00daa4431d0a4cf", - "shasum": "" - }, - "require": { - "php": "^7.1.3", - "symfony/polyfill-ctype": "~1.8" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.3-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Filesystem\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Filesystem Component", - "homepage": "https://symfony.com", - "time": "2019-06-03T20:27:40+00:00" - }, - { - "name": "symfony/finder", - "version": "v4.3.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/finder.git", - "reference": "b3d4f4c0e4eadfdd8b296af9ca637cfbf51d8176" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/b3d4f4c0e4eadfdd8b296af9ca637cfbf51d8176", - "reference": "b3d4f4c0e4eadfdd8b296af9ca637cfbf51d8176", - "shasum": "" - }, - "require": { - "php": "^7.1.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.3-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Finder\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Finder Component", - "homepage": "https://symfony.com", - "time": "2019-05-26T20:47:49+00:00" - }, - { - "name": "symfony/polyfill-ctype", - "version": "v1.11.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "82ebae02209c21113908c229e9883c419720738a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/82ebae02209c21113908c229e9883c419720738a", - "reference": "82ebae02209c21113908c229e9883c419720738a", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "suggest": { - "ext-ctype": "For best performance" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.11-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - }, - "files": [ - "bootstrap.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - }, - { - "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" - } - ], - "description": "Symfony polyfill for ctype functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "ctype", - "polyfill", - "portable" - ], - "time": "2019-02-06T07:57:58+00:00" - }, - { - "name": "symfony/polyfill-mbstring", - "version": "v1.11.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "fe5e94c604826c35a32fa832f35bd036b6799609" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fe5e94c604826c35a32fa832f35bd036b6799609", - "reference": "fe5e94c604826c35a32fa832f35bd036b6799609", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "suggest": { - "ext-mbstring": "For best performance" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.11-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" - }, - "files": [ - "bootstrap.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for the Mbstring extension", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "mbstring", - "polyfill", - "portable", - "shim" - ], - "time": "2019-02-06T07:57:58+00:00" - }, - { - "name": "symfony/polyfill-php73", - "version": "v1.11.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "d1fb4abcc0c47be136208ad9d68bf59f1ee17abd" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/d1fb4abcc0c47be136208ad9d68bf59f1ee17abd", - "reference": "d1fb4abcc0c47be136208ad9d68bf59f1ee17abd", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.11-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Php73\\": "" - }, - "files": [ - "bootstrap.php" - ], - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "time": "2019-02-06T07:57:58+00:00" - }, - { - "name": "symfony/process", - "version": "v4.3.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/process.git", - "reference": "856d35814cf287480465bb7a6c413bb7f5f5e69c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/856d35814cf287480465bb7a6c413bb7f5f5e69c", - "reference": "856d35814cf287480465bb7a6c413bb7f5f5e69c", - "shasum": "" - }, - "require": { - "php": "^7.1.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.3-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Process\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Process Component", - "homepage": "https://symfony.com", - "time": "2019-05-30T16:10:05+00:00" - }, - { - "name": "symfony/service-contracts", - "version": "v1.1.5", - "source": { - "type": "git", - "url": "https://github.com/symfony/service-contracts.git", - "reference": "f391a00de78ec7ec8cf5cdcdae59ec7b883edb8d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f391a00de78ec7ec8cf5cdcdae59ec7b883edb8d", - "reference": "f391a00de78ec7ec8cf5cdcdae59ec7b883edb8d", - "shasum": "" - }, - "require": { - "php": "^7.1.3", - "psr/container": "^1.0" - }, - "suggest": { - "symfony/service-implementation": "" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.1-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Contracts\\Service\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Generic abstractions related to writing services", - "homepage": "https://symfony.com", - "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" - ], - "time": "2019-06-13T11:15:36+00:00" - }, - { - "name": "symfony/stopwatch", - "version": "v4.3.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/stopwatch.git", - "reference": "6b100e9309e8979cf1978ac1778eb155c1f7d93b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/6b100e9309e8979cf1978ac1778eb155c1f7d93b", - "reference": "6b100e9309e8979cf1978ac1778eb155c1f7d93b", - "shasum": "" - }, - "require": { - "php": "^7.1.3", - "symfony/service-contracts": "^1.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.3-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Stopwatch\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Stopwatch Component", - "homepage": "https://symfony.com", - "time": "2019-05-27T08:16:38+00:00" - }, - { - "name": "symfony/yaml", - "version": "v4.3.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/yaml.git", - "reference": "c60ecf5ba842324433b46f58dc7afc4487dbab99" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/c60ecf5ba842324433b46f58dc7afc4487dbab99", - "reference": "c60ecf5ba842324433b46f58dc7afc4487dbab99", - "shasum": "" - }, - "require": { - "php": "^7.1.3", - "symfony/polyfill-ctype": "~1.8" - }, - "conflict": { - "symfony/console": "<3.4" - }, - "require-dev": { - "symfony/console": "~3.4|~4.0" - }, - "suggest": { - "symfony/console": "For validating YAML files using the lint command" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.3-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Yaml\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Yaml Component", - "homepage": "https://symfony.com", - "time": "2019-04-06T14:04:46+00:00" - }, - { - "name": "theseer/tokenizer", - "version": "1.1.3", - "source": { - "type": "git", - "url": "https://github.com/theseer/tokenizer.git", - "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/11336f6f84e16a720dae9d8e6ed5019efa85a0f9", - "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-tokenizer": "*", - "ext-xmlwriter": "*", - "php": "^7.0" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" - } - ], - "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", - "time": "2019-06-13T22:48:21+00:00" - }, - { - "name": "webmozart/assert", - "version": "1.4.0", - "source": { - "type": "git", - "url": "https://github.com/webmozart/assert.git", - "reference": "83e253c8e0be5b0257b881e1827274667c5c17a9" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/webmozart/assert/zipball/83e253c8e0be5b0257b881e1827274667c5c17a9", - "reference": "83e253c8e0be5b0257b881e1827274667c5c17a9", - "shasum": "" - }, - "require": { - "php": "^5.3.3 || ^7.0", - "symfony/polyfill-ctype": "^1.8" - }, - "require-dev": { - "phpunit/phpunit": "^4.6", - "sebastian/version": "^1.0.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.3-dev" - } - }, - "autoload": { - "psr-4": { - "Webmozart\\Assert\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" - } - ], - "description": "Assertions to validate method input/output with nice error messages.", - "keywords": [ - "assert", - "check", - "validate" - ], - "time": "2018-12-25T11:19:39+00:00" - } - ], - "aliases": [], - "minimum-stability": "stable", - "stability-flags": { - "mito/yii2-coding-standards": 10 - }, - "prefer-stable": false, - "prefer-lowest": false, - "platform": { - "php": ">=7.1.0" - }, - "platform-dev": [] -} diff --git a/phpcs.sh b/phpcs.sh index ea8c84f4..ca0ef0aa 100644 --- a/phpcs.sh +++ b/phpcs.sh @@ -1,8 +1,8 @@ #!/bin/sh -php ./vendor/bin/phpcs --standard=vendor/mito/yii2-coding-standards/Application src/interfaces && php ./vendor/bin/phpcs --standard=vendor/mito/yii2-coding-standards/Application src/models +php ./vendor/bin/phpcs --standard=vendor/mito/yii2-coding-standards/Application src SRC=$? -php ./vendor/bin/phpcs --standard=vendor/mito/yii2-coding-standards/Application -s --exclude=PSR1.Files.SideEffects,PSR1.Classes.ClassDeclaration --extensions=php tests +php ./vendor/bin/phpcs --standard=vendor/mito/yii2-coding-standards/Application -s --exclude=PSR1.Files.SideEffects,PSR1.Classes.ClassDeclaration --extensions=php --ignore=migrations/* tests TESTS=$? if [ $SRC -ne 0 ] || [ $TESTS -ne 0 ]; then diff --git a/src/Bootstrap.php b/src/Bootstrap.php new file mode 100644 index 00000000..1b419ef5 --- /dev/null +++ b/src/Bootstrap.php @@ -0,0 +1,49 @@ +setContainerDefinitions($app); + } + + /** + * Returns the DI Container definitions map in an array, with key-value pairs. + * The keys are the Interface names and the values are the class names of the desired implementation of the interface. + * + * example: + * + * ``` + * return [ + * albertborsos\ddd\interfaces\HydratorInterface::class => albertborsos\ddd\hydrators\ActiveHydrator::class, + * ]; + * ``` + * + * @return array + */ + protected static function getContainerDefinitions(): array + { + return [ + HydratorInterface::class => ActiveHydrator::class, + ]; + } +} diff --git a/src/base/EntityEvent.php b/src/base/EntityEvent.php new file mode 100644 index 00000000..414bcb1b --- /dev/null +++ b/src/base/EntityEvent.php @@ -0,0 +1,22 @@ +attributes)) { + $this->attributes = [ + EntityInterface::EVENT_BEFORE_INSERT => $this->slugAttribute, + EntityInterface::EVENT_BEFORE_UPDATE => $this->slugAttribute, + ]; + } + + if ($this->attribute === null && $this->value === null) { + throw new InvalidConfigException('Either "attribute" or "value" property must be specified.'); + } + + if ($this->ensureUnique && empty($this->repository)) { + throw new InvalidConfigException(get_called_class() . '::$repository must be set!'); + } + + if ($this->repository) { + /** @var ActiveRepositoryInterface $repository */ + $repository = \Yii::createObject($this->repository); + $this->setRepository($repository); + } + } + + /** + * @return ActiveRepositoryInterface + */ + protected function getRepository() + { + return $this->repository; + } + + /** + * @param ActiveRepositoryInterface $repository + */ + protected function setRepository(ActiveRepositoryInterface $repository): void + { + $this->repository = $repository; + } + + /** + * {@inheritdoc} + */ + protected function getValue($event) + { + if (!$this->isNewSlugNeededByEvent($event)) { + return $this->owner->{$this->slugAttribute}; + } + + if ($this->attribute !== null) { + $slugParts = []; + foreach ((array)$this->attribute as $attribute) { + $part = ArrayHelper::getValue($this->owner, $attribute); + if ($this->skipOnEmpty && $this->isEmpty($part)) { + return $this->owner->{$this->slugAttribute}; + } + $slugParts[] = $part; + } + $slug = $this->generateSlug($slugParts); + } else { + $slug = parent::getValue($event); + } + + return $this->ensureUnique ? $this->makeUnique($slug) : $slug; + } + + protected function isNewSlugNeededByEvent(EntityEvent $event) + { + if (empty($this->owner->{$this->slugAttribute})) { + return true; + } + + if ($this->immutable) { + return false; + } + + if ($this->attribute === null) { + return true; + } + + foreach ((array)$this->attribute as $attribute) { + if (in_array($attribute, array_keys($event->dirtyAttributes))) { + return true; + } + } + + return false; + } + + /** + * Checks if given slug value is unique. + * @param string $slug slug value + * @return bool whether slug is unique. + */ + protected function validateSlug($slug) + { + /* @var $validator UniqueValidator */ + /* @var $model BaseActiveRecord */ + $validator = Yii::createObject(array_merge( + [ + 'class' => UniqueValidator::className(), + ], + $this->uniqueValidator + )); + + $model = Yii::createObject($this->repository->getDataModelClass()); + $model->clearErrors(); + $model->{$this->slugAttribute} = $slug; + + $validator->validateAttribute($model, $this->slugAttribute); + return !$model->hasErrors(); + } +} diff --git a/src/behaviors/TimestampBehavior.php b/src/behaviors/TimestampBehavior.php new file mode 100644 index 00000000..057f1dfb --- /dev/null +++ b/src/behaviors/TimestampBehavior.php @@ -0,0 +1,33 @@ +setDefaultAttributes(); + parent::init(); + } + + protected function setDefaultAttributes(): void + { + if (!empty($this->attributes)) { + return; + } + + $this->attributes = [ + EntityInterface::EVENT_BEFORE_INSERT => [$this->createdAtAttribute, $this->updatedAtAttribute], + EntityInterface::EVENT_BEFORE_UPDATE => $this->updatedAtAttribute, + ]; + } +} diff --git a/src/data/ActiveEntityDataProvider.php b/src/data/ActiveEntityDataProvider.php new file mode 100644 index 00000000..3e82fcba --- /dev/null +++ b/src/data/ActiveEntityDataProvider.php @@ -0,0 +1,70 @@ + $this->entityClass, + * 'hydrator' => $this->hydrator, + * 'query' => $query, + * 'pagination' => [ + * 'pageSize' => 20, + * ], + * ]); + * ``` + * + * @package albertborsos\ddd\data + * @since 2.0.0 + */ +class ActiveEntityDataProvider extends ActiveDataProvider +{ + /** @var string */ + public $entityClass; + + /** @var HydratorInterface */ + public $hydrator; + + /** + * @throws InvalidConfigException + */ + public function init() + { + parent::init(); + if (empty($this->entityClass)) { + throw new InvalidConfigException(get_class($this) . '::$entityClass must be set.'); + } + + if (!Yii::createObject($this->entityClass) instanceof EntityInterface) { + throw new InvalidConfigException(get_class($this) . '::$entityClass must implements ' . EntityInterface::class); + } + + if (!$this->hydrator instanceof HydratorInterface) { + throw new InvalidConfigException(get_called_class() . '::$hydrator must implements `' . HydratorInterface::class . '`'); + } + } + + /** + * @return AbstractEntity[] + * @throws InvalidConfigException + */ + protected function prepareModels() + { + return $this->hydrator->hydrateAll($this->entityClass, parent::prepareModels()); + } +} diff --git a/src/data/ActiveEvent.php b/src/data/ActiveEvent.php new file mode 100644 index 00000000..0a6544d0 --- /dev/null +++ b/src/data/ActiveEvent.php @@ -0,0 +1,30 @@ +hydrator = new \samdark\hydrator\Hydrator($map); + } + + /** + * @param $className + * @param array|Model $data + * @return object + * @throws \yii\base\InvalidConfigException + */ + public function hydrate($className, $data) + { + $model = $this->hydrator->hydrateInto($data, \Yii::createObject($className)); + + if (!$model instanceof EntityInterface) { + return $model; + } + + $entity = $model; + unset($model); + + $relationMapping = $entity->relationMapping(); + if (empty($relationMapping)) { + return $entity; + } + + foreach ($relationMapping as $relationName => $entityClass) { + /** @var EntityInterface $relationEntity */ + $relationEntity = \Yii::createObject($entityClass); + $relationHydrator = \Yii::createObject(static::class, [$relationEntity->fieldMapping()]); + + if (!isset($data->$relationName)) { + continue; + } + + $relationData = is_array($data->$relationName) + ? $relationHydrator->hydrateAll($entityClass, $data->$relationName) + : $relationHydrator->hydrate($entityClass, $data->$relationName->attributes); + + $this->hydrator->hydrateInto([$relationName => $relationData], $entity); + } + + return $entity; + } + + /** + * @param $className + * @param array|Model[] $data + * @return array + */ + public function hydrateAll($className, array $data): array + { + return array_map(function ($activeRecord) use ($className) { + return $this->hydrate($className, $activeRecord); + }, $data); + } + + /** + * @param $object + * @param array $data + * @return object + */ + public function hydrateInto($object, array $data) + { + return $this->hydrator->hydrateInto($data, $object); + } + + /** + * @param $object + * @return array + */ + public function extract($object): array + { + return $this->hydrator->extract($object); + } +} diff --git a/src/interfaces/ActiveRepositoryInterface.php b/src/interfaces/ActiveRepositoryInterface.php new file mode 100644 index 00000000..c66ce22f --- /dev/null +++ b/src/interfaces/ActiveRepositoryInterface.php @@ -0,0 +1,80 @@ +storeEntity($pageEntity, ['slug']); + * ``` + * + * Then if you want to find a Page by it's slug attribute, you can get a cache key based on the slug: + * + * ```php + * public function findBySlug($slug): ?PageEntity + * { + * $pageEntity = $this->hydrate(['slug' => $slug]); + * + * return $this->findEntityByKey($pageEntity->getCacheKey(['slug'])); + * } + * ``` + * + * You can also pass a postfix to the cache key, if you want to store a related entity. + * + * ```php + * $cacheRepository->set($pageEntity->getCacheKey([], 'next-page'), $nextPage) + * ``` + * + * @param string $postfix + * @param array $keyAttributes to override default key attributes + * @return string + */ + public function getCacheKey(array $keyAttributes = [], string $postfix = null): string; + + /** + * Returns the data attributes and properties mapping with the relation mapping too. + * This required to hydrate the Entity. + * + * @return array + */ + public function fieldMapping(): array; + + /** + * Mapping of property keys to entity classnames. + * + * @return array + */ + public function relationMapping(): array; + + /** + * Returns properties and their values which are not relation properties. + * + * @return array + */ + public function getDataAttributes(): array; + + /** + * Triggers an event. + * This method represents the happening of an event. It invokes + * all attached handlers for the event including class-level handlers. + * @param string $name the event name + * @param Event $event the event parameter. If not set, a default [[Event]] object will be created. + */ + public function trigger($name, Event $event = null); +} diff --git a/src/interfaces/HydratorInterface.php b/src/interfaces/HydratorInterface.php new file mode 100644 index 00000000..e81c4c1d --- /dev/null +++ b/src/interfaces/HydratorInterface.php @@ -0,0 +1,19 @@ +repository; + } + + /** + * @return bool + */ + public function execute(): bool + { + /** @var FormObject $form */ + $form = $this->getForm(); + + /** @var AbstractEntity $entity */ + $entity = $this->getEntity() ?? $this->getRepository()->hydrate([]); + $entity->setAttributes($form->attributes, false); + + if ($this->getRepository()->save($entity)) { + $this->setId($entity->id); + return true; + } + + $form->addErrors($entity->getErrors()); + + return false; + } +} diff --git a/src/models/AbstractEntity.php b/src/models/AbstractEntity.php new file mode 100644 index 00000000..930c6b97 --- /dev/null +++ b/src/models/AbstractEntity.php @@ -0,0 +1,143 @@ +getPrimaryKey()) ? $this->getPrimaryKey() : [$this->getPrimaryKey()]; + $keys = array_filter($keys); + + array_walk($keys, function ($key) use ($model) { + $this->{$key} = $model->{$key}; + }); + } + + /** + * Returns a unique cache key for the entity. + * + * You can override the key attributes. If you want to find a Page entity by it's slug attribute, + * then you can store the entity with a different key(s) then the default key(s). + * + * ```php + * $cacheRepository->storeEntity($pageEntity, ['slug']); + * ``` + * + * Then if you want to find a Page by it's slug attribute, you can get a cache key based on the slug: + * + * ```php + * public function findBySlug($slug): ?PageEntity + * { + * $pageEntity = $this->hydrate(['slug' => $slug]); + * + * return $this->findEntityByKey($pageEntity->getCacheKey(['slug'])); + * } + * ``` + * + * You can also pass a postfix to the cache key, if you want to store a related entity. + * + * ```php + * $cacheRepository->set($pageEntity->getCacheKey([], 'next-page'), $nextPage) + * ``` + * + * @param string $postfix + * @param array $keyAttributes to override default key attributes + * @return string + * @throws InvalidConfigException + */ + public function getCacheKey(array $keyAttributes = [], string $postfix = null): string + { + if (empty($keyAttributes)) { + $keyAttributes = is_array($this->getPrimaryKey()) ? $this->getPrimaryKey() : [$this->getPrimaryKey()]; + } + + $ids = array_map(function ($keyAttribute) { + return $this->{$keyAttribute}; + }, array_filter($keyAttributes)); + + if (empty($ids)) { + throw new InvalidConfigException('Primary key must be set for entities to generate a unique cache key.'); + } + + $ids = array_combine($keyAttributes, $ids); + + return implode('_', array_filter(array_merge([static::class], [http_build_query($ids)], [$postfix]))); + } + + /** + * Returns properties and their values which are not relation properties. + * + * @return array + */ + public function getDataAttributes(): array + { + return array_map(function ($property) { + return $this->{$property}; + }, $this->getDataAttributesPropertiesMap()); + } + + /** + * Returns the data attributes and properties mapping with the relation mapping too. + * This required to hydrate the Entity. + * + * @return array + */ + public function fieldMapping(): array + { + $fields = $this->getDataAttributesPropertiesMap(); + $relationFields = array_keys($this->relationMapping()); + + return array_merge($fields, array_combine($relationFields, $relationFields)); + } + + /** + * Returns the data attributes of the model and the properties of the entity in key value pairs. + * The keys are the attributes/fields/columns of the data model and the values are the properties of the entity. + * + * ```php + * [ + * 'id' => 'id', + * 'parent_id' => 'parentId', + * 'name' => 'name', + * ] + * ``` + * + * @return array|null + */ + private function getDataAttributesPropertiesMap(): array + { + $map = array_map(function ($propertyName) { + return Inflector::underscore($propertyName); + }, array_combine($this->fields(), $this->fields())); + + return array_flip($map); + } +} diff --git a/src/models/AbstractService.php b/src/models/AbstractService.php index 788efd94..5922847a 100644 --- a/src/models/AbstractService.php +++ b/src/models/AbstractService.php @@ -2,12 +2,10 @@ namespace albertborsos\ddd\models; -use albertborsos\ddd\interfaces\BusinessObject; +use albertborsos\ddd\interfaces\EntityInterface; use albertborsos\ddd\interfaces\FormObject; +use albertborsos\ddd\interfaces\RepositoryInterface; use yii\base\Component; -use yii\helpers\ArrayHelper; -use yii\web\Link; -use yii\web\Linkable; /** * Class AbstractDomain @@ -15,6 +13,12 @@ */ abstract class AbstractService extends Component { + /** + * @var string|RepositoryInterface + * @since 2.0.0 + */ + protected $repository; + /** * The ID of the (main) object * @var integer|mixed @@ -27,53 +31,65 @@ abstract class AbstractService extends Component private $_form; /** - * @var \yii\db\ActiveRecord|BusinessObject + * @var EntityInterface */ - private $_model; + private $_entity; - public function __construct(FormObject $form = null, BusinessObject $model = null) + public function __construct(FormObject $form = null, EntityInterface $entity = null, $config = []) { if ($form) { $this->setForm($form); } - if ($model) { - $this->setModel($model); + if ($entity) { + $this->setEntity($entity); + } + if ($this->repository) { + $repository = \Yii::createObject($this->repository); + $this->setRepository($repository); } - parent::__construct([]); + parent::__construct($config); } /** * @return boolean */ - abstract public function execute(); + abstract public function execute(): bool; /** * @return FormObject|\yii\base\Model */ - protected function getForm() + protected function getForm(): ?FormObject { return $this->_form; } + /** + * @return EntityInterface + */ + protected function getEntity(): ?EntityInterface + { + return $this->_entity; + } /** - * @return BusinessObject|ActiveRecord + * @return RepositoryInterface + * @since 2.0.0 */ - protected function getModel() + protected function getRepository() { - return $this->_model; + return $this->repository; } /** * @param $id */ - protected function setId($id) + protected function setId($id): void { $this->_id = $id; } /** - * @return int + * @return int|array */ public function getId() { @@ -83,16 +99,24 @@ public function getId() /** * @param FormObject $form */ - private function setForm(FormObject $form) + private function setForm(FormObject $form): void { $this->_form = $form; } /** - * @param BusinessObject $model + * @param EntityInterface $entity + */ + private function setEntity(EntityInterface $entity): void + { + $this->_entity = $entity; + } + + /** + * @param RepositoryInterface $repository */ - private function setModel(BusinessObject $model) + protected function setRepository(RepositoryInterface $repository): void { - $this->_model = $model; + $this->repository = $repository; } } diff --git a/src/repositories/AbstractActiveRepository.php b/src/repositories/AbstractActiveRepository.php new file mode 100644 index 00000000..2f311e7e --- /dev/null +++ b/src/repositories/AbstractActiveRepository.php @@ -0,0 +1,321 @@ +validateDataModelClass(); + } + + /** + * @return ActiveQueryInterface the newly created [[ActiveQueryInterface]] instance. + */ + public function find(): ActiveQueryInterface + { + return call_user_func([$this->getDataModelClass(), 'find']); + } + + /** + * @param $condition + * @return EntityInterface|mixed + * @throws \yii\base\InvalidConfigException + */ + public function findOne($condition): ?EntityInterface + { + $model = call_user_func([$this->getDataModelClass(), 'findOne'], $condition); + + if (empty($model)) { + return null; + } + + return $this->hydrate($model); + } + + /** + * @param $condition + * @return EntityInterface[]|array + */ + public function findAll($condition): array + { + $models = call_user_func([$this->getDataModelClass(), 'findAll'], $condition); + + return $this->hydrateAll($models); + } + + /** + * @param EntityInterface|Model $entity + * @param bool $runValidation + * @param null $attributeNames + * @return bool|mixed + * @throws \Throwable + * @throws \yii\base\InvalidConfigException + */ + public function save(EntityInterface $entity, $runValidation = true, $attributeNames = null): bool + { + /** @var ActiveRecord $activeRecord */ + $activeRecord = $this->findByEntity($entity); + + if (empty($activeRecord)) { + return $this->insert($entity, $runValidation, $attributeNames, false); + } + + return $this->update($entity, $activeRecord, $runValidation, $attributeNames); + } + + /** + * @param EntityInterface $entity + * @param bool $runValidation + * @param null $attributeNames + * @param bool $checkIsNewRecord + * @return bool + * @throws InvalidConfigException + * @throws \Throwable + */ + public function insert(EntityInterface $entity, $runValidation = true, $attributeNames = null, $checkIsNewRecord = true): bool + { + if ($checkIsNewRecord && !empty($this->findByEntity($entity))) { + throw new InvalidArgumentException('Entity already exists, but `insert` method is called'); + } + + /** @var ActiveRecord $activeRecord */ + $activeRecord = \Yii::createObject($this->getDataModelClass(), [$entity->getDataAttributes()]); + + return $this->insertInternal($entity, $activeRecord, $runValidation, $attributeNames); + } + + /** + * @param EntityInterface $entity + * @param ActiveRecordInterface|null $activeRecord + * @param bool $runValidation + * @param null $attributeNames + * @return bool + * @throws InvalidConfigException + * @throws \Throwable + * @throws \yii\db\StaleObjectException + */ + public function update(EntityInterface $entity, ActiveRecordInterface $activeRecord = null, $runValidation = true, $attributeNames = null): bool + { + /** @var ActiveRecord $activeRecord */ + $activeRecord = $activeRecord ?: $this->findByEntity($entity); + + if (empty($activeRecord)) { + throw new InvalidArgumentException('Entity is not stored yet, but `update` method is called'); + } + + return $this->updateInternal($entity, $activeRecord, $runValidation, $attributeNames); + } + + /** + * @param EntityInterface|Model $entity + * @return bool|int + * @throws \yii\base\InvalidConfigException + */ + public function delete(EntityInterface $entity): bool + { + /** @var ActiveRecordInterface $activeRecord */ + $activeRecord = $this->findByEntity($entity); + + if (empty($activeRecord)) { + return false; + } + + if (!$this->beforeDelete($entity)) { + return false; + } + + if ($activeRecord->delete() !== false) { + $entity->trigger(EntityInterface::EVENT_AFTER_DELETE, new ActiveEvent(['sender' => $activeRecord])); + return true; + } + + return false; + } + + /** + * @param EntityInterface $entity + * @return ActiveRecordInterface|null + * @throws InvalidConfigException + */ + protected function findByEntity(EntityInterface $entity): ?ActiveRecordInterface + { + return \Yii::createObject([$this->getDataModelClass(), 'findOne'], [$this->createFindConditionByEntityKeys($entity)]); + } + + /** + * @return string + */ + public function getDataModelClass(): string + { + return $this->dataModelClass; + } + + /** + * @param $className + * @throws InvalidConfigException + */ + public function setDataModelClass($className): void + { + $this->dataModelClass = $className; + $this->validateDataModelClass(); + } + + /** + * @param EntityInterface $entity + * @param ActiveRecord $activeRecord + * @param $runValidation + * @param $attributes + * @return bool + * @throws \Throwable + */ + private function insertInternal(EntityInterface $entity, ActiveRecord $activeRecord, $runValidation, $attributes): bool + { + if (!$this->beforeSave(true, $entity)) { + return false; + } + + $activeRecord->setAttributes($entity->getDataAttributes(), false); + + if ($activeRecord->insert($runValidation, $attributes)) { + $entity->trigger(EntityInterface::EVENT_AFTER_SAVE, new ActiveEvent(['sender' => $activeRecord, 'scenario' => ActiveEvent::SCENARIO_INSERT])); + $entity->setPrimaryKey($activeRecord); + return true; + } + + $entity->addErrors($activeRecord->getErrors()); + +// $this->afterSave(true, []); + + return false; + } + + /** + * @param EntityInterface $entity + * @param ActiveRecord $activeRecord + * @param $runValidation + * @param $attributes + * @return bool + * @throws \Throwable + * @throws \yii\db\StaleObjectException + */ + private function updateInternal(EntityInterface $entity, ActiveRecord $activeRecord, $runValidation, $attributes): bool + { + $activeRecord->setAttributes($entity->getDataAttributes(), false); + + if (!$this->beforeSave(false, $entity, $activeRecord->getDirtyAttributes())) { + return false; + } + + // update attributes again, which are modified by behaviors + $activeRecord->setAttributes($entity->getDataAttributes(), false); + + if ($activeRecord->update($runValidation, $attributes) !== false) { + $entity->trigger(EntityInterface::EVENT_AFTER_SAVE, new ActiveEvent(['sender' => $activeRecord, 'scenario' => ActiveEvent::SCENARIO_UPDATE])); + $entity->setPrimaryKey($activeRecord); + return true; + } + + $entity->addErrors($activeRecord->getErrors()); + + return false; + } + + /** + * @throws InvalidConfigException + */ + private function validateDataModelClass(): void + { + if (empty($this->dataModelClass) || !\Yii::createObject($this->dataModelClass) instanceof ActiveRecordInterface) { + throw new InvalidConfigException(get_called_class() . '::$dataModelClass must implements `yii\db\ActiveRecordInterface`'); + } + } + + /** + * @param null $isolationLevel + * @return Transaction + * @throws InvalidConfigException + */ + public function beginTransaction($isolationLevel = null): Transaction + { + return $this->resolveDatabase()->beginTransaction($isolationLevel); + } + + /** + * @return Connection + * @throws InvalidConfigException + */ + protected function resolveDatabase(): Connection + { + return \Yii::createObject([$this->getDataModelClass(), 'getDb']); + } + + /** + * @param bool $insert + * @param EntityInterface $entity + * @param array $dirtyAttributes + * @return bool + */ + public function beforeSave(bool $insert, EntityInterface $entity, array $dirtyAttributes = []) + { + $event = new EntityEvent(['dirtyAttributes' => $dirtyAttributes]); + $entity->trigger($insert ? EntityInterface::EVENT_BEFORE_INSERT : EntityInterface::EVENT_BEFORE_UPDATE, $event); + + return $event->isValid; + } + + /** + * @param EntityInterface $entity + * @return bool + */ + public function beforeDelete(EntityInterface $entity) + { + $event = new EntityEvent(); + $entity->trigger(EntityInterface::EVENT_BEFORE_DELETE, $event); + + return $event->isValid; + } + + /** + * @param EntityInterface $entity + * @param bool $skipEmptyAttributes + * @return array + */ + protected function createFindConditionByEntityKeys(EntityInterface $entity, $skipEmptyAttributes = false): array + { + $keys = is_array($entity->getPrimaryKey()) ? $entity->getPrimaryKey() : [$entity->getPrimaryKey()]; + + $condition = []; + + array_walk($keys, function ($key) use (&$condition, $entity) { + $condition[$key] = $entity->{$key}; + }); + + if ($skipEmptyAttributes) { + $condition = array_filter($condition); + } + + return $condition; + } +} diff --git a/src/repositories/AbstractCacheRepository.php b/src/repositories/AbstractCacheRepository.php new file mode 100644 index 00000000..5b64fb13 --- /dev/null +++ b/src/repositories/AbstractCacheRepository.php @@ -0,0 +1,22 @@ +getEntityClass(), $key]); + } +} diff --git a/src/repositories/AbstractRepository.php b/src/repositories/AbstractRepository.php new file mode 100644 index 00000000..bd54130a --- /dev/null +++ b/src/repositories/AbstractRepository.php @@ -0,0 +1,79 @@ +validateEntityClass(); + $this->initHydrator(); + } + + public function hydrate($data): EntityInterface + { + return $this->hydrator->hydrate($this->entityClass, $data); + } + + public function hydrateInto(EntityInterface $entity, $data): EntityInterface + { + return $this->hydrator->hydrateInto($entity, $data); + } + + public function hydrateAll($models): array + { + return $this->hydrator->hydrateAll($this->entityClass, $models); + } + + /** + * @return string + */ + public function getEntityClass(): string + { + return $this->entityClass; + } + + /** + * @throws InvalidConfigException + */ + protected function validateEntityClass(): void + { + if (empty($this->entityClass) || !\Yii::createObject($this->entityClass) instanceof EntityInterface) { + throw new InvalidConfigException(get_called_class() . '::$entityClass must implements `' . EntityInterface::class . '`'); + } + } + + protected function initHydrator(): void + { + $entity = \Yii::createObject($this->entityClass); + $this->hydrator = \Yii::createObject($this->hydrator, [$entity->fieldMapping()]); + if (!$this->hydrator instanceof HydratorInterface) { + throw new InvalidConfigException(get_called_class() . '::$hydrator must implements `' . HydratorInterface::class . '`'); + } + } +} diff --git a/src/repositories/CacheRepository.php b/src/repositories/CacheRepository.php new file mode 100644 index 00000000..6fd2be7a --- /dev/null +++ b/src/repositories/CacheRepository.php @@ -0,0 +1,98 @@ +cache = Instance::ensure($this->cache, CacheInterface::class); + } + + public function get($key) + { + return $this->cache->get($key); + } + + public function set($key, $value, $duration = null, $dependency = null) + { + return $this->cache->set($key, $value, $duration, $dependency); + } + + public function delete($key) + { + return $this->cache->delete($key); + } + + /** + * @param $id + * @return EntityInterface|null + */ + public function findById($id): ?EntityInterface + { + /** @var EntityInterface $entity */ + $entity = $this->hydrate(['id' => $id]); + + return $this->findEntityByKey($entity->getCacheKey()); + } + + /** + * @param EntityInterface $entity + * @return EntityInterface|null + */ + public function findByEntity(EntityInterface $entity): ?EntityInterface + { + return $this->findEntityByKey($entity->getCacheKey()); + } + + /** + * @param string $key + * @return EntityInterface|null + */ + public function findEntityByKey(string $key): ?EntityInterface + { + $data = $this->cache->get($key); + if (empty($data)) { + return null; + } + + return $this->hydrate((array)$data); + } + + /** + * @param EntityInterface $entity + * @param array $keyAttributes + * @param null $duration + * @param null $dependency + * @return bool|mixed + */ + public function storeEntity(EntityInterface $entity, array $keyAttributes = [], $duration = null, $dependency = null) + { + return $this->set($entity->getCacheKey($keyAttributes), $entity->getDataAttributes(), $duration, $dependency); + } + + /** + * Creates data provider instance with search query applied + * + * @param $params + * @param null $formName + * @return BaseDataProvider + */ + public function search($params, $formName = null): BaseDataProvider + { + // TODO: Implement search() method. + } +} diff --git a/src/traits/ActiveFormTrait.php b/src/traits/ActiveFormTrait.php new file mode 100644 index 00000000..467ceb73 --- /dev/null +++ b/src/traits/ActiveFormTrait.php @@ -0,0 +1,35 @@ +repository = \Yii::createObject($this->repository); + } + + /** + * @param string|null $interface + * @return ActiveRepositoryInterface + * @throws InvalidConfigException + */ + public function getRepository($interface = null): ActiveRepositoryInterface + { + if (empty($interface)) { + return $this->repository; + } + + return \Yii::createObject($interface); + } +} diff --git a/src/traits/EvaluateAttributesTrait.php b/src/traits/EvaluateAttributesTrait.php new file mode 100644 index 00000000..60ac3def --- /dev/null +++ b/src/traits/EvaluateAttributesTrait.php @@ -0,0 +1,47 @@ +skipUpdateOnClean + && $event->name == EntityInterface::EVENT_BEFORE_UPDATE + && empty($event->dirtyAttributes) + ) { + return; + } + + if (empty($this->attributes[$event->name])) { + return; + } + + $attributes = (array)$this->attributes[$event->name]; + $value = $this->getValue($event); + foreach ($attributes as $attribute) { + // ignore attribute names which are not string (e.g. when set by TimestampBehavior::updatedAtAttribute) + if (!is_string($attribute)) { + continue; + } + if ($this->preserveNonEmptyValues && !empty($this->owner->$attribute)) { + continue; + } + $this->owner->$attribute = $value; + } + } +} diff --git a/src/traits/SetContainerDefinitionsTrait.php b/src/traits/SetContainerDefinitionsTrait.php new file mode 100644 index 00000000..39df01c3 --- /dev/null +++ b/src/traits/SetContainerDefinitionsTrait.php @@ -0,0 +1,47 @@ + albertborsos\ddd\hydrators\ActiveHydrator::class, + * ]; + * ``` + * + * @return array + */ + abstract protected static function getContainerDefinitions(): array; + + /** + * Sets the DI Container definitions for the application. + * + * @param Application $app + * @param bool $overwrite if it is true, then it will overwrite the definition for the existing keys. + */ + protected function setContainerDefinitions(Application $app, $overwrite = false): void + { + foreach (static::getContainerDefinitions() as $interface => $class) { + if (Yii::$container->has($interface) && !$overwrite) { + continue; + } + + $app->setContainer(['definitions' => [ltrim($interface, '\\') => ltrim($class, '\\')]]); + } + } +} diff --git a/src/web/AbstractActiveController.php b/src/web/AbstractActiveController.php new file mode 100644 index 00000000..20bd9f22 --- /dev/null +++ b/src/web/AbstractActiveController.php @@ -0,0 +1,56 @@ +repository = \Yii::createObject($this->repository); + } + + /** + * Finds the entity based on its primary key value. + * If the entity is not found, a 404 HTTP exception will be thrown. + * @param $id + * @return EntityInterface|null + * @throws InvalidConfigException + * @throws NotFoundHttpException + */ + protected function findEntity($id): ?EntityInterface + { + $entity = $this->getRepository()->findOne($id); + if ($entity === null) { + throw new NotFoundHttpException('The requested page does not exist.'); + } + + return $entity; + } + + /** + * @param string|null $interface + * @return ActiveRepositoryInterface + * @throws InvalidConfigException + */ + public function getRepository($interface = null): ActiveRepositoryInterface + { + if (empty($interface)) { + return $this->repository; + } + + return \Yii::createObject($interface); + } +} diff --git a/src/web/AbstractCacheController.php b/src/web/AbstractCacheController.php new file mode 100644 index 00000000..b18ed008 --- /dev/null +++ b/src/web/AbstractCacheController.php @@ -0,0 +1,56 @@ +repository = \Yii::createObject($this->repository); + } + + /** + * Finds the entity based on its primary key value. + * If the entity is not found, a 404 HTTP exception will be thrown. + * @param $id + * @return EntityInterface|null + * @throws InvalidConfigException + * @throws NotFoundHttpException + */ + protected function findEntity($id): ?EntityInterface + { + $entity = $this->getRepository()->findById($id); + if ($entity === null) { + throw new NotFoundHttpException('The requested page does not exist.'); + } + + return $entity; + } + + /** + * @param string|null $interface + * @return CacheRepositoryInterface + * @throws InvalidConfigException + */ + public function getRepository($interface = null): CacheRepositoryInterface + { + if (empty($interface)) { + return $this->repository; + } + + return \Yii::createObject($interface); + } +} diff --git a/src/web/Controller.php b/src/web/Controller.php new file mode 100644 index 00000000..55218df7 --- /dev/null +++ b/src/web/Controller.php @@ -0,0 +1,44 @@ +repository = \Yii::createObject($this->repository); + } + + /** + * @param string|null $interface + * @return ActiveRepositoryInterface + * @throws InvalidConfigException + */ + public function getRepository($interface = null): RepositoryInterface + { + if (empty($interface)) { + return $this->repository; + } + + return \Yii::createObject($interface); + } + + /** + * Finds the entity based on its primary key value. + * If the entity is not found, a 404 HTTP exception will be thrown. + * @param $id + * @return EntityInterface|null + * @throws InvalidConfigException + * @throws NotFoundHttpException + */ + abstract protected function findEntity(): ?EntityInterface; +} diff --git a/tests/_support/AcceptanceTester.php b/tests/_support/AcceptanceTester.php deleted file mode 100644 index 4c7dcbb6..00000000 --- a/tests/_support/AcceptanceTester.php +++ /dev/null @@ -1,26 +0,0 @@ -modelClass, 'findOne'], $id); + $entityClass = $this->entityClass ?? $this->modelClass; + + return Yii::createObject($entityClass, [$config]); } } diff --git a/tests/_support/base/AbstractServiceTest.php b/tests/_support/base/AbstractServiceTest.php index 996eb88b..284d9a58 100644 --- a/tests/_support/base/AbstractServiceTest.php +++ b/tests/_support/base/AbstractServiceTest.php @@ -2,51 +2,37 @@ namespace albertborsos\ddd\tests\support\base; +use albertborsos\ddd\interfaces\RepositoryInterface; use Yii; -abstract class AbstractServiceTest extends Unit +abstract class AbstractServiceTest extends AbstractFormTest { - protected $formClass; + /** + * @var string + */ protected $serviceClass; - protected $modelClass; /** - * @param array $loadParams - * @param \albertborsos\ddd\interfaces\BusinessObject|null $businessObject - * @param array $initParams - * @return \albertborsos\ddd\interfaces\FormObject|\yii\base\Model + * @var string */ - protected function mockForm($loadParams = [], \albertborsos\ddd\interfaces\BusinessObject $businessObject = null, $initParams = [], $isInitParamsAreArguments = false) - { - if ($isInitParamsAreArguments) { - $params = $businessObject ? array_merge([$businessObject], $initParams) : $initParams; - } else { - $params = $businessObject ? [$businessObject, $initParams] : [$initParams]; - } - - /** @var \yii\base\Model|\albertborsos\ddd\interfaces\FormObject $form */ - $form = Yii::createObject($this->formClass, $params); - $form->load($loadParams, ''); - - return $form; - } + protected $repositoryInterface; /** * @param \albertborsos\ddd\interfaces\FormObject $formObject - * @param \albertborsos\ddd\interfaces\BusinessObject|null $businessObject + * @param \albertborsos\ddd\interfaces\EntityInterface|null $entity * @return \albertborsos\ddd\models\AbstractService */ - protected function mockService(\albertborsos\ddd\interfaces\FormObject $formObject, \albertborsos\ddd\interfaces\BusinessObject $businessObject = null) + protected function mockService(\albertborsos\ddd\interfaces\FormObject $formObject, \albertborsos\ddd\interfaces\EntityInterface $entity = null) { - return Yii::createObject($this->serviceClass, [$formObject, $businessObject]); + return Yii::createObject($this->serviceClass, [$formObject, $entity]); } /** - * @param $id - * @return \yii\db\ActiveRecord + * @return RepositoryInterface + * @throws \yii\base\InvalidConfigException */ - protected function getModel($id) + protected function getRepository() { - return call_user_func([$this->modelClass, 'findOne'], $id); + return Yii::createObject($this->repositoryInterface); } } diff --git a/tests/_support/base/BootstrapWithDefinitionOverride.php b/tests/_support/base/BootstrapWithDefinitionOverride.php new file mode 100644 index 00000000..2c1ada57 --- /dev/null +++ b/tests/_support/base/BootstrapWithDefinitionOverride.php @@ -0,0 +1,13 @@ +setContainerDefinitions($app, true); + } +} diff --git a/tests/_support/base/Customer.php b/tests/_support/base/Customer.php new file mode 100644 index 00000000..81c80ad1 --- /dev/null +++ b/tests/_support/base/Customer.php @@ -0,0 +1,12 @@ +makePartial()->shouldAllowMockingProtectedMethods(); - foreach ($config['attributes'] as $attribute => $value) { + foreach ($config['attributes'] ?? [] as $attribute => $value) { if (!is_array($value)) { $model->$attribute = $value; continue; @@ -58,7 +58,7 @@ private function mockObject(array $config): MockInterface $model->shouldReceive('get' . ucfirst($attribute))->andReturn($returnObjects)->atLeast()->once(); } - foreach ($config['settings'] as $method => $returnValue) { + foreach ($config['settings'] ?? [] as $method => $returnValue) { $model->shouldReceive($method)->andReturn($returnValue)->atLeast()->once(); } diff --git a/tests/_support/base/StubEntity.php b/tests/_support/base/StubEntity.php new file mode 100644 index 00000000..0d63161c --- /dev/null +++ b/tests/_support/base/StubEntity.php @@ -0,0 +1,19 @@ +setId(1); return true; @@ -25,8 +25,13 @@ public function testGetForm() return $this->getForm(); } - public function testGetModel() + public function testGetEntity() { - return $this->getModel(); + return $this->getEntity(); + } + + public function testGetRepository() + { + return $this->getRepository(); } } diff --git a/tests/_support/base/StubbedModel.php b/tests/_support/base/StubbedModel.php deleted file mode 100644 index 8aebbfc1..00000000 --- a/tests/_support/base/StubbedModel.php +++ /dev/null @@ -1,11 +0,0 @@ -initFixtures(); + } } diff --git a/tests/_support/base/UserMock.php b/tests/_support/base/UserMock.php new file mode 100644 index 00000000..08326f0d --- /dev/null +++ b/tests/_support/base/UserMock.php @@ -0,0 +1,22 @@ +isGuest = false; + $this->id = $id; + } + public function logout() + { + $this->isGuest = true; + $this->id = null; + } +} diff --git a/tests/_support/base/domains/customer/cache/CustomerAddressCacheRepository.php b/tests/_support/base/domains/customer/cache/CustomerAddressCacheRepository.php new file mode 100644 index 00000000..3e210cc7 --- /dev/null +++ b/tests/_support/base/domains/customer/cache/CustomerAddressCacheRepository.php @@ -0,0 +1,12 @@ +get($this->postfixedKey(self::POSTFIX_VIP_CUSTOMERS)); + } + + public function updateVipCustomers($customers) + { + return $this->set($this->postfixedKey(self::POSTFIX_VIP_CUSTOMERS), $customers); + } +} diff --git a/tests/_support/base/domains/customer/entities/Customer.php b/tests/_support/base/domains/customer/entities/Customer.php new file mode 100644 index 00000000..34fd168b --- /dev/null +++ b/tests/_support/base/domains/customer/entities/Customer.php @@ -0,0 +1,45 @@ + CustomerAddress::class, + ]; + } +} diff --git a/tests/_support/base/domains/customer/entities/CustomerAddress.php b/tests/_support/base/domains/customer/entities/CustomerAddress.php new file mode 100644 index 00000000..c930b385 --- /dev/null +++ b/tests/_support/base/domains/customer/entities/CustomerAddress.php @@ -0,0 +1,44 @@ + TimestampBehavior::class, + 'blameable' => BlameableBehavior::class, + 'sluggable' => [ + 'class' => SluggableBehavior::class, + 'attribute' => 'name', + 'ensureUnique' => true, + 'repository' => CustomerWithBehaviorsActiveRepository::class, + ], + ]; + } + + public function fields() + { + return [ + 'id', + 'name', + 'slug', + 'createdAt', + 'createdBy', + 'updatedAt', + 'updatedBy', + ]; + } + + public function extraFields() + { + return [ + 'customerAddresses', + ]; + } + + /** + * Mapping of property keys to entity classnames. + * + * @return array + */ + public function relationMapping(): array + { + return [ + 'customerAddresses' => CustomerAddress::class, + ]; + } +} diff --git a/tests/_support/base/domains/customer/entities/CustomerWithModifiedBehaviors.php b/tests/_support/base/domains/customer/entities/CustomerWithModifiedBehaviors.php new file mode 100644 index 00000000..4ffd3934 --- /dev/null +++ b/tests/_support/base/domains/customer/entities/CustomerWithModifiedBehaviors.php @@ -0,0 +1,33 @@ + [ + 'class' => TimestampBehavior::class, + 'attributes' => [ + EntityInterface::EVENT_BEFORE_INSERT => ['createdAt'], + ], + ], + 'blameable' => [ + 'class' => BlameableBehavior::class, + 'attributes' => [ + EntityInterface::EVENT_BEFORE_INSERT => ['createdBy'], + ], + ], + ]; + } +} diff --git a/tests/_support/base/domains/customer/interfaces/CustomerActiveRepositoryInterface.php b/tests/_support/base/domains/customer/interfaces/CustomerActiveRepositoryInterface.php new file mode 100644 index 00000000..7669e696 --- /dev/null +++ b/tests/_support/base/domains/customer/interfaces/CustomerActiveRepositoryInterface.php @@ -0,0 +1,10 @@ + 255], + ]; + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getCustomerAddresses() + { + return $this->hasMany(CustomerAddress::className(), ['customer_id' => 'id'])->inverseOf('customer'); + } +} diff --git a/tests/_support/base/domains/customer/mysql/CustomerActiveRepository.php b/tests/_support/base/domains/customer/mysql/CustomerActiveRepository.php new file mode 100644 index 00000000..27c239fe --- /dev/null +++ b/tests/_support/base/domains/customer/mysql/CustomerActiveRepository.php @@ -0,0 +1,66 @@ +find(); + + if ($params['expand'] ?? false) { + $query->with(explode(',', $params['expand'])); + } + + // add conditions that should always apply here + + $dataProvider = new ActiveEntityDataProvider([ + 'entityClass' => $this->entityClass, + 'hydrator' => $this->hydrator, + 'query' => $query, + 'pagination' => [ + 'params' => $params, + ], + 'sort' => [ + 'params' => $params, + ], + ]); + + $model = \Yii::createObject($this->dataModelClass); + + $model->load($params, $formName); + + if (!$model->validate()) { + // uncomment the following line if you do not want to return any records when validation fails + // $query->where('0=1'); + return $dataProvider; + } + + // grid filtering conditions + $query->andFilterWhere([ + 'id' => $model->id, + ]); + + $query->andFilterWhere(['like', 'name', $model->name]); + + return $dataProvider; + } +} diff --git a/tests/_support/base/domains/customer/mysql/CustomerAddress.php b/tests/_support/base/domains/customer/mysql/CustomerAddress.php new file mode 100644 index 00000000..c72c87a7 --- /dev/null +++ b/tests/_support/base/domains/customer/mysql/CustomerAddress.php @@ -0,0 +1,49 @@ + 255], + [['customer_id'], 'exist', 'skipOnError' => true, 'targetClass' => Customer::className(), 'targetAttribute' => ['customer_id' => 'id']], + ]; + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getCustomer() + { + return $this->hasOne(Customer::className(), ['id' => 'customer_id'])->inverseOf('customerAddresses'); + } +} diff --git a/tests/_support/base/domains/customer/mysql/CustomerAddressActiveRepository.php b/tests/_support/base/domains/customer/mysql/CustomerAddressActiveRepository.php new file mode 100644 index 00000000..e4b4d677 --- /dev/null +++ b/tests/_support/base/domains/customer/mysql/CustomerAddressActiveRepository.php @@ -0,0 +1,69 @@ +find(); + + if ($params['expand'] ?? false) { + $query->with(explode(',', $params['expand'])); + } + + // add conditions that should always apply here + + $dataProvider = new ActiveEntityDataProvider([ + 'entityClass' => $this->entityClass, + 'hydrator' => $this->hydrator, + 'query' => $query, + 'pagination' => [ + 'params' => $params, + ], + 'sort' => [ + 'params' => $params, + ], + ]); + + $model = \Yii::createObject($this->dataModelClass); + + $model->load($params, $formName); + + if (!$model->validate()) { + // uncomment the following line if you do not want to return any records when validation fails + // $query->where('0=1'); + return $dataProvider; + } + + // grid filtering conditions + $query->andFilterWhere([ + 'id' => $model->id, + 'customer_id' => $model->customer_id, + 'zip_code' => $model->zip_code, + ]); + + $query->andFilterWhere(['like', 'city', $model->city]) + ->andFilterWhere(['like', 'street', $model->street]); + + return $dataProvider; + } +} diff --git a/tests/_support/base/domains/customer/mysql/CustomerWithBehaviorsActiveRepository.php b/tests/_support/base/domains/customer/mysql/CustomerWithBehaviorsActiveRepository.php new file mode 100644 index 00000000..c2b57a4d --- /dev/null +++ b/tests/_support/base/domains/customer/mysql/CustomerWithBehaviorsActiveRepository.php @@ -0,0 +1,13 @@ +fakeEventClass) { + return parent::beforeSave($insert, $entity, $dirtyAttributes); + } + + $event = new ModelEvent(); + $entity->trigger($insert ? EntityInterface::EVENT_BEFORE_INSERT : EntityInterface::EVENT_BEFORE_UPDATE, $event); + + return $event->isValid; + } +} diff --git a/tests/_support/base/domains/customer/mysql/InvalidEntityCustomerActiveRepository.php b/tests/_support/base/domains/customer/mysql/InvalidEntityCustomerActiveRepository.php new file mode 100644 index 00000000..d5a22e96 --- /dev/null +++ b/tests/_support/base/domains/customer/mysql/InvalidEntityCustomerActiveRepository.php @@ -0,0 +1,14 @@ + 'ID', + 'customer_id' => 'Customer ID', + 'zip_code' => 'Zip Code', + 'city' => 'City', + 'street' => 'Street', + ]; + } +} diff --git a/tests/_support/base/domains/customer/traits/CustomerAttributeLabelsTrait.php b/tests/_support/base/domains/customer/traits/CustomerAttributeLabelsTrait.php new file mode 100644 index 00000000..2bb11155 --- /dev/null +++ b/tests/_support/base/domains/customer/traits/CustomerAttributeLabelsTrait.php @@ -0,0 +1,20 @@ + 'ID', + 'name' => 'Name', + 'slug' => 'Slug', + ]; + } +} diff --git a/tests/_support/base/modules/api/Module.php b/tests/_support/base/modules/api/Module.php new file mode 100644 index 00000000..9a1ecbdd --- /dev/null +++ b/tests/_support/base/modules/api/Module.php @@ -0,0 +1,42 @@ +registerSubModules($app, $this); + } + + /** + * Returns the submodules to bootstrap in an array with key-value pairs. + * The keys are the IDs of the submodules, and the keys are the classnames of the submodules. + * + * fore example: + * + * ``` + * return [ + * 'v1' => \albertborsos\ddd\tests\support\base\modules\admin\v1\Module::class, + * ]; + * ``` + * + * @return array + */ + protected static function getSubModules() + { + return [ + 'v1' => \albertborsos\ddd\tests\support\base\modules\admin\v1\Module::class, + ]; + } +} diff --git a/tests/_support/base/modules/api/v1/Module.php b/tests/_support/base/modules/api/v1/Module.php new file mode 100644 index 00000000..a8baae84 --- /dev/null +++ b/tests/_support/base/modules/api/v1/Module.php @@ -0,0 +1,8 @@ + IndexAction::class, + 'view' => ViewAction::class, + 'create' => [ + 'class' => CreateAction::class, + 'formClass' => CreateCustomerForm::class, + 'serviceClass' => CreateCustomerService::class, + ], + 'update' => [ + 'class' => UpdateAction::class, + 'formClass' => UpdateCustomerForm::class, + 'serviceClass' => UpdateCustomerService::class, + ], + 'delete' => [ + 'class' => DeleteAction::class, + 'formClass' => DeleteCustomerForm::class, + 'serviceClass' => DeleteCustomerService::class, + ], + 'options' => OptionsAction::class, + ]; + } +} + +/** + * @OA\Get( + * path="/api/v1/customers", + * tags={"api/v1/customers"}, + * summary="List of available Customers", + * @OA\Parameter( + * in="query", + * name="expand", + * required=false, + * @OA\Items( + * type="string", + * enum={"customerAddresses"}, + * ), + * ), + * @OA\Parameter(ref="#/components/parameters/pageSize"), + * @OA\Parameter(ref="#/components/parameters/page"), + * @OA\Response( + * response = 200, + * description = "Successful Response", + * @OA\MediaType( + * mediaType="application/json", + * @OA\Schema( + * @OA\Property( + * property="items", + * type="array", + * @OA\Items(ref="#/components/schemas/Customer"), + * ), + * @OA\Property( + * property="_meta", + * ref="#/components/schemas/MetaFields", + * ), + * ), + * ), + * ), + * ), + */ + +/** + * @OA\Get( + * path="/api/v1/customers/{id}", + * tags={"api/v1/customers"}, + * summary="Find Customer by ID", + * @OA\Parameter( + * in="path", + * name="id", + * required=true, + * @OA\Items( + * type="integer", + * default=1, + * ), + * ), + * @OA\Parameter( + * in="query", + * name="expand", + * required=false, + * @OA\Items( + * type="string", + * enum={"customerAddresses"}, + * ), + * ), + * @OA\Response( + * response = 200, + * description = "Successful Response", + * @OA\MediaType( + * mediaType="application/json", + * @OA\Schema(ref="#/components/schemas/Customer"), + * ), + * ), + * ), + */ diff --git a/tests/_support/base/services/customer/AbstractCustomerService.php b/tests/_support/base/services/customer/AbstractCustomerService.php new file mode 100644 index 00000000..254208f1 --- /dev/null +++ b/tests/_support/base/services/customer/AbstractCustomerService.php @@ -0,0 +1,20 @@ +getRepository()->delete($this->getEntity()); + } +} diff --git a/tests/_support/base/services/customer/UpdateCustomerService.php b/tests/_support/base/services/customer/UpdateCustomerService.php new file mode 100644 index 00000000..43b658a9 --- /dev/null +++ b/tests/_support/base/services/customer/UpdateCustomerService.php @@ -0,0 +1,14 @@ + 255], + ]; + } +} diff --git a/tests/_support/base/services/customer/forms/CreateCustomerForm.php b/tests/_support/base/services/customer/forms/CreateCustomerForm.php new file mode 100644 index 00000000..a63cc64f --- /dev/null +++ b/tests/_support/base/services/customer/forms/CreateCustomerForm.php @@ -0,0 +1,15 @@ + 'ddd-console', + 'basePath' => dirname(__DIR__), + ] +); +$application = new yii\console\Application($config); +$exitCode = $application->run(); +exit($exitCode); diff --git a/tests/config/main.local.example b/tests/config/main.local.example new file mode 100644 index 00000000..8acaf29b --- /dev/null +++ b/tests/config/main.local.example @@ -0,0 +1,11 @@ + [ + 'db' => [ + 'dsn' => getenv('TEST_DB_DSN'), + 'username' => getenv('DB_USERNAME'), + 'password' => getenv('DB_PASSWORD'), + ], + ], +]; diff --git a/tests/config/main.php b/tests/config/main.php new file mode 100644 index 00000000..959e403f --- /dev/null +++ b/tests/config/main.php @@ -0,0 +1,37 @@ + 'ddd-test', + 'basePath' => dirname(__DIR__), + 'components' => [ + 'db' => [ + 'class' => \yii\db\Connection::class, + 'dsn' => getenv('DB_TEST_DSN'), + 'username' => getenv('DB_USERNAME'), + 'password' => getenv('DB_PASSWORD'), + 'charset' => 'utf8', + ], + 'cache' => [ + 'class' => \yii\caching\FileCache::class, + ], + 'user' => \albertborsos\ddd\tests\support\base\UserMock::class, + ], + 'container' => [ + 'definitions' => [ + \albertborsos\ddd\interfaces\HydratorInterface::class => \albertborsos\ddd\hydrators\ActiveHydrator::class, + \albertborsos\ddd\tests\support\base\domains\customer\interfaces\CustomerActiveRepositoryInterface::class => \albertborsos\ddd\tests\support\base\domains\customer\mysql\CustomerActiveRepository::class, + \albertborsos\ddd\tests\support\base\domains\customer\interfaces\CustomerCacheRepositoryInterface::class => \albertborsos\ddd\tests\support\base\domains\customer\cache\CustomerCacheRepository::class, + \albertborsos\ddd\tests\support\base\domains\customer\interfaces\CustomerAddressActiveRepositoryInterface::class => \albertborsos\ddd\tests\support\base\domains\customer\mysql\CustomerAddressActiveRepository::class, + \albertborsos\ddd\tests\support\base\domains\customer\interfaces\CustomerAddressCacheRepositoryInterface::class => \albertborsos\ddd\tests\support\base\domains\customer\cache\CustomerAddressCacheRepository::class, + ], + ], +]; + +$localConfigFile = dirname(__FILE__) . '/main.local.php'; + +$localConfig = []; +if (is_file($localConfigFile)) { + $localConfig = require($localConfigFile); +} + +return \yii\helpers\ArrayHelper::merge($config, $localConfig); diff --git a/tests/fixtures/CustomerFixtures.php b/tests/fixtures/CustomerFixtures.php new file mode 100644 index 00000000..32082583 --- /dev/null +++ b/tests/fixtures/CustomerFixtures.php @@ -0,0 +1,13 @@ + 1, + 'name' => 'Albert', + 'slug' => 'albert', + 'created_at' => 1, + 'created_by' => 10, + 'updated_at' => 1, + 'updated_by' => 10, + ], + [ + 'id' => 2, + 'name' => 'Noncsi', + 'slug' => 'noncsi', + 'created_at' => 1, + 'created_by' => 10, + 'updated_at' => 2, + 'updated_by' => 11, + ], +]; diff --git a/tests/fixtures/data/customer.php b/tests/fixtures/data/customer.php new file mode 100644 index 00000000..763ee86b --- /dev/null +++ b/tests/fixtures/data/customer.php @@ -0,0 +1,11 @@ + 1, + 'name' => 'Albert', + ], + [ + 'id' => 2, + 'name' => 'Noncsi', + ], +]; diff --git a/tests/functional.suite.yml b/tests/functional.suite.yml deleted file mode 100644 index 94c1fe29..00000000 --- a/tests/functional.suite.yml +++ /dev/null @@ -1,12 +0,0 @@ -# Codeception Test Suite Configuration -# -# Suite for functional tests -# Emulate web requests and make application process them -# Include one of framework modules (Symfony2, Yii2, Laravel5) to use it -# Remove this suite if you don't use frameworks - -actor: FunctionalTester -modules: - enabled: - # add a framework module here - - \Helper\Functional \ No newline at end of file diff --git a/tests/migrations/m190807_151625_add_customer_tables.php b/tests/migrations/m190807_151625_add_customer_tables.php new file mode 100644 index 00000000..99f52d92 --- /dev/null +++ b/tests/migrations/m190807_151625_add_customer_tables.php @@ -0,0 +1,45 @@ +createTable(self::TABLE_NAME_CUSTOMER, [ + 'id' => $this->bigPrimaryKey(), + 'name' => $this->string(), + 'slug' => $this->string(), + 'created_at' => $this->bigInteger(), + 'created_by' => $this->bigInteger(), + 'updated_at' => $this->bigInteger(), + 'updated_by' => $this->bigInteger(), + ]); + + $this->createTable(self::TABLE_NAME_CUSTOMER_ADDRESS, [ + 'id' => $this->bigPrimaryKey(), + 'customer_id' => $this->bigInteger(), + 'zip_code' => $this->integer(), + 'city' => $this->string(), + 'street' => $this->string(), + 'created_at' => $this->bigInteger(), + 'created_by' => $this->bigInteger(), + 'updated_at' => $this->bigInteger(), + 'updated_by' => $this->bigInteger(), + ]); + + $this->addForeignKey(self::FK_CUSTOMER_ADDRESS_CUSTOMER_ID_CUSTOMER_ID, self::TABLE_NAME_CUSTOMER_ADDRESS, 'customer_id', self::TABLE_NAME_CUSTOMER, 'id'); + } + + public function down() + { + $this->dropForeignKey(self::FK_CUSTOMER_ADDRESS_CUSTOMER_ID_CUSTOMER_ID, self::TABLE_NAME_CUSTOMER_ADDRESS); + + $this->dropTable(self::TABLE_NAME_CUSTOMER_ADDRESS); + $this->dropTable(self::TABLE_NAME_CUSTOMER); + } +} diff --git a/tests/runtime/cache/.gitkeep b/tests/runtime/cache/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit.suite.yml b/tests/unit.suite.yml index 026c91db..143598c6 100644 --- a/tests/unit.suite.yml +++ b/tests/unit.suite.yml @@ -3,7 +3,9 @@ # Suite for unit or integration tests. actor: UnitTester +bootstrap: _bootstrap.php modules: enabled: - Asserts - - \Helper\Unit \ No newline at end of file + - \Helper\Unit + diff --git a/tests/unit/BootstrapTest.php b/tests/unit/BootstrapTest.php new file mode 100644 index 00000000..77783fdf --- /dev/null +++ b/tests/unit/BootstrapTest.php @@ -0,0 +1,20 @@ +bootstrap(\Yii::$app); + } + + public function testBootstrapWithOverride() + { + $bootstrap = (new BootstrapWithDefinitionOverride())->bootstrap(\Yii::$app); + } +} diff --git a/tests/unit/MockConfigTest.php b/tests/unit/MockConfigTest.php index fc90647a..c936c4c4 100644 --- a/tests/unit/MockConfigTest.php +++ b/tests/unit/MockConfigTest.php @@ -7,7 +7,7 @@ class MockConfigTest extends \Codeception\PHPUnit\TestCase public function mockConfigDataProvider() { return [ - [\albertborsos\ddd\tests\support\base\StubbedForm::class, ['email' => 'a@b.hu'], ['validate' => true]], + [\albertborsos\ddd\tests\support\base\StubForm::class, ['email' => 'a@b.hu'], ['validate' => true]], ]; } diff --git a/tests/unit/MockTraitTest.php b/tests/unit/MockTraitTest.php index ff04961e..4711f3d9 100644 --- a/tests/unit/MockTraitTest.php +++ b/tests/unit/MockTraitTest.php @@ -7,9 +7,10 @@ class MockTraitTest extends \Codeception\PHPUnit\TestCase public function mockObjectDataProvider() { return [ - 'mock form attribute and method' => [\albertborsos\ddd\tests\support\base\StubbedForm::class, ['email' => 'a@b.hu'], ['validate' => true]], - 'mock service execute method' => [\albertborsos\ddd\tests\support\base\StubbedService::class, [], ['execute' => true]], - 'mock service with multiple settings' => [\albertborsos\ddd\tests\support\base\StubbedService::class, [], [ + 'mock form attributes only' => [\albertborsos\ddd\tests\support\base\StubForm::class, ['email' => 'a@b.hu'], []], + 'mock form attribute and method' => [\albertborsos\ddd\tests\support\base\StubForm::class, ['email' => 'a@b.hu'], ['validate' => true]], + 'mock service execute method' => [\albertborsos\ddd\tests\support\base\StubService::class, [], ['execute' => true]], + 'mock service with multiple settings' => [\albertborsos\ddd\tests\support\base\StubService::class, [], [ 'execute' => true, 'failedExecute' => false, ]], @@ -26,6 +27,38 @@ public function mockObjectDataProvider() public function testCreateMock($mockedClass, $attributes, $settings) { $mockConfig = \albertborsos\ddd\tests\support\base\MockConfig::create($mockedClass, $attributes, $settings); + $this->testMockConfig($mockConfig, $attributes, $settings); + } + + /** + * @dataProvider mockObjectDataProvider + * + * @param $mockedClass + * @param $attributes + * @param $settings + */ + public function testToMockObject($class, $attributes, $settings) + { + if (!empty($class)) { + $mockConfig['class'] = $class; + } + if (!empty($attributes)) { + $mockConfig['attributes'] = $attributes; + } + if (!empty($settings)) { + $mockConfig['settings'] = $settings; + } + + $this->testMockConfig($mockConfig, $attributes, $settings); + } + + /** + * @param $attributes + * @param $settings + * @param $mockConfig + */ + protected function testMockConfig($mockConfig, $attributes, $settings): void + { $mockedObject = $this->mockObject($mockConfig); foreach ($attributes as $attribute => $expectedValue) { diff --git a/tests/unit/_bootstrap.php b/tests/unit/_bootstrap.php index af583860..eb6200b5 100644 --- a/tests/unit/_bootstrap.php +++ b/tests/unit/_bootstrap.php @@ -3,8 +3,11 @@ // ensure we get report on all possible php errors error_reporting(-1); -define('YII_ENABLE_ERROR_HANDLER', false); -define('YII_DEBUG', true); - require_once(__DIR__ . '/../../vendor/autoload.php'); require_once(__DIR__ . '/../../vendor/yiisoft/yii2/Yii.php'); + +$config = require(__DIR__ . '/../config/main.php'); + +$app = new \yii\console\Application($config); + +Yii::setAlias('@tests', dirname(__DIR__)); diff --git a/tests/unit/behaviors/BlameableBehaviorTest.php b/tests/unit/behaviors/BlameableBehaviorTest.php new file mode 100644 index 00000000..dec4c8d8 --- /dev/null +++ b/tests/unit/behaviors/BlameableBehaviorTest.php @@ -0,0 +1,166 @@ + CustomerWithBehaviorsFixtures::class, + ]; + } + + protected function setUp(): void + { + parent::setUp(); + $this->initFixtures(); + } + + protected function tearDown() + { + parent::tearDown(); + $this->logoutUser(); + } + + public function testInsert() + { + $this->authenticateUser(self::DEFAULT_USER_ID); + + $data = [ + 'id' => 3, + 'name' => 'Test blameable attributes are filled on insert', + ]; + + /** @var AbstractActiveRepository $repository */ + $repository = \Yii::createObject(CustomerWithBehaviorsActiveRepository::class); + + $entity = $repository->hydrate($data); + $this->assertTrue($repository->insert($entity)); + + /** @var CustomerWithBehaviors $entity */ + $entity = $repository->findOne($data['id']); + + $this->assertEquals(self::DEFAULT_USER_ID, $entity->createdBy); + $this->assertEquals(self::DEFAULT_USER_ID, $entity->updatedBy); + $this->assertEquals($entity->createdBy, $entity->updatedBy); + } + + public function testInsertWhenUserisGuest() + { + $data = [ + 'id' => 3, + 'name' => 'Test blameable attributes are filled on insert', + ]; + + /** @var AbstractActiveRepository $repository */ + $repository = \Yii::createObject(CustomerWithBehaviorsActiveRepository::class); + + $entity = $repository->hydrate($data); + $this->assertTrue($repository->insert($entity)); + + /** @var CustomerWithBehaviors $entity */ + $entity = $repository->findOne($data['id']); + + $this->assertNull($entity->createdBy); + $this->assertNull($entity->updatedBy); + } + + public function testUpdate() + { + $this->authenticateUser(self::UPDATER_USER_ID); + + $data = [ + 'id' => 1, + 'name' => 'Test updatedBy timestamp attribute is modified on update', + ]; + + /** @var AbstractActiveRepository $repository */ + $repository = \Yii::createObject(CustomerWithBehaviorsActiveRepository::class); + /** @var CustomerWithBehaviors $entity */ + $entity = $repository->findOne($data['id']); + + $oldCreatedBy = $entity->createdBy; + $oldUpdatedBy = $entity->updatedBy; + + $entity->setAttributes($data, false); + $this->assertTrue($repository->update($entity)); + + $entity = $repository->findOne($data['id']); + + $this->assertEquals($oldCreatedBy, $entity->createdBy); + $this->assertEquals(self::UPDATER_USER_ID, $entity->updatedBy); + $this->assertNotEquals($oldUpdatedBy, $entity->updatedBy); + } + + public function testUpdateWhenUserIsGuest() + { + $data = [ + 'id' => 1, + 'name' => 'Test updatedBy timestamp attribute is modified on update', + ]; + + /** @var AbstractActiveRepository $repository */ + $repository = \Yii::createObject(CustomerWithBehaviorsActiveRepository::class); + /** @var CustomerWithBehaviors $entity */ + $entity = $repository->findOne($data['id']); + + $oldCreatedBy = $entity->createdBy; + $oldUpdatedBy = $entity->updatedBy; + + $entity->setAttributes($data, false); + $this->assertTrue($repository->update($entity)); + + $entity = $repository->findOne($data['id']); + + $this->assertEquals($oldCreatedBy, $entity->createdBy); + $this->assertNull($entity->updatedBy); + } + + public function testEmptyAttributes() + { + $this->authenticateUser(self::DEFAULT_USER_ID); + + /** @var AbstractActiveRepository $repository */ + $repository = \Yii::createObject(CustomerWithModifiedBehaviorsActiveRepository::class); + + $data = [ + 'id' => 3, + 'name' => 'test only createdBy', + ]; + + /** @var CustomerWithBehaviors $entity */ + $entity = $repository->hydrate($data); + + $this->assertTrue($repository->insert($entity)); + + /** @var CustomerWithBehaviors $entity */ + $entity = $repository->findOne($data['id']); + + $this->assertNotEmpty($entity->createdBy); + $this->assertEmpty($entity->updatedBy); + } + + private function authenticateUser(int $id) + { + \Yii::$app->get('user')->login($id); + } + + private function logoutUser() + { + \Yii::$app->get('user')->logout(); + } +} diff --git a/tests/unit/behaviors/SluggableBehaviorTest.php b/tests/unit/behaviors/SluggableBehaviorTest.php new file mode 100644 index 00000000..d4cf7db7 --- /dev/null +++ b/tests/unit/behaviors/SluggableBehaviorTest.php @@ -0,0 +1,110 @@ + CustomerWithBehaviorsFixtures::class, + ]; + } + + protected function setUp(): void + { + parent::setUp(); + $this->initFixtures(); + } + + public function testInsert() + { + $data = [ + 'id' => 3, + 'name' => 'Test slug attribute is filled on insert', + ]; + + /** @var AbstractActiveRepository $repository */ + $repository = \Yii::createObject(CustomerWithBehaviorsActiveRepository::class); + + $entity = $repository->hydrate($data); + $this->assertTrue($repository->insert($entity)); + + /** @var CustomerWithBehaviors $entity */ + $entity = $repository->findOne($data['id']); + + $this->assertNotEmpty($entity->slug); + } + + public function testUpdate() + { + $data = [ + 'id' => 1, + 'name' => 'Test slug attribute is modified on update', + ]; + + /** @var AbstractActiveRepository $repository */ + $repository = \Yii::createObject(CustomerWithBehaviorsActiveRepository::class); + /** @var CustomerWithBehaviors $entity */ + $entity = $repository->findOne($data['id']); + + $oldSlug = $entity->slug; + + $entity->setAttributes($data, false); + $this->assertTrue($repository->update($entity)); + + $entity = $repository->findOne($data['id']); + + $this->assertNotEquals($oldSlug, $entity->slug); + } + + public function testUpdateWithoutModification() + { + $data = [ + 'id' => 1, + 'name' => 'Albert', + ]; + + /** @var AbstractActiveRepository $repository */ + $repository = \Yii::createObject(CustomerWithBehaviorsActiveRepository::class); + /** @var CustomerWithBehaviors $entity */ + $entity = $repository->findOne($data['id']); + + $oldSlug = $entity->slug; + + $entity->setAttributes($data, false); + $this->assertTrue($repository->update($entity)); + + $entity = $repository->findOne($data['id']); + + $this->assertEquals($oldSlug, $entity->slug); + } + + public function testUniqueSlug() + { + $data = [ + 'id' => 3, + 'name' => 'Albert', + ]; + + /** @var AbstractActiveRepository $repository */ + $repository = \Yii::createObject(CustomerWithBehaviorsActiveRepository::class); + /** @var CustomerWithBehaviors $entity */ + $entity = $repository->hydrate($data); + $this->assertTrue($repository->insert($entity)); + + $entity = $repository->findOne($data['id']); + + $this->assertNotEquals('albert', $entity->slug); + } +} diff --git a/tests/unit/behaviors/TimestampBehaviorTest.php b/tests/unit/behaviors/TimestampBehaviorTest.php new file mode 100644 index 00000000..a8c134f5 --- /dev/null +++ b/tests/unit/behaviors/TimestampBehaviorTest.php @@ -0,0 +1,119 @@ + CustomerWithBehaviorsFixtures::class, + ]; + } + + protected function setUp(): void + { + parent::setUp(); + $this->initFixtures(); + } + + public function testInsert() + { + $data = [ + 'id' => 3, + 'name' => 'Test timestamp attributes are filled on insert', + ]; + + /** @var AbstractActiveRepository $repository */ + $repository = \Yii::createObject(CustomerWithBehaviorsActiveRepository::class); + + $entity = $repository->hydrate($data); + $this->assertTrue($repository->insert($entity)); + + /** @var CustomerWithBehaviors $entity */ + $entity = $repository->findOne($data['id']); + + $this->assertNotEmpty($entity->createdAt); + $this->assertNotEmpty($entity->updatedAt); + $this->assertEquals($entity->createdAt, $entity->updatedAt); + } + + public function testUpdate() + { + $data = [ + 'id' => 1, + 'name' => 'Test updatedAt timestamp attribute is modified on update', + ]; + + /** @var AbstractActiveRepository $repository */ + $repository = \Yii::createObject(CustomerWithBehaviorsActiveRepository::class); + /** @var CustomerWithBehaviors $entity */ + $entity = $repository->findOne($data['id']); + + $oldCreatedAt = $entity->createdAt; + $oldUpdatedAt = $entity->updatedAt; + + $entity->setAttributes($data, false); + $this->assertTrue($repository->update($entity)); + + $entity = $repository->findOne($data['id']); + + $this->assertNotEmpty($entity->updatedAt); + $this->assertNotEquals($oldUpdatedAt, $entity->updatedAt); + $this->assertNotEmpty($entity->createdAt); + $this->assertEquals($oldCreatedAt, $entity->createdAt); + } + + public function testEmptyAttributes() + { + /** @var AbstractActiveRepository $repository */ + $repository = \Yii::createObject(CustomerWithModifiedBehaviorsActiveRepository::class); + + $data = [ + 'id' => 3, + 'name' => 'test only createdAt', + ]; + + /** @var CustomerWithBehaviors $entity */ + $entity = $repository->hydrate($data); + + $this->assertTrue($repository->insert($entity)); + + /** @var CustomerWithBehaviors $entity */ + $entity = $repository->findOne($data['id']); + + $this->assertNotEmpty($entity->createdAt); + $this->assertEmpty($entity->updatedAt); + } + + /** + * @expectedException \yii\base\InvalidConfigException + * @expectedExceptionMessageRegExp /must be used with `albertborsos\\ddd\\base\\EntityEvent`$/ + */ + public function testInvalidEventException() + { + /** @var CustomerWithModifiedBehaviorsActiveRepository $repository */ + $repository = \Yii::createObject(CustomerWithModifiedBehaviorsActiveRepository::class); + $repository->fakeEventClass = true; + + $data = [ + 'id' => 3, + 'name' => 'test invalid event exception', + ]; + + /** @var CustomerWithBehaviors $entity */ + $entity = $repository->hydrate($data); + + $this->assertTrue($repository->insert($entity)); + } +} diff --git a/tests/unit/data/ActiveEntityDataProviderTest.php b/tests/unit/data/ActiveEntityDataProviderTest.php new file mode 100644 index 00000000..9652accf --- /dev/null +++ b/tests/unit/data/ActiveEntityDataProviderTest.php @@ -0,0 +1,88 @@ + CustomerFixtures::class, + ]; + } + + protected function setUp(): void + { + parent::setUp(); + $this->initFixtures(); + } + + public function testReturnsEntities() + { + $dataProvider = new ActiveEntityDataProvider([ + 'entityClass' => Customer::class, + 'hydrator' => \Yii::createObject(HydratorInterface::class, [\Yii::createObject([new Customer(), 'fieldMapping'])]), + 'query' => \albertborsos\ddd\tests\support\base\domains\customer\mysql\Customer::find(), + ]); + + foreach ($dataProvider->getModels() as $model) { + $this->assertInstanceOf(Customer::class, $model); + } + } + + /** + * @expectedException \yii\base\InvalidConfigException + */ + public function testThrowsErrorIfEntityClassIsMissing() + { + $dataProvider = new ActiveEntityDataProvider([ + 'entityClass' => null, + 'hydrator' => \Yii::createObject(HydratorInterface::class, [\Yii::createObject([new Customer(), 'fieldMapping'])]), + 'query' => \albertborsos\ddd\tests\support\base\domains\customer\mysql\Customer::find(), + ]); + } + + /** + * @expectedException \yii\base\InvalidConfigException + */ + public function testThrowsErrorIfEntityClassNotInstanceOfEntityInterface() + { + $dataProvider = new ActiveEntityDataProvider([ + 'entityClass' => \albertborsos\ddd\tests\support\base\domains\customer\mysql\Customer::class, + 'hydrator' => \Yii::createObject(HydratorInterface::class, [\Yii::createObject([new Customer(), 'fieldMapping'])]), + 'query' => \albertborsos\ddd\tests\support\base\domains\customer\mysql\Customer::find(), + ]); + } + + public function dataProviderInvalidHydrator() + { + return [ + 'hydrator is missing (null)' => [null], + 'hydrator is missing (empty string)' => [''], + 'hydrator is not instance of HydratorInterface' => [new \yii\base\Model()], + ]; + } + + /** + * @dataProvider dataProviderInvalidHydrator + * @expectedException \yii\base\InvalidConfigException + */ + public function testThrowsErrorIfHydratorIsMissing($hydrator) + { + $dataProvider = new ActiveEntityDataProvider([ + 'entityClass' => Customer::class, + 'hydrator' => $hydrator, + 'query' => \albertborsos\ddd\tests\support\base\domains\customer\mysql\Customer::find(), + ]); + } +} diff --git a/tests/unit/hydrators/ActiveHydratorTest.php b/tests/unit/hydrators/ActiveHydratorTest.php new file mode 100644 index 00000000..a4cae54d --- /dev/null +++ b/tests/unit/hydrators/ActiveHydratorTest.php @@ -0,0 +1,268 @@ + [Customer::class, ['id' => 'id', 'name' => 'name', 'customerAddresses' => 'customerAddresses'], ['id' => 1, 'name' => 'Name']], + 'model' => [Customer::class, ['id' => 'id', 'name' => 'name', 'customerAddresses' => 'customerAddresses'], new DynamicModel(['id' => 1, 'name' => 'Name'])], + 'model with relation data' => [Customer::class, ['id' => 'id', 'name' => 'name', 'customerAddresses' => 'customerAddresses'], new DynamicModel(['id' => 1, 'name' => 'Name', 'customerAddresses' => [ + ['id' => 1, 'customer_id' => 1, 'zip_code' => 2030, 'city' => 'Érd', 'street' => 'Balatoni út 51.'], + ]])], + 'model with relation model' => [Customer::class, ['id' => 'id', 'name' => 'name', 'customerAddresses' => 'customerAddresses'], new DynamicModel(['id' => 1, 'name' => 'Name', 'customerAddresses' => [ + new DynamicModel(['id' => 1, 'customer_id' => 1, 'zip_code' => 2030, 'city' => 'Érd', 'street' => 'Balatoni út 51.']), + ]])], + ]; + } + + public function dataProviderHydrateIntoObject() + { + return [ + 'data array' => [Customer::class, ['id' => 'id', 'name' => 'name', 'customerAddresses' => 'customerAddresses'], ['id' => 1, 'name' => 'Name']], + 'data array with relation' => [Customer::class, ['id' => 'id', 'name' => 'name', 'customerAddresses' => 'customerAddresses'], ['id' => 1, 'name' => 'Name', 'customerAddresses' => [ + ['id' => 1, 'customer_id' => 1, 'zip_code' => 2030, 'city' => 'Érd', 'street' => 'Balatoni út 51.'], + ]]], + ]; + } + + /** + * @dataProvider dataProviderHydrateSingleEntity + */ + public function testHydrateSingleEntity($entityClass, $map, $data) + { + $hydrator = $this->mockHydrator($map); + + /** @var EntityInterface $entity */ + $entity = $hydrator->hydrate($entityClass, $data); + + $this->assertHydratedEntity($entityClass, $entity, $data); + } + + public function dataProviderHydrateSingleModel() + { + return [ + 'data array' => [CustomerModel::class, ['id' => 'id', 'name' => 'name'], ['id' => 1, 'name' => 'Name']], + 'model' => [CustomerModel::class, ['id' => 'id', 'name' => 'name'], new DynamicModel(['id' => 1, 'name' => 'Name'])], + ]; + } + + /** + * @dataProvider dataProviderHydrateSingleModel + */ + public function testHydrateSingleModel($modelClass, $map, $data) + { + $hydrator = $this->mockHydrator($map); + + /** @var Model $model */ + $model = $hydrator->hydrate($modelClass, $data); + + $this->assertHydratedModel($modelClass, $data, $model); + } + + public function dataProviderHydrateMultipleModels() + { + return [ + 'data array' => [CustomerModel::class, ['id' => 'id', 'name' => 'name'], [ + ['id' => 1, 'name' => 'Name'], + ['id' => 2, 'name' => 'Name'], + ['id' => 3, 'name' => 'Name'], + ]], + 'model' => [CustomerModel::class, ['id' => 'id', 'name' => 'name'], [ + new DynamicModel(['id' => 1, 'name' => 'Name']), + new DynamicModel(['id' => 2, 'name' => 'Name']), + new DynamicModel(['id' => 3, 'name' => 'Name']), + ]], + ]; + } + + /** + * @dataProvider dataProviderHydrateMultipleModels + */ + public function testHydrateMultipleModels($modelClass, $map, $data) + { + $hydrator = $this->mockHydrator($map); + + $models = $hydrator->hydrateAll($modelClass, $data); + + foreach ($data as $i => $modelData) { + $this->assertHydratedModel($modelClass, $modelData, $models[$i]); + } + } + + public function dataProviderHydrateMultipleEntities() + { + return [ + 'data array' => [Customer::class, ['id' => 'id', 'name' => 'name', 'customerAddresses' => 'customerAddresses'], [ + ['id' => 1, 'name' => 'Name'], + ['id' => 2, 'name' => 'Name'], + ['id' => 3, 'name' => 'Name'], + ]], + 'model' => [Customer::class, ['id' => 'id', 'name' => 'name', 'customerAddresses' => 'customerAddresses'], [ + new DynamicModel(['id' => 1, 'name' => 'Name']), + new DynamicModel(['id' => 2, 'name' => 'Name']), + new DynamicModel(['id' => 3, 'name' => 'Name']), + ]], + 'model with relation data' => [Customer::class, ['id' => 'id', 'name' => 'name', 'customerAddresses' => 'customerAddresses'], [ + new DynamicModel(['id' => 1, 'name' => 'Name', 'customerAddresses' => [ + ['id' => 1, 'customer_id' => 1, 'zip_code' => 2030, 'city' => 'Érd', 'street' => 'Balatoni út 51.'], + ]]), + new DynamicModel(['id' => 2, 'name' => 'Name', 'customerAddresses' => [ + ['id' => 2, 'customer_id' => 2, 'zip_code' => 2030, 'city' => 'Érd', 'street' => 'Balatoni út 51.'], + ]]), + new DynamicModel(['id' => 3, 'name' => 'Name', 'customerAddresses' => [ + ['id' => 3, 'customer_id' => 3, 'zip_code' => 2030, 'city' => 'Érd', 'street' => 'Balatoni út 51.'], + ]]), + ]], + 'model with relation model' => [Customer::class, ['id' => 'id', 'name' => 'name', 'customerAddresses' => 'customerAddresses'], [ + new DynamicModel(['id' => 1, 'name' => 'Name', 'customerAddresses' => [ + new DynamicModel(['id' => 1, 'customer_id' => 1, 'zip_code' => 2030, 'city' => 'Érd', 'street' => 'Balatoni út 51.']), + ]]), + new DynamicModel(['id' => 2, 'name' => 'Name', 'customerAddresses' => [ + new DynamicModel(['id' => 2, 'customer_id' => 2, 'zip_code' => 2030, 'city' => 'Érd', 'street' => 'Balatoni út 51.']), + ]]), + new DynamicModel(['id' => 3, 'name' => 'Name', 'customerAddresses' => [ + new DynamicModel(['id' => 3, 'customer_id' => 3, 'zip_code' => 2030, 'city' => 'Érd', 'street' => 'Balatoni út 51.']), + ]]), + ]], + ]; + } + + /** + * @dataProvider dataProviderHydrateMultipleEntities + * + * @param $entityClass + * @param $map + * @param $models + * @return array + */ + public function testHydrateMultipleEntities($entityClass, $map, $models) + { + $hydrator = $this->mockHydrator($map); + + /** @var EntityInterface $entity */ + $entities = $hydrator->hydrateAll($entityClass, $models); + + foreach ($entities as $i => $entity) { + $this->assertHydratedEntity($entityClass, $entity, $models[$i]); + } + } + + /** + * @dataProvider dataProviderHydrateIntoObject + * + * @param $entityClass + * @param $map + * @param $data + * @throws \yii\base\InvalidConfigException + */ + public function testHydrateInto($entityClass, $map, $data) + { + $hydrator = $this->mockHydrator($map); + + $model = \Yii::createObject($entityClass); + foreach ($data as $attribute => $value) { + $this->assertNotEquals($value, $model->$attribute); + } + + /** @var EntityInterface $entity */ + $entity = $hydrator->hydrateInto($model, $data); + + foreach ($data as $attribute => $value) { + $this->assertEquals($value, $entity->$attribute); + } + } + + public function dataProviderExtractHydratedModel() + { + return [ + 'customer' => [Customer::class, ['id' => 'id', 'name' => 'name'], ['id' => 1, 'name' => 'Name']], + 'customerAddress' => [CustomerAddress::class, ['id' => 'id', 'customer_id' => 'customerId', 'zip_code' => 'zipCode', 'city' => 'city', 'street' => 'street'], ['id' => 1, 'customer_id' => 1, 'zip_code' => 2030, 'city' => 'Érd', 'street' => 'Balatoni út 51.']], + ]; + } + + /** + * @dataProvider dataProviderExtractHydratedModel + * @param $entityClass + * @param $map + * @param $data + * @throws \yii\base\InvalidConfigException + */ + public function testExtractHydratedModel($entityClass, $map, $data) + { + $hydrator = $this->mockHydrator($map); + + $entity = $hydrator->hydrate($entityClass, $data); + + $this->assertEquals($data, $hydrator->extract($entity)); + } + + /** + * @return ActiveHydrator + */ + protected function mockHydrator($map) + { + return new ActiveHydrator($map); + } + + /** + * @param $entityClass + * @param EntityInterface $entity + * @param $data + */ + protected function assertHydratedEntity($entityClass, EntityInterface $entity, $data): void + { + $this->assertInstanceOf($entityClass, $entity); + + $dataAttributes = $entity->getDataAttributes(); + $relationAttributes = array_keys($entity->relationMapping()); + + $attributes = $data instanceof Model ? $data->attributes : $data; + + foreach ($attributes as $attribute => $expectedValue) { + if (in_array($attribute, $relationAttributes) && !is_array($entity->$attribute)) { + // on-to-one relation + $this->assertInstanceOf($entity->relationMapping()[$attribute], $entity->$attribute); + } elseif (in_array($attribute, $relationAttributes) && is_array($entity->$attribute)) { + // on-to-many relation + foreach ($entity->$attribute as $relationModel) { + $this->assertInstanceOf($entity->relationMapping()[$attribute], $relationModel); + } + } else { + // data property + $this->assertEquals($expectedValue, $entity->$attribute); + } + } + } + + /** + * @param $modelClass + * @param $data + * @param Model $model + */ + private function assertHydratedModel($modelClass, $data, Model $model): void + { + $this->assertInstanceOf($modelClass, $model); + + $attributes = $data instanceof Model ? $data->attributes : $data; + + foreach ($attributes as $attribute => $expectedValue) { + $this->assertEquals($expectedValue, $model->$attribute); + } + } +} diff --git a/tests/unit/models/AbstractActiveServiceTest.php b/tests/unit/models/AbstractActiveServiceTest.php new file mode 100644 index 00000000..f2bdf449 --- /dev/null +++ b/tests/unit/models/AbstractActiveServiceTest.php @@ -0,0 +1,43 @@ + 'Active Customer Service Test']); + $service = new CreateCustomerService($form); + + $this->assertTrue($service->execute()); + $this->assertNotNull($service->getId()); + + $repository = \Yii::createObject(CustomerActiveRepositoryInterface::class); + /** @var Customer $model */ + $model = $repository->findOne($service->getId()); + + $this->assertInstanceOf(Customer::class, $model); + $this->assertEquals('Active Customer Service Test', $model->name); + } + + public function testExecuteButModelValidationFails() + { + $longerThan255Character = \Yii::$app->security->generateRandomString(256); + $form = new CreateCustomerForm(['name' => $longerThan255Character]); + $service = new CreateCustomerService($form); + + $this->assertFalse($service->execute()); + $this->assertNull($service->getId()); + $this->assertCount(1, $form->getErrors()); + $this->assertArrayHasKey('name', $form->getErrors()); + } +} diff --git a/tests/unit/models/AbstractEntityTest.php b/tests/unit/models/AbstractEntityTest.php new file mode 100644 index 00000000..9de73c1d --- /dev/null +++ b/tests/unit/models/AbstractEntityTest.php @@ -0,0 +1,182 @@ +assertEquals($entity->getPrimaryKey(), ['id']); + } + + public function dataProviderSetPrimaryKey() + { + return array_merge( + $this->dataProviderInvalidPrimaryKeys(), + $this->dataProviderValidPrimaryKeys() + ); + } + + public function dataProviderInvalidPrimaryKeys() + { + return [ + 'no primary key (null)' => [['id' => 1, 'name' => 'Name'], Customer::class, ['getPrimaryKey' => null]], + 'no primary key (empty string)' => [['id' => 1, 'name' => 'Name'], Customer::class, ['getPrimaryKey' => '']], + 'no primary key (array with empty string)' => [['id' => 1, 'name' => 'Name'], Customer::class, ['getPrimaryKey' => ['']]], + 'no primary key (array with null value)' => [['id' => 1, 'name' => 'Name'], Customer::class, ['getPrimaryKey' => [null]]], + ]; + } + + /** + * @return array + */ + public function dataProviderValidPrimaryKeys(): array + { + return [ + 'standard primary key (string)' => [['id' => 1, 'name' => 'Name'], Customer::class, ['getPrimaryKey' => 'id']], + 'standard primary key (array)' => [['id' => 1, 'name' => 'Name'], Customer::class, ['getPrimaryKey' => ['id']]], + 'composite key' => [['id' => 1, 'name' => 'Name'], Customer::class, ['getPrimaryKey' => ['id', 'name']]], + ]; + } + + /** + * @dataProvider dataProviderSetPrimaryKey + */ + public function testSetPrimaryKey($modelAttributes, $entityClass, $entitySettings) + { + $model = new DynamicModel($modelAttributes); + + /** @var EntityInterface $entity */ + $entity = $this->mockObject(['class' => $entityClass, 'settings' => $entitySettings]); + $entity->setPrimaryKey($model); + + foreach ($entity->attributes as $attribute => $value) { + $isPrimaryKey = in_array($attribute, is_array($entity->getPrimaryKey()) ? $entity->getPrimaryKey() : [$entity->getPrimaryKey()]); + $this->assertEquals($isPrimaryKey ? $value : null, $entity->$attribute); + } + } + + /** + * @dataProvider dataProviderInvalidPrimaryKeys + * @expectedException \yii\base\InvalidConfigException + */ + public function testCacheKeyWithInvalidPrimaryKeysWillThrowsException($modelAttributes, $entityClass, $entitySettings) + { + $model = new DynamicModel($modelAttributes); + /** @var EntityInterface $entity */ + $entity = $this->mockObject(['class' => $entityClass, 'settings' => $entitySettings]); + $entity->setPrimaryKey($model); + + $entity->getCacheKey(); + } + + /** + * @dataProvider dataProviderValidPrimaryKeys + */ + public function testCacheKey($modelAttributes, $entityClass, $entitySettings) + { + $model = new DynamicModel($modelAttributes); + /** @var EntityInterface $entity */ + $entity = $this->mockObject(['class' => $entityClass, 'settings' => $entitySettings, 'attributes' => $modelAttributes]); + $entity->setPrimaryKey($model); + + $this->assertNotEmpty($entity->getCacheKey()); + $this->assertNotEquals($entity->getCacheKey(), get_class($entity)); + } + + /** + * @dataProvider dataProviderValidPrimaryKeys + */ + public function testCacheKeyWithCustomKeyAttributes($modelAttributes, $entityClass, $entitySettings) + { + $model = new DynamicModel($modelAttributes); + /** @var EntityInterface $entity */ + $entity = $this->mockObject(['class' => $entityClass, 'settings' => $entitySettings, 'attributes' => $modelAttributes]); + $entity->setPrimaryKey($model); + + $this->assertNotEmpty($entity->getCacheKey(['name'])); + $this->assertNotEquals($entity->getCacheKey(['name']), get_class($entity)); + $this->assertNotEquals($entity->getCacheKey(['name']), $entity->getCacheKey()); + } + + /** + * @dataProvider dataProviderValidPrimaryKeys + */ + public function testCacheKeyWithPostfix($modelAttributes, $entityClass, $entitySettings) + { + $model = new DynamicModel($modelAttributes); + /** @var EntityInterface $entity */ + $entity = $this->mockObject(['class' => $entityClass, 'settings' => $entitySettings, 'attributes' => $modelAttributes]); + $entity->setPrimaryKey($model); + + $postfix = 'postfix'; + + $this->assertNotEmpty($entity->getCacheKey([], $postfix)); + $this->assertNotEquals($entity->getCacheKey([], $postfix), get_class($entity)); + $this->assertNotEquals($entity->getCacheKey([], $postfix), $entity->getCacheKey()); + + $this->assertEquals($entity->getCacheKey([], $postfix), implode('_', [$entity->getCacheKey(), $postfix])); + } + + public function dataProviderDataAttributes() + { + return [ + 'Customer Entity' => [Customer::class, [], array_fill_keys(['id', 'name'], null)], + 'CustomerAddress Entity' => [CustomerAddress::class, [], array_fill_keys(['id', 'customer_id', 'zip_code', 'city', 'street'], null)], + 'Customer Form with custom property' => [CreateCustomerForm::class, [], array_fill_keys(['id', 'name'], null)], + ]; + } + + /** + * @dataProvider dataProviderDataAttributes + * + * @param $entityClass + * @param $entitySettings + * @param $expectedDataAttributes + */ + public function testGetDataAttributes($entityClass, $entitySettings, $expectedDataAttributes) + { + /** @var EntityInterface $entity */ + $entity = $this->mockObject(['class' => $entityClass, 'settings' => $entitySettings]); + + $this->assertEquals($expectedDataAttributes, $entity->getDataAttributes()); + } + + /** + * @return array + */ + public function dataProviderFieldMapping() + { + return [ + 'standard fields' => [Customer::class, [], ['id' => 'id', 'name' => 'name', 'customerAddresses' => 'customerAddresses']], + 'fields with snake_case' => [CustomerAddress::class, [], ['id' => 'id', 'customer_id' => 'customerId', 'zip_code' => 'zipCode', 'city' => 'city', 'street' => 'street']], + ]; + } + + /** + * @dataProvider dataProviderFieldMapping + * + * @param $entityClass + * @param $entitySettings + * @param $expectedDataAttributes + */ + public function testFieldmapping($entityClass, $entitySettings, $expectedMapping) + { + /** @var EntityInterface $entity */ + $entity = $this->mockObject(['class' => $entityClass, 'settings' => $entitySettings]); + + $this->assertEquals($expectedMapping, $entity->fieldMapping()); + } +} diff --git a/tests/unit/models/AbstractServiceTest.php b/tests/unit/models/AbstractServiceTest.php index 5093afbe..22ee1aec 100644 --- a/tests/unit/models/AbstractServiceTest.php +++ b/tests/unit/models/AbstractServiceTest.php @@ -2,74 +2,90 @@ namespace albertborsos\ddd\tests\unit\models; -use albertborsos\ddd\tests\support\base\StubbedForm; -use albertborsos\ddd\tests\support\base\StubbedModel; -use albertborsos\ddd\tests\support\base\StubbedService; +use albertborsos\ddd\tests\support\base\domains\customer\entities\Customer; +use albertborsos\ddd\tests\support\base\services\customer\CreateCustomerService; +use albertborsos\ddd\tests\support\base\services\customer\forms\CreateCustomerForm; +use albertborsos\ddd\tests\support\base\services\customer\forms\UpdateCustomerForm; +use albertborsos\ddd\tests\support\base\services\customer\UpdateCustomerService; +use albertborsos\ddd\tests\support\base\StubForm; +use albertborsos\ddd\tests\support\base\StubEntity; +use albertborsos\ddd\tests\support\base\StubService; use Codeception\PHPUnit\TestCase; +use Codeception\Util\Debug; use yii\base\Model; class AbstractServiceTest extends TestCase { /** + * @param $serviceClass * @param null $form - * @param null $model - * @return StubbedService + * @param null $entity + * @return UpdateCustomerService|object + * @throws \yii\base\InvalidConfigException */ - public function mockService($form = null, $model = null) + public function mockService($serviceClass, $args = []) { - if (!empty(func_get_args())) { - return \Yii::createObject(StubbedService::className(), func_get_args()); + if (!empty($args)) { + return \Yii::createObject($serviceClass, $args); } - return \Yii::createObject(StubbedService::className()); + return \Yii::createObject($serviceClass); } - public function invalidConstructionDataProvider() + public function constructionDataProvider() { return [ - 'no arguments is valid' => [[], null], - 'invalid form interface' => [[new Model()], 'TypeError'], - 'invalid model interface' => [[new StubbedForm(), new Model()], 'TypeError'], - 'valid arguments' => [[new StubbedForm(), new StubbedModel()], null], + 'no arguments is valid' => [StubService::class, [], null], + 'invalid form interface (model)' => [CreateCustomerService::class, [new Model()], 'TypeError'], + 'invalid form interface (form)' => [CreateCustomerService::class, [new UpdateCustomerForm()], 'TypeError'], + 'invalid entity interface' => [UpdateCustomerService::class, [new UpdateCustomerForm(), new Model()], 'TypeError'], + 'valid arguments' => [UpdateCustomerService::class, [new UpdateCustomerForm(), new Customer()], null], ]; } /** - * @dataProvider invalidConstructionDataProvider + * @dataProvider constructionDataProvider * * @param $constructorArguments * @param $expectedException */ - public function testInvalidObjectInitialization($constructorArguments, $expectedException) + public function testInvalidObjectInitialization($serviceClass, $constructorArguments, $expectedException) { if ($expectedException !== null) { $this->expectException($expectedException); } - call_user_func_array([$this, 'mockService'], $constructorArguments); + call_user_func_array([$this, 'mockService'], [$serviceClass, $constructorArguments]); } public function testGetFormObject() { - $mockedForm = new StubbedForm(); - $service = $this->mockService($mockedForm); + $mockedForm = new StubForm(); + $service = $this->mockService(StubService::class, [$mockedForm]); $this->assertSame($mockedForm, $service->testGetForm()); } - public function testGetModelObject() + public function testGetEntityObject() { - $mockedModel = new StubbedModel(); - $service = $this->mockService(null, $mockedModel); + $mockedEntity = new StubEntity(); + $service = $this->mockService(StubService::class, [null, $mockedEntity]); - $this->assertSame($mockedModel, $service->testGetModel()); + $this->assertSame($mockedEntity, $service->testGetEntity()); + } + + public function testGetRepository() + { + $service = $this->mockService(StubService::class); + + $this->assertNull($service->testGetRepository()); } public function testExecuteOk() { - $mockedForm = new StubbedForm(); - $mockedModel = new StubbedModel(); + $mockedForm = new StubForm(); + $mockedModel = new StubEntity(); - $service = $this->mockService($mockedForm, $mockedModel); + $service = $this->mockService(StubService::class, [$mockedForm, $mockedModel]); $this->assertNull($service->getId()); $this->assertTrue($service->execute()); @@ -79,10 +95,10 @@ public function testExecuteOk() public function testExecuteFailed() { - $mockedForm = new StubbedForm(); - $mockedModel = new StubbedModel(); + $mockedForm = new StubForm(); + $mockedModel = new StubEntity(); - $service = $this->mockService($mockedForm, $mockedModel); + $service = $this->mockService(StubService::class, [$mockedForm, $mockedModel]); $this->assertEmpty($mockedForm->errors); $this->assertNull($service->getId()); diff --git a/tests/unit/repositories/AbstractActiveRepositoryTest.php b/tests/unit/repositories/AbstractActiveRepositoryTest.php new file mode 100644 index 00000000..559250c5 --- /dev/null +++ b/tests/unit/repositories/AbstractActiveRepositoryTest.php @@ -0,0 +1,350 @@ + CustomerFixtures::class, + ]; + } + + protected function setUp(): void + { + parent::setUp(); + $this->initFixtures(); + } + + public function invalidDataModelClassDataProvider() + { + return [ + 'dataModelClass is null' => [CustomerActiveRepository::class, null], + 'dataModelClass is empty string' => [CustomerActiveRepository::class, ''], + 'dataModelClass is not implementing ActiveRecordInterface' => [CustomerActiveRepository::class, Customer::class], + ]; + } + + /** + * @dataProvider invalidDataModelClassDataProvider + * @expectedException \yii\base\InvalidConfigException + * @expectedExceptionMessageRegExp /\$dataModelClass must implements `yii\\db\\ActiveRecordInterface`$/ + */ + public function testMissingDataModelClass($repositoryClass, $dataModelClass) + { + $this->mockObject(MockConfig::create($repositoryClass, ['dataModelClass' => $dataModelClass])); + } + + public function testFind() + { + $repository = \Yii::createObject(CustomerActiveRepositoryInterface::class); + + $this->assertInstanceOf(ActiveQueryInterface::class, $repository->find()); + } + + public function testFindOne() + { + $repository = \Yii::createObject(CustomerActiveRepositoryInterface::class); + + $fixtureId = $this->getFixture('customers')[0]['id']; + + $this->assertInstanceOf(\albertborsos\ddd\tests\support\base\domains\customer\entities\Customer::class, $repository->findOne($fixtureId)); + } + + public function testFindAll() + { + $repository = \Yii::createObject(CustomerActiveRepositoryInterface::class); + + $fixtureId = $this->getFixture('customers')[0]['id']; + + $entities = $repository->findAll(['id' => $fixtureId]); + $this->assertCount(1, $entities); + + foreach ($entities as $entity) { + $this->assertInstanceOf(\albertborsos\ddd\tests\support\base\domains\customer\entities\Customer::class, $entity); + } + } + + public function saveDataProvider() + { + return [ + 'create customer' => [true, CustomerActiveRepositoryInterface::class, ['id' => 4, 'name' => 'Test to Save via repository']], + 'update customer' => [false, CustomerActiveRepositoryInterface::class, ['id' => 1, 'name' => 'Test to Save via repository']], + ]; + } + + /** + * @dataProvider saveDataProvider + * + * @param $isNewRecord + * @param $repositoryClass + * @param $data + * @throws \yii\base\InvalidConfigException + */ + public function testSave($isNewRecord, $repositoryClass, $data) + { + /** @var AbstractActiveRepository $repository */ + $repository = \Yii::createObject($repositoryClass); + + $entity = $repository->findOne($data['id']); + if ($isNewRecord) { + $this->assertNull($entity); + } else { + $this->assertInstanceOf($repository->getEntityClass(), $entity); + } + + $entity = $repository->hydrate($data); + $this->assertTrue($repository->save($entity)); + + $entity = $repository->findOne($data['id']); + $this->assertInstanceOf($repository->getEntityClass(), $entity); + + foreach ($entity->fields() as $attribute) { + $this->assertEquals($data[$attribute], $entity->$attribute); + } + } + + public function testInsert() + { + $data = [ + 'id' => 5, + 'name' => 'Test to Insert via repository', + ]; + + /** @var AbstractActiveRepository $repository */ + $repository = \Yii::createObject(CustomerActiveRepositoryInterface::class); + + $entity = $repository->hydrate($data); + $this->assertTrue($repository->insert($entity)); + + $entity = $repository->findOne($data['id']); + $this->assertInstanceOf($repository->getEntityClass(), $entity); + + foreach ($entity->fields() as $attribute) { + $this->assertEquals($data[$attribute], $entity->$attribute); + } + } + + /** + * @expectedException \yii\base\InvalidArgumentException + */ + public function testCallUpdateForNonExistingRecord() + { + $data = [ + 'id' => 6, + 'name' => 'Test to Update via repository', + ]; + + /** @var AbstractActiveRepository $repository */ + $repository = \Yii::createObject(CustomerActiveRepositoryInterface::class); + + $entity = $repository->hydrate($data); + $repository->update($entity); + } + + /** + * @expectedException \yii\base\InvalidArgumentException + */ + public function testCallInsertForExistingRecord() + { + $data = [ + 'id' => 1, + 'name' => 'Test to Insert via repository', + ]; + + /** @var AbstractActiveRepository $repository */ + $repository = \Yii::createObject(CustomerActiveRepositoryInterface::class); + + $entity = $repository->hydrate($data); + $repository->insert($entity); + } + + public function testUpdate() + { + $data = [ + 'id' => 1, + 'name' => 'Test to Update via repository', + ]; + + /** @var AbstractActiveRepository $repository */ + $repository = \Yii::createObject(CustomerActiveRepositoryInterface::class); + + /** @var \albertborsos\ddd\tests\support\base\domains\customer\entities\Customer $entity */ + $entity = $repository->findOne($data['id']); + $entity->setAttributes($data, false); + + $this->assertTrue($repository->update($entity)); + + $entity = $repository->findOne($data['id']); + $this->assertInstanceOf($repository->getEntityClass(), $entity); + + foreach ($entity->fields() as $attribute) { + $this->assertEquals($data[$attribute], $entity->$attribute); + } + } + + public function testUpdateWithNoModification() + { + $data = [ + 'id' => 1, + 'name' => 'Albert', + ]; + + /** @var AbstractActiveRepository $repository */ + $repository = \Yii::createObject(CustomerActiveRepositoryInterface::class); + + $entity = $repository->hydrate($data); + $this->assertTrue($repository->update($entity)); + + $entity = $repository->findOne($data['id']); + $this->assertInstanceOf($repository->getEntityClass(), $entity); + + foreach ($entity->fields() as $attribute) { + $this->assertEquals($data[$attribute], $entity->$attribute); + } + } + + public function deleteExistingRecordDataProvider() + { + return [ + 'delete existing customer' => [CustomerActiveRepositoryInterface::class, 1], + ]; + } + + /** + * @dataProvider deleteExistingRecordDataProvider + * + * @param $isNewRecord + * @param $repositoryClass + * @param $data + * @throws \yii\base\InvalidConfigException + */ + public function testDeleteExistingRecord($repositoryClass, $recordId) + { + /** @var AbstractActiveRepository $repository */ + $repository = \Yii::createObject($repositoryClass); + $entity = $repository->findOne($recordId); + + $this->assertInstanceOf($repository->getEntityClass(), $entity); + $this->assertTrue($repository->delete($entity)); + + $this->assertNull($repository->findOne($recordId)); + } + + /** + * @expectedException TypeError + * + * @param $isNewRecord + * @param $repositoryClass + * @param $data + * @throws \yii\base\InvalidConfigException + */ + public function testDeleteNotExistingRecord() + { + /** @var AbstractActiveRepository $repository */ + $repository = \Yii::createObject(CustomerActiveRepositoryInterface::class); + $entity = $repository->findOne(4); + + $this->assertNull($entity); + $repository->delete($entity); + } + + /** + * @param $isNewRecord + * @param $repositoryClass + * @param $data + * @throws \yii\base\InvalidConfigException + */ + public function testDeleteEmptyEntity() + { + /** @var AbstractActiveRepository $repository */ + $repository = \Yii::createObject(CustomerActiveRepositoryInterface::class); + $entity = $repository->hydrate([]); + + $this->assertFalse($repository->delete($entity)); + } + + public function testBeginTransaction() + { + /** @var AbstractActiveRepository $repository */ + $repository = \Yii::createObject(CustomerActiveRepositoryInterface::class); + $transaction = $repository->beginTransaction(); + $this->assertNotNull($transaction); + $this->assertTrue($transaction->isActive); + + $transaction->rollBack(); + } + + public function testTransactionCommit() + { + $attributes = [ + 'name' => 'Transaction Commit', + ]; + + /** @var AbstractActiveRepository $repository */ + $repository = \Yii::createObject(CustomerActiveRepositoryInterface::class); + $transaction = $repository->beginTransaction(); + + try { + $this->assertEmpty($repository->findOne($attributes)); + $form = new CreateCustomerForm($attributes); + $this->assertTrue($form->validate()); + $service = new CreateCustomerService($form); + $this->assertTrue($service->execute()); + $this->assertInstanceOf(\albertborsos\ddd\tests\support\base\domains\customer\entities\Customer::class, $repository->findOne($attributes)); + $transaction->commit(); + } catch (Exception $e) { + return false; + } + + $entity = $repository->findOne($attributes); + $this->assertInstanceOf(\albertborsos\ddd\tests\support\base\domains\customer\entities\Customer::class, $entity); + + $repository->delete($entity); + } + + public function testTransactionRollback() + { + $attributes = [ + 'name' => 'Transaction Rollback', + ]; + + /** @var AbstractActiveRepository $repository */ + $repository = \Yii::createObject(CustomerActiveRepositoryInterface::class); + $transaction = $repository->beginTransaction(); + + try { + $this->assertEmpty($repository->findOne($attributes)); + $form = new CreateCustomerForm($attributes); + $this->assertTrue($form->validate()); + $service = new CreateCustomerService($form); + $this->assertTrue($service->execute()); + $this->assertInstanceOf(\albertborsos\ddd\tests\support\base\domains\customer\entities\Customer::class, $repository->findOne($attributes)); + + throw new Exception('rollback'); + } catch (Exception $e) { + $transaction->rollBack(); + $this->assertEmpty($repository->findOne($attributes)); + } + } +} diff --git a/tests/unit/repositories/AbstractRepositoryTest.php b/tests/unit/repositories/AbstractRepositoryTest.php new file mode 100644 index 00000000..2eb2f8a0 --- /dev/null +++ b/tests/unit/repositories/AbstractRepositoryTest.php @@ -0,0 +1,49 @@ +hydrate([]); + + $data = ['id' => 9, 'name' => 'hydrate into']; + $hydratedEntity = $repository->hydrateInto($entity, $data); + + foreach ($data as $attribute => $value) { + $this->assertEquals($value, $hydratedEntity->$attribute); + } + } + + /** + * @expectedException \yii\base\InvalidConfigException + * @expectedExceptionMessageRegExp /\$entityClass must implements `albertborsos\\ddd\\interfaces\\EntityInterface`$/ + */ + public function testInvalidEntityClass() + { + \Yii::createObject(InvalidEntityCustomerActiveRepository::class); + } + + /** + * @expectedException \yii\base\InvalidConfigException + * @expectedExceptionMessageRegExp /\$hydrator must implements `albertborsos\\ddd\\interfaces\\HydratorInterface`$/ + */ + public function testInvalidHydratorClass() + { + new InvalidHydratorCustomerActiveRepository(); + } +} diff --git a/tests/unit/repositories/CacheRepositoryTest.php b/tests/unit/repositories/CacheRepositoryTest.php new file mode 100644 index 00000000..60c26c79 --- /dev/null +++ b/tests/unit/repositories/CacheRepositoryTest.php @@ -0,0 +1,99 @@ +updateVipCustomers($customerIds); + + $this->assertEquals($customerIds, $repository->getVipCustomers()); + } + + public function testSetAndGetAndDelete() + { + $repository = \Yii::createObject(CustomerCacheRepositoryInterface::class); + + $key = 'test-key'; + $value = 'test-value'; + + $this->assertTrue($repository->set($key, $value)); + $this->assertEquals($value, $repository->get($key)); + $this->assertTrue($repository->delete($key)); + $this->assertNotEquals($value, $repository->get($key)); + $this->assertFalse($repository->get($key)); + } + + public function entityDataProvider() + { + return [ + 'customer' => [['id' => 1, 'name' => 'Albert']], + ]; + } + + /** + * @dataProvider entityDataProvider + * @param $data + * @throws \yii\base\InvalidConfigException + */ + public function testFindByEntity($data) + { + $repository = \Yii::createObject(CustomerCacheRepositoryInterface::class); + + $customer = $repository->hydrate($data); + + $repository->storeEntity($customer); + + $this->assertEquals($customer, $repository->findByEntity($customer)); + + $this->assertTrue($repository->delete($customer->getCacheKey())); + $this->assertEmpty($repository->findByEntity($customer)); + } + + /** + * @dataProvider entityDataProvider + * @param $data + * @throws \yii\base\InvalidConfigException + */ + public function testFindById($data) + { + $repository = \Yii::createObject(CustomerCacheRepositoryInterface::class); + + $customer = $repository->hydrate($data); + + $repository->storeEntity($customer); + + $this->assertEquals($customer, $repository->findById($data['id'])); + + $this->assertTrue($repository->delete($customer->getCacheKey())); + $this->assertEmpty($repository->findById($data['id'])); + } + + /** + * @dataProvider entityDataProvider + * @param $data + * @throws \yii\base\InvalidConfigException + */ + public function testFindEntityByKey($data) + { + $repository = \Yii::createObject(CustomerCacheRepositoryInterface::class); + + $customer = $repository->hydrate($data); + + $repository->storeEntity($customer); + + $this->assertEquals($customer, $repository->findEntityByKey($customer->getCacheKey())); + $this->assertTrue($repository->delete($customer->getCacheKey())); + $this->assertEmpty($repository->findEntityByKey($customer->getCacheKey())); + } +} diff --git a/tests/unit/traits/ActiveFormTraitTest.php b/tests/unit/traits/ActiveFormTraitTest.php new file mode 100644 index 00000000..9d48a70b --- /dev/null +++ b/tests/unit/traits/ActiveFormTraitTest.php @@ -0,0 +1,29 @@ +getRepository(); + } + + public function testGetRepository() + { + $form = new CreateCustomerForm(); + $this->assertInstanceOf(CustomerActiveRepository::class, $form->getRepository()); + + $this->assertInstanceOf(CustomerAddressActiveRepository::class, $form->getRepository(CustomerAddressActiveRepository::class)); + } +}