From 19fe98f990367b4b2dc1a979b5527c0b99dbc4d3 Mon Sep 17 00:00:00 2001 From: Alex Masterov Date: Wed, 1 Jun 2016 21:55:22 +0300 Subject: [PATCH 1/6] Improve quality to be Scrutinizer CI compliant (#57) * Use a single line comment * Add a separate method to retrieve the Request body --- src/Configuration/DiactorosConfiguration.php | 6 +--- src/Input.php | 36 ++++++++++++++------ 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/src/Configuration/DiactorosConfiguration.php b/src/Configuration/DiactorosConfiguration.php index 2bf79cf..e7f8033 100644 --- a/src/Configuration/DiactorosConfiguration.php +++ b/src/Configuration/DiactorosConfiguration.php @@ -17,13 +17,9 @@ class DiactorosConfiguration implements ConfigurationInterface */ public function apply(Injector $injector) { + // Wrong type hint. see https://github.com/relayphp/Relay.Relay/issues/25 $injector->alias( RequestInterface::class, - // It should not be necessary to force all requests to be server - // requests, except that Relay uses the wrong type hint: - // https://github.com/relayphp/Relay.Relay/issues/25 - // - // 'Zend\Diactoros\Request' ServerRequest::class ); diff --git a/src/Input.php b/src/Input.php index 73ca588..ca79a9e 100644 --- a/src/Input.php +++ b/src/Input.php @@ -8,7 +8,7 @@ class Input implements InputInterface { /** - * Flatten all input from the request + * Flatten all input from the request. * * @param ServerRequestInterface $request * @@ -18,20 +18,11 @@ public function __invoke( ServerRequestInterface $request ) { $attrs = $request->getAttributes(); - $body = $request->getParsedBody(); + $body = $this->body($request); $cookies = $request->getCookieParams(); $query = $request->getQueryParams(); $uploads = $request->getUploadedFiles(); - if (empty($body)) { - $body = []; - } elseif (is_object($body)) { - // Because the parsed body may also be represented as an object, - // additional parsing is required. This is a bit dirty but works - // very well for anonymous objects. - $body = json_decode(json_encode($body), true); - } - // Order matters here! Important values go last! return array_replace( $query, @@ -41,4 +32,27 @@ public function __invoke( $attrs ); } + + /** + * @param ServerRequestInterface $request + * + * @return array + */ + private function body(ServerRequestInterface $request) + { + $body = $request->getParsedBody(); + + if (empty($body)) { + return []; + } + + if (is_object($body)) { + // Because the parsed body may also be represented as an object, + // additional parsing is required. This is a bit dirty but works + // very well for anonymous objects. + $body = json_decode(json_encode($body), true); + } + + return $body; + } } From 0b4d290d07bc075eed84e883848be7fca5d76e6c Mon Sep 17 00:00:00 2001 From: Woody Gilk Date: Mon, 6 Jun 2016 08:49:14 -0500 Subject: [PATCH 2/6] Switch from routing to split dispatching (#63) Rather than having a single large routing table, split routing into separate dispatching classes, similar to a middleware set. --- MIGRATING.md | 140 +++++++++++++++++++++++ src/Application.php | 26 +++-- src/Dispatching/DispatchingSet.php | 50 ++++++++ src/Exception/DispatchingException.php | 25 ++++ tests/ApplicationTest.php | 42 ++++--- tests/Dispatching/DispatchingSetTest.php | 50 ++++++++ 6 files changed, 304 insertions(+), 29 deletions(-) create mode 100644 MIGRATING.md create mode 100644 src/Dispatching/DispatchingSet.php create mode 100644 src/Exception/DispatchingException.php create mode 100644 tests/Dispatching/DispatchingSetTest.php diff --git a/MIGRATING.md b/MIGRATING.md new file mode 100644 index 0000000..ecfc065 --- /dev/null +++ b/MIGRATING.md @@ -0,0 +1,140 @@ +Migrating +========= + +Use this document to help you transition away from deprecated code. + +## Switching from Routing to Dispatching + +Version 3.0.0 replaces the routing callback with dispatching callables, which +are typically defined as classes with an `__invoke` method in your project. + +### Changes + +Routing that used to be defined as: + +```php +Application::build() +// ... +->setRouting(function (Equip\Directory $directory) { + return $directory + ->get('/', Domain\Welcome::class) + ->get('/hello[/{name}]', Domain\Hello::class) + ->post('/hello[/{name}]', Domain\Hello::class) + ; // End of routing +}) +``` + +Would now be defined as: + +```php +Application::build() +// ... +->setDispatching([ + Project\Hello\HelloDispatcher::class, + Project\Welcome\WelcomeDispatcher::class, +]) +``` + +### Temporary Solution + +As a temporary solution, you may reuse the routing function for dispatching: + +```php +Application::build() +// ... +->setDispatching([ + function (Equip\Directory $directory) { + return $directory + ->get('/', Domain\Welcome::class) + ->get('/hello[/{name}]', Domain\Hello::class) + ->post('/hello[/{name}]', Domain\Hello::class) + ; // End of routing + }, +]) +``` + + +### Splitting Dispatchers + +The long term solution for handling dispatching is to add separate classes for +each resource you want to dispatch. Splitting the current routing would become: + +```php +namespace Equip\Project\Hello; + +use Equip\Directory; + +class HelloDispatcher +{ + public function __invoke(Directory $directory) + { + return $directory + ->get('/hello[/{name}]', HelloDomain::class) + ->post('/hello[/{name}]', HelloDomain::class) + ; + } +} +``` + +```php +namespace Equip\Project\Welcome; + +use Equip\Directory; + +class WelcomeDispatcher +{ + public function __invoke(Directory $directory) + { + return $directory->get('/', WelcomeDomain::class); + } +} +``` + +For simplicity, you may only want to have a single dispatcher when starting a +new project and as your application grows you can switch to separate files. + + +### Resource Based Structure + +This also implies a change towards resource-based code structure, as opposed to +type-based structure. Instead of organizing your files by the type: + +``` +src/ + Domain/ + Hello.php + Welcome.php + Exceptions/ + HelloError.php + WelcomeError.php +``` + +Files are logically grouped by the resource they represent: + +``` +src/ + Hello/ + HelloDomain.php + HelloError.php + Welcome/ + WelcomeDomain.php + WelcomeError.php +``` + +It is recommended that your organize your Equip projects this way but not required. +The benefit to doing so that it becomes very easy to determine what code can be +found based on its name. + +If you have common code that is shared between different resource contexts, feel +free to create a "common" directory: + +``` +src/ + Common/ + Filesystem.php + Database.php + Hello/ + ... + Welcome/ + ... +``` diff --git a/src/Application.php b/src/Application.php index ac128de..3a74138 100644 --- a/src/Application.php +++ b/src/Application.php @@ -5,6 +5,7 @@ use Auryn\Injector; use Equip\Configuration\ConfigurationSet; use Equip\Directory; +use Equip\Dispatching\DispatchingSet; use Equip\Middleware\MiddlewareSet; use Relay\Relay; @@ -16,15 +17,17 @@ class Application * @param Injector $injector * @param ConfigurationSet $configuration * @param MiddlewareSet $middleware + * @param DispatchingSet $dispatching * * @return static */ public static function build( Injector $injector = null, ConfigurationSet $configuration = null, - MiddlewareSet $middleware = null + MiddlewareSet $middleware = null, + DispatchingSet $dispatching = null ) { - return new static($injector, $configuration, $middleware); + return new static($injector, $configuration, $middleware, $dispatching); } /** @@ -43,23 +46,26 @@ public static function build( private $middleware; /** - * @var callable|string + * @var DispatchingSet */ - private $routing; + private $dispatching; /** * @param Injector $injector * @param ConfigurationSet $configuration * @param MiddlewareSet $middleware + * @param DispatchingSet $dispatching */ public function __construct( Injector $injector = null, ConfigurationSet $configuration = null, - MiddlewareSet $middleware = null + MiddlewareSet $middleware = null, + DispatchingSet $dispatching = null ) { $this->injector = $injector ?: new Injector; $this->configuration = $configuration ?: new ConfigurationSet; $this->middleware = $middleware ?: new MiddlewareSet; + $this->dispatching = $dispatching ?: new DispatchingSet; } /** @@ -89,15 +95,15 @@ public function setMiddleware(array $middleware) } /** - * Change routing + * Change dispatching * - * @param callable|string $routing + * @param array $dispatching * * @return self */ - public function setRouting($routing) + public function setDispatching(array $dispatching) { - $this->routing = $routing; + $this->dispatching = $this->dispatching->withValues($dispatching); return $this; } @@ -114,7 +120,7 @@ public function run($runner = Relay::class) return $this->injector ->share($this->middleware) - ->prepare(Directory::class, $this->routing) + ->prepare(Directory::class, $this->dispatching) ->execute($runner); } } diff --git a/src/Dispatching/DispatchingSet.php b/src/Dispatching/DispatchingSet.php new file mode 100644 index 0000000..ea7a200 --- /dev/null +++ b/src/Dispatching/DispatchingSet.php @@ -0,0 +1,50 @@ +make($dispatch); + } + $directory = $dispatch($directory); + } + + return $directory; + } + + /** + * @inheritDoc + * + * @throws DispatchingException + * If $classes does not conform to type expectations. + */ + protected function assertValid(array $classes) + { + parent::assertValid($classes); + + foreach ($classes as $dispatching) { + if (!(is_callable($dispatching) || method_exists($dispatching, '__invoke'))) { + throw DispatchingException::notInvokable($dispatching); + } + } + } +} diff --git a/src/Exception/DispatchingException.php b/src/Exception/DispatchingException.php new file mode 100644 index 0000000..b9dd0a8 --- /dev/null +++ b/src/Exception/DispatchingException.php @@ -0,0 +1,25 @@ + Injector::class, 'configuration' => ConfigurationSet::class, 'middleware' => MiddlewareSet::class, - 'routing' => null, + 'dispatching' => DispatchingSet::class, ]; foreach ($props as $name => $expected) { @@ -37,10 +38,6 @@ private function assertApplication($app) $props[$name] = $value; } - - if (!empty($props['routing'])) { - $this->assertTrue(is_callable($props['routing'])); - } } public function testBuild() @@ -54,8 +51,9 @@ public function testCreate() $injector = $this->getMock(Injector::class); $configuration = $this->getMock(ConfigurationSet::class); $middleware = $this->getMock(MiddlewareSet::class); + $dispatching = $this->getMock(DispatchingSet::class); - $app = new Application($injector, $configuration, $middleware); + $app = new Application($injector, $configuration, $middleware, $dispatching); $this->assertApplication($app); } @@ -98,28 +96,35 @@ public function testSetMiddleware() $this->assertApplication($app); } - public function testSetRouting() + public function testSetDispatching() { - $app = new Application(); + $data = [ + function ($directory) { + return $directory; + }, + ]; - // Routing can be a closure ... - $app->setRouting(function () { - }); - $this->assertApplication($app); + $dispatching = $this->getMock(DispatchingSet::class); + $dispatching + ->expects($this->once()) + ->method('withValues') + ->with($data) + ->willReturn(clone $dispatching); + + $app = new Application(null, null, null, $dispatching); + $app->setDispatching($data); - // ... or a callback - $app->setRouting([$this, __FUNCTION__]); $this->assertApplication($app); + } public function testRun() { $injector = $this->getMock(Injector::class); $middleware = $this->getMock(MiddlewareSet::class); + $dispatching = $this->getMock(DispatchingSet::class); $config1 = $this->getMock(ConfigurationInterface::class); $config2 = $this->getMock(ConfigurationInterface::class); - $routing = function () { - }; $config1 ->expects($this->once()) @@ -146,7 +151,7 @@ public function testRun() $injector ->expects($this->once()) ->method('prepare') - ->with(Directory::class, $routing) + ->with(Directory::class, $dispatching) ->willReturnSelf(); $injector @@ -154,9 +159,8 @@ public function testRun() ->method('execute') ->with(Relay::class); - $app = Application::build($injector, null, $middleware); + $app = Application::build($injector, null, $middleware, $dispatching); $app->setConfiguration([get_class($config1), $config2]); - $app->setRouting($routing); $app->run(); } } diff --git a/tests/Dispatching/DispatchingSetTest.php b/tests/Dispatching/DispatchingSetTest.php new file mode 100644 index 0000000..28c7715 --- /dev/null +++ b/tests/Dispatching/DispatchingSetTest.php @@ -0,0 +1,50 @@ +setExpectedExceptionRegExp( + DispatchingException::class, + '/Dispatcher .* is not invokable/i' + ); + + new DispatchingSet([__CLASS__]); + } + + public function testWithValidEntries() + { + $dispatchers = [ + function () { + } + ]; + $dispatching = new DispatchingSet($dispatchers); + $this->assertSame($dispatchers, $dispatching->toArray()); + } + + public function testPrepareDirectory() + { + $dispatchers = [ + function (Directory $directory) { + return $directory->get('/', DomainInterface::class); + } + ]; + + $dispatching = new DispatchingSet($dispatchers); + $directory = new Directory; + + $prepared = $dispatching($directory, $this->getMock(Injector::class)); + + $this->assertInstanceOf(Directory::class, $prepared); + $this->assertNotSame($prepared, $directory); + } +} From fcdbd969daaf68dd6af21d78d7770a875d780da0 Mon Sep 17 00:00:00 2001 From: Woody Gilk Date: Mon, 6 Jun 2016 08:50:19 -0500 Subject: [PATCH 3/6] Start changelog for version 3.0 --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b07c31..9eb45b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ This project adheres to [Semantic Versioning](http://semver.org/). _..._ +## 3.0.0 - ? + +- Replace routing with dispatching, see MIGRATING for details + ## 2.2.0 - ? - Replace generic exceptions with specific exceptions in handlers From c5d44d332808f083881ffe09abf0c55cb6307c2a Mon Sep 17 00:00:00 2001 From: Alex Masterov Date: Sat, 18 Jun 2016 18:14:07 +0300 Subject: [PATCH 4/6] Upgrade PHPUnit version to 5.4 (#58) * Remove PHPUnit from composer, require users to install PHPUnit globally * Upgrade PHPUnit version to 5.4 in Travis CI * Use TestCase::createMock instead of using TestCase::getMock (deprecated) * Use TestCase::setExpectedExceptionRegExp instead of using annotations --- .travis.yml | 23 ++++++++++++--- composer.json | 1 - tests/ActionTest.php | 6 ++-- tests/ApplicationTest.php | 28 +++++++++---------- tests/Configuration/ConfigurationSetTest.php | 22 ++++++++------- tests/Configuration/EnvConfigurationTest.php | 23 ++++++++------- .../Configuration/RedisConfigurationTest.php | 8 ++++-- tests/DirectoryTest.php | 16 ++++++----- tests/DirectoryTestCase.php | 2 +- tests/Dispatching/DispatchingSetTest.php | 2 +- tests/Formatter/PlatesFormatterTest.php | 2 +- tests/Handler/DispatchHandlerTest.php | 19 +++++++------ tests/Handler/ExceptionHandlerTest.php | 2 +- tests/Handler/JsonContentHandlerTest.php | 16 ++++++----- tests/InputTest.php | 4 +-- tests/Middleware/MiddlewareSetTest.php | 2 +- tests/Responder/FormattedResponderTest.php | 4 +-- 17 files changed, 105 insertions(+), 75 deletions(-) diff --git a/.travis.yml b/.travis.yml index e6af653..5e38ab6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,23 +1,38 @@ language: php php: - - 5.5 - 5.6 - 7.0 - hhvm +sudo: false + +cache: + directories: + - $HOME/.composer/cache + - vendor + +git: + depth: 1 + +env: + global: + - PHPUNIT_VERSION="^5.4" + matrix: include: - - php: 5.5 - env: 'COMPOSER_FLAGS="--prefer-stable --prefer-lowest"' + - php: 5.6 + env: + - COMPOSER_FLAGS="--prefer-stable --prefer-lowest" services: - redis-server before_script: - - bash -c 'if [ "$TRAVIS_PHP_VERSION" != "hhvm" ]; then phpenv config-add .travis.php.ini; fi' + - bash -c 'if [ "${TRAVIS_PHP_VERSION}" != "hhvm" ]; then phpenv config-add .travis.php.ini; fi' - travis_retry composer self-update - travis_retry composer update ${COMPOSER_FLAGS} --no-interaction --prefer-source + - travis_retry composer require phpunit/phpunit:${PHPUNIT_VERSION} script: - vendor/bin/phpunit diff --git a/composer.json b/composer.json index d47d763..04e7315 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,6 @@ "josegonzalez/dotenv": "^2.0", "league/plates": "^3.1", "monolog/monolog": "^1.19", - "phpunit/phpunit": "^4.8|^5.0", "zendframework/zend-diactoros": "^1.0.4" }, "suggest": { diff --git a/tests/ActionTest.php b/tests/ActionTest.php index 2bd413a..f4d5d20 100644 --- a/tests/ActionTest.php +++ b/tests/ActionTest.php @@ -14,19 +14,19 @@ class ActionTest extends TestCase { public function testInstance() { - $domain = get_class($this->getMock(DomainInterface::class)); + $domain = get_class($this->createMock(DomainInterface::class)); $action = new Action($domain); $this->assertSame($domain, $action->getDomain()); $this->assertSame(Input::class, $action->getInput()); $this->assertSame(ChainedResponder::class, $action->getResponder()); - $responder = get_class($this->getMock(ResponderInterface::class)); + $responder = get_class($this->createMock(ResponderInterface::class)); $action = new Action($domain, $responder); $this->assertSame($responder, $action->getResponder()); - $input = get_class($this->getMock(InputInterface::class)); + $input = get_class($this->createMock(InputInterface::class)); $action = new Action($domain, null, $input); $this->assertSame($input, $action->getInput()); diff --git a/tests/ApplicationTest.php b/tests/ApplicationTest.php index 79aad75..f123f22 100644 --- a/tests/ApplicationTest.php +++ b/tests/ApplicationTest.php @@ -48,10 +48,10 @@ public function testBuild() public function testCreate() { - $injector = $this->getMock(Injector::class); - $configuration = $this->getMock(ConfigurationSet::class); - $middleware = $this->getMock(MiddlewareSet::class); - $dispatching = $this->getMock(DispatchingSet::class); + $injector = $this->createMock(Injector::class); + $configuration = $this->createMock(ConfigurationSet::class); + $middleware = $this->createMock(MiddlewareSet::class); + $dispatching = $this->createMock(DispatchingSet::class); $app = new Application($injector, $configuration, $middleware, $dispatching); @@ -61,10 +61,10 @@ public function testCreate() public function testSetConfiguration() { $data = [ - $this->getMock(ConfigurationInterface::class), + $this->createMock(ConfigurationInterface::class), ]; - $configuration = $this->getMock(ConfigurationSet::class); + $configuration = $this->createMock(ConfigurationSet::class); $configuration ->expects($this->once()) ->method('withValues') @@ -80,10 +80,10 @@ public function testSetConfiguration() public function testSetMiddleware() { $data = [ - $this->getMock(MiddlewareInterface::class), + $this->createMock(MiddlewareInterface::class), ]; - $middleware = $this->getMock(MiddlewareSet::class); + $middleware = $this->createMock(MiddlewareSet::class); $middleware ->expects($this->once()) ->method('withValues') @@ -104,7 +104,7 @@ function ($directory) { }, ]; - $dispatching = $this->getMock(DispatchingSet::class); + $dispatching = $this->createMock(DispatchingSet::class); $dispatching ->expects($this->once()) ->method('withValues') @@ -120,11 +120,11 @@ function ($directory) { public function testRun() { - $injector = $this->getMock(Injector::class); - $middleware = $this->getMock(MiddlewareSet::class); - $dispatching = $this->getMock(DispatchingSet::class); - $config1 = $this->getMock(ConfigurationInterface::class); - $config2 = $this->getMock(ConfigurationInterface::class); + $injector = $this->createMock(Injector::class); + $middleware = $this->createMock(MiddlewareSet::class); + $dispatching = $this->createMock(DispatchingSet::class); + $config1 = $this->createMock(ConfigurationInterface::class); + $config2 = $this->createMock(ConfigurationInterface::class); $config1 ->expects($this->once()) diff --git a/tests/Configuration/ConfigurationSetTest.php b/tests/Configuration/ConfigurationSetTest.php index 63522c7..b29fca9 100644 --- a/tests/Configuration/ConfigurationSetTest.php +++ b/tests/Configuration/ConfigurationSetTest.php @@ -5,14 +5,16 @@ use Auryn\Injector; use Equip\Configuration\ConfigurationInterface; use Equip\Configuration\ConfigurationSet; +use Equip\Exception\ConfigurationException; use PHPUnit_Framework_TestCase as TestCase; +use stdClass; class ConfigurationSetTest extends TestCase { public function testSet() { - $config = $this->getMock(ConfigurationInterface::class); - $injector = $this->getMock(Injector::class); + $config = $this->createMock(ConfigurationInterface::class); + $injector = $this->createMock(Injector::class); $injector ->expects($this->once()) @@ -34,8 +36,8 @@ public function testSet() public function testSetObject() { - $config = $this->getMock(ConfigurationInterface::class); - $injector = $this->getMock(Injector::class); + $config = $this->createMock(ConfigurationInterface::class); + $injector = $this->createMock(Injector::class); $config ->expects($this->once()) @@ -49,13 +51,13 @@ public function testSetObject() $set->apply($injector); } - /** - * @expectedException Equip\Exception\ConfigurationException - * @expectedExceptionRegExp /class .* must implement ConfigurationInterface/i - */ public function testInvalidClass() { - $set = new ConfigurationSet; - $set = $set->withValue('\stdClass'); + $this->setExpectedExceptionRegExp( + ConfigurationException::class, + '/class .* must implement .*ConfigurationInterface/i' + ); + + (new ConfigurationSet)->withValue(stdClass::class); } } diff --git a/tests/Configuration/EnvConfigurationTest.php b/tests/Configuration/EnvConfigurationTest.php index 435a275..dca44e4 100644 --- a/tests/Configuration/EnvConfigurationTest.php +++ b/tests/Configuration/EnvConfigurationTest.php @@ -4,6 +4,7 @@ use Equip\Configuration\EnvConfiguration; use Equip\Env; +use Equip\Exception\EnvException; use josegonzalez\Dotenv\Loader; class EnvConfigurationTest extends ConfigurationTestCase @@ -42,22 +43,24 @@ public function testApply() $this->destroyEnv(); } - /** - * @expectedException \Equip\Exception\EnvException - * @expectedExceptionMessageRegExp /unable to automatically detect/i - */ public function testUnableToDetect() { - $config = new EnvConfiguration; + $this->setExpectedExceptionRegExp( + EnvException::class, + '/unable to automatically detect/i' + ); + + new EnvConfiguration; } - /** - * @expectedException \Equip\Exception\EnvException - * @expectedExceptionMessageRegExp /environment file .* does not exist/i - */ public function testInvalidRoot() { - $config = new EnvConfiguration('/tmp/bad/path/.env'); + $this->setExpectedExceptionRegExp( + EnvException::class, + '/environment file .* does not exist/i' + ); + + new EnvConfiguration('/tmp/bad/path/.env'); } /** diff --git a/tests/Configuration/RedisConfigurationTest.php b/tests/Configuration/RedisConfigurationTest.php index a358e23..af722d3 100644 --- a/tests/Configuration/RedisConfigurationTest.php +++ b/tests/Configuration/RedisConfigurationTest.php @@ -12,9 +12,13 @@ class RedisConfigurationTest extends TestCase { public function testApply() { + if (!class_exists(Redis::class)) { + $this->markTestSkipped('Redis is not installed'); + } + $injector = new Injector; $injector->delegate(Redis::class, function() { - $redisMock = $this->getMock(Redis::class, ['connect']); + $redisMock = $this->createMock(Redis::class, ['connect']); $redisMock ->expects($this->once()) @@ -24,7 +28,7 @@ public function testApply() }); $config = new RedisConfiguration( - $this->getMock(Env::class) + $this->createMock(Env::class) ); $config->apply($injector); diff --git a/tests/DirectoryTest.php b/tests/DirectoryTest.php index df0e219..571d3e4 100644 --- a/tests/DirectoryTest.php +++ b/tests/DirectoryTest.php @@ -4,6 +4,7 @@ use Equip\Adr\DomainInterface; use Equip\Directory; +use Equip\Exception\DirectoryException; use Equip\Input; use Equip\Structure\Dictionary; @@ -24,13 +25,14 @@ public function testDictionary() $this->assertInstanceOf(Dictionary::class, $this->directory); } - /** - * @expectedException \Equip\Exception\DirectoryException - * @expectedExceptionRegExp /entry .* is not an action/i - */ public function testInvalidAction() { - $directory = $this->directory->withValue('GET /', $this); + $this->setExpectedExceptionRegExp( + DirectoryException::class, + '/Directory entry .* is not an .* instance/i' + ); + + $this->directory->withValue('GET /', $this); } public function testAction() @@ -39,7 +41,7 @@ public function testAction() $directory = $this->directory->action('LIST', '/', $action); $this->assertTrue($directory->hasValue('LIST /')); - $this->assertSame($action, $directory->getValue("LIST /")); + $this->assertSame($action, $directory->getValue('LIST /')); } public function testActionWithDomain() @@ -75,7 +77,7 @@ public function dataHttpMethods() ['PATCH'], ['HEAD'], ['DELETE'], - ['OPTIONS'] + ['OPTIONS'], ]; } } diff --git a/tests/DirectoryTestCase.php b/tests/DirectoryTestCase.php index 801d5d5..5ea87bf 100644 --- a/tests/DirectoryTestCase.php +++ b/tests/DirectoryTestCase.php @@ -40,6 +40,6 @@ protected function getMockAction( */ protected function getMockDomain() { - return $this->getMock(DomainInterface::class); + return $this->createMock(DomainInterface::class); } } diff --git a/tests/Dispatching/DispatchingSetTest.php b/tests/Dispatching/DispatchingSetTest.php index 28c7715..5d98a52 100644 --- a/tests/Dispatching/DispatchingSetTest.php +++ b/tests/Dispatching/DispatchingSetTest.php @@ -42,7 +42,7 @@ function (Directory $directory) { $dispatching = new DispatchingSet($dispatchers); $directory = new Directory; - $prepared = $dispatching($directory, $this->getMock(Injector::class)); + $prepared = $dispatching($directory, $this->createMock(Injector::class)); $this->assertInstanceOf(Directory::class, $prepared); $this->assertNotSame($prepared, $directory); diff --git a/tests/Formatter/PlatesFormatterTest.php b/tests/Formatter/PlatesFormatterTest.php index 5db1347..e5550b1 100644 --- a/tests/Formatter/PlatesFormatterTest.php +++ b/tests/Formatter/PlatesFormatterTest.php @@ -44,7 +44,7 @@ public function testResponse() 'footer' => 'footer' ]; - $payload = $this->getMock(PayloadInterface::class); + $payload = $this->createMock(PayloadInterface::class); $payload->expects($this->any()) ->method('getSetting') diff --git a/tests/Handler/DispatchHandlerTest.php b/tests/Handler/DispatchHandlerTest.php index d6dac1c..dc18058 100644 --- a/tests/Handler/DispatchHandlerTest.php +++ b/tests/Handler/DispatchHandlerTest.php @@ -4,6 +4,7 @@ use EquipTests\DirectoryTestCase; use Equip\Directory; +use Equip\Exception\HttpException; use Equip\Handler\ActionHandler; use Equip\Handler\DispatchHandler; use Zend\Diactoros\Response; @@ -38,12 +39,13 @@ public function testHandle() $this->dispatch($directory, $request, $response, $next); } - /** - * @expectedException \Equip\Exception\HttpException - * @expectedExceptionRegExp /cannot find any resource at/i - */ public function testNotFoundException() { + $this->setExpectedExceptionRegExp( + HttpException::class, + '/cannot find any resource at/i' + ); + $handler = new DispatchHandler($this->directory); $request = $this->getRequest('GET', '/'); $response = new Response; @@ -58,12 +60,13 @@ function ($request, $response) { ); } - /** - * @expectedException \Equip\Exception\HttpException - * @expectedExceptionRegExp /cannot access resource .* using method/i - */ public function testMethodNotAllowedException() { + $this->setExpectedExceptionRegExp( + HttpException::class, + '/cannot access resource .* using method/i' + ); + $handler = new DispatchHandler($this->directory); $request = $this->getRequest('POST'); $response = new Response; diff --git a/tests/Handler/ExceptionHandlerTest.php b/tests/Handler/ExceptionHandlerTest.php index 85f8026..cecfcf3 100644 --- a/tests/Handler/ExceptionHandlerTest.php +++ b/tests/Handler/ExceptionHandlerTest.php @@ -111,7 +111,7 @@ class MockMonologConfiguration extends TestCase implements ConfigurationInterfac public function apply(Injector $injector) { $injector->delegate(LoggerInterface::class, function () { - $loggerMock = $this->getMock(LoggerInterface::class); + $loggerMock = $this->createMock(LoggerInterface::class); $loggerMock ->expects($this->atLeastOnce()) diff --git a/tests/Handler/JsonContentHandlerTest.php b/tests/Handler/JsonContentHandlerTest.php index 855d525..130e516 100644 --- a/tests/Handler/JsonContentHandlerTest.php +++ b/tests/Handler/JsonContentHandlerTest.php @@ -1,7 +1,7 @@ setExpectedExceptionRegExp( + HttpException::class, + '/json.* syntax error/i', + 400 + ); + $request = $this->getRequest( $mime = 'application/json', $body = '{not json}' ); $response = new Response; $handler = new JsonContentHandler; - $resolved = $handler($request, $response, function ($req, $res) { + + $handler($request, $response, function ($req, $res) { $this->fail('Handler callback unexpectedly invoked'); }); } diff --git a/tests/InputTest.php b/tests/InputTest.php index 24971f1..bd019eb 100644 --- a/tests/InputTest.php +++ b/tests/InputTest.php @@ -68,7 +68,7 @@ public function testParsedBody($expected, $body) public function testUploadedFiles() { $files = [ - 'file' => $this->getMock(UploadedFileInterface::class), + 'file' => $this->createMock(UploadedFileInterface::class), ]; $request = new ServerRequest; @@ -123,7 +123,7 @@ public function testMerge() $this->assertSame($value, $this->execute($request)); $value = [ - 'merge' => $this->getMock(UploadedFileInterface::class), + 'merge' => $this->createMock(UploadedFileInterface::class), ]; $request = $request->withParsedBody($value); $this->assertSame($value, $this->execute($request)); diff --git a/tests/Middleware/MiddlewareSetTest.php b/tests/Middleware/MiddlewareSetTest.php index 57fa0b4..39abfde 100644 --- a/tests/Middleware/MiddlewareSetTest.php +++ b/tests/Middleware/MiddlewareSetTest.php @@ -22,7 +22,7 @@ public function testWithInvalidEntries() public function testWithValidEntries() { $middleware = [ - $this->getMock(MiddlewareInterface::class), + $this->createMock(MiddlewareInterface::class), function () { } ]; diff --git a/tests/Responder/FormattedResponderTest.php b/tests/Responder/FormattedResponderTest.php index 563ef5f..faabdee 100644 --- a/tests/Responder/FormattedResponderTest.php +++ b/tests/Responder/FormattedResponderTest.php @@ -121,8 +121,8 @@ public function testResponse() public function testEmptyPayload() { $payload = new Payload; - $request = $this->getMock(ServerRequestInterface::class); - $response = $this->getMock(ResponseInterface::class); + $request = $this->createMock(ServerRequestInterface::class); + $response = $this->createMock(ResponseInterface::class); $returned = call_user_func($this->responder, $request, $response, $payload); $this->assertSame($returned, $response); } From 0db9791d15bd0f9dd960f8bc94a312087bff6b50 Mon Sep 17 00:00:00 2001 From: Woody Gilk Date: Tue, 16 Aug 2016 10:27:47 -0500 Subject: [PATCH 5/6] Make actions first class citizens (#75) According to the [intent of ADR][1], the Action part of the triad is meant to be a completely custom class that provides a boundary between HTTP and domain logic. The action is aware of the domain and knows how to direct output through a responder. This is not how Equip has been treating the domain. Our attempts to make Domain pseudo-HTTP-aware with Payload and Input has not been very successful and is probably the least concise bit of the framework. [1]: http://pmjones.io/adr/ Also includes changes from: * Remove payload from formatting (#77) * Restore content negotiation (#78) * Add documentation for v3 changes (#79) * Remove payload and input classes (#80) * Remove content negotiation (#84) --- composer.json | 1 - docs/index.md | 267 +++++++++++------- src/Action.php | 79 ------ src/Configuration/PayloadConfiguration.php | 22 -- .../PlatesResponderConfiguration.php | 17 -- src/Contract/ActionInterface.php | 25 ++ src/Contract/InputInterface.php | 28 ++ src/Directory.php | 62 ++-- src/Exception/DirectoryException.php | 8 +- src/Formatter/FormatterInterface.php | 10 +- src/Formatter/JsonFormatter.php | 6 +- src/Formatter/PlatesFormatter.php | 30 +- src/Handler/ActionHandler.php | 61 +--- src/Input.php | 58 ---- src/Payload.php | 151 ---------- src/Responder/ChainedResponder.php | 67 ----- src/Responder/FormattedResponder.php | 168 ----------- src/Responder/RedirectResponder.php | 28 -- src/Responder/StatusResponder.php | 70 ----- tests/ActionTest.php | 35 --- .../PayloadConfigurationTest.php | 24 -- .../PlatesResponderConfigurationTest.php | 32 --- tests/DirectoryTest.php | 20 +- tests/DirectoryTestCase.php | 45 --- tests/Dispatching/DispatchingSetTest.php | 4 +- tests/Fake/FakeDomain.php | 17 -- tests/Fake/FakeExceptionHandler.php | 17 -- tests/Fake/FakeInput.php | 9 - tests/Fake/FakeResponder.php | 9 - tests/Formatter/HtmlFormatterTest.php | 1 - tests/Formatter/JsonFormatterTest.php | 7 +- tests/Formatter/PlatesFormatterTest.php | 17 +- tests/Handler/ActionHandlerTest.php | 15 +- tests/Handler/DispatchHandlerTest.php | 12 +- tests/InputTest.php | 155 ---------- tests/PayloadTest.php | 76 ----- tests/Responder/ChainedResponderTest.php | 96 ------- tests/Responder/FormattedResponderTest.php | 129 --------- tests/Responder/RedirectResponderTest.php | 46 --- tests/Responder/StatusResponderTest.php | 49 ---- 40 files changed, 309 insertions(+), 1664 deletions(-) delete mode 100644 src/Action.php delete mode 100644 src/Configuration/PayloadConfiguration.php delete mode 100644 src/Configuration/PlatesResponderConfiguration.php create mode 100644 src/Contract/ActionInterface.php create mode 100644 src/Contract/InputInterface.php delete mode 100644 src/Input.php delete mode 100644 src/Payload.php delete mode 100644 src/Responder/ChainedResponder.php delete mode 100644 src/Responder/FormattedResponder.php delete mode 100644 src/Responder/RedirectResponder.php delete mode 100644 src/Responder/StatusResponder.php delete mode 100644 tests/ActionTest.php delete mode 100644 tests/Configuration/PayloadConfigurationTest.php delete mode 100644 tests/Configuration/PlatesResponderConfigurationTest.php delete mode 100644 tests/DirectoryTestCase.php delete mode 100644 tests/Fake/FakeDomain.php delete mode 100644 tests/Fake/FakeExceptionHandler.php delete mode 100644 tests/Fake/FakeInput.php delete mode 100644 tests/Fake/FakeResponder.php delete mode 100644 tests/InputTest.php delete mode 100644 tests/PayloadTest.php delete mode 100644 tests/Responder/ChainedResponderTest.php delete mode 100644 tests/Responder/FormattedResponderTest.php delete mode 100644 tests/Responder/RedirectResponderTest.php delete mode 100644 tests/Responder/StatusResponderTest.php diff --git a/composer.json b/composer.json index 7a2920f..b81208f 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,6 @@ "minimum-stability": "stable", "require": { "php": ">=5.5", - "equip/adr": "^2.0", "equip/config": "^1.0.2", "equip/structure": "^1.0", "filp/whoops": "^2.0", diff --git a/docs/index.md b/docs/index.md index 80987b1..b06ec9d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -107,7 +107,6 @@ The following configurations are typically used by default: * [`AurynConfiguration`](https://github.com/equip/framework/blob/master/src/Configuration/AurynConfiguration.php) - Use the `Injector` instance as a singleton and to resolve [actions](https://github.com/pmjones/adr#controller-vs-action) * [`DiactorosConfiguration`](https://github.com/equip/framework/blob/master/src/Configuration/DiactorosConfiguration.php) - Use [Diactoros](https://github.com/zendframework/zend-diactoros/) for the framework [PSR-7](http://www.php-fig.org/psr/psr-7/) implementation -* [`PayloadConfiguration`](https://github.com/equip/framework/blob/master/src/Configuration/PayloadConfiguration.php) - Use the default Equip class as the implementation for [`PayloadInterface`](https://github.com/equip/adr/blob/master/src/PayloadInterface.php) * [`RelayConfiguration`](https://github.com/equip/framework/blob/master/src/Configuration/RelayConfiguration.php) - Use [Relay](http://relayphp.com) for the framework middleware dispatcher * [`WhoopsConfiguration`](https://github.com/equip/framework/blob/master/src/Configuration/WhoopsConfiguration.php) - Use [Whoops](http://filp.github.io/whoops/) for handling exceptions @@ -118,7 +117,6 @@ The following configurations are available but not used by default: * [`EnvConfiguration`](https://github.com/equip/framework/blob/master/src/Configuration/EnvConfiguration.php) - Use [Dotenv](https://github.com/josegonzalez/php-dotenv) to populate the content of [`Env`](https://github.com/equip/framework/blob/master/src/Env.php) * [`MonologConfiguration`](https://github.com/equip/framework/blob/master/src/Configuration/MonologConfiguration.php) - Use [Monolog](https://github.com/Seldaek/monolog/) for the framework [PSR-3](http://www.php-fig.org/psr/psr-3/) implementation * [`PlatesConfiguration`](https://github.com/equip/framework/blob/master/src/Configuration/PlatesConfiguration.php) - Configure the [Plates](http://platesphp.com/) template engine -* [`PlatesResponderConfiguration`](https://github.com/equip/framework/blob/master/src/Configuration/PlatesResponderConfiguration.php) - Use [Plates](http://platesphp.com/) as the default [responder](#responders) * [`RedisConfiguration`](https://github.com/equip/framework/blob/master/src/Configuration/RedisConfiguration.php) - Use [Redis](http://redis.io) for in-memory store #### Setting The Env File @@ -163,7 +161,6 @@ Equip\Application::build() ->setConfiguration([ Equip\Configuration\AurynConfiguration::class, Equip\Configuration\DiactorosConfiguration::class, - Equip\Configuration\PayloadConfiguration::class, Equip\Configuration\RelayConfiguration::class, Equip\Configuration\WhoopsConfiguration::class, ]) @@ -232,23 +229,24 @@ Equip\Application::build() Equip uses [FastRoute](https://github.com/nikic/FastRoute) internally for routing. As such, it uses that library's URI pattern syntax; see [its documentation](https://github.com/nikic/FastRoute#defining-routes) for more details. -The directory maps URIs to the corresponding [domain](https://github.com/pmjones/adr#model-vs-domain) that the should be used. This is implemented in the Equip [`Directory`](https://github.com/equip/framework/blob/master/src/Directory.php) class. Here is an example of what configuring an instance of it could look like: +The directory maps URIs to the corresponding [action](#actions) that should be used. This is implemented in the Equip [`Directory`](https://github.com/equip/framework/blob/master/src/Directory.php) class. Here is an example of what configuring an instance of it could look like: ```php -use Acme\Domain; +use Acme\Action; Equip\Application::build() // ... ->setRouting(function (Equip\Directory $directory) { return $directory - ->any('/', Domain\Providers::class) - ->get('/providers', Domain\GetProviders::class) - ->get('/providers/{provider}', Domain\GetProvider::class) - ->post('/providers/{provider}', Domain\SynchronizeProvider::class) - ->post('/providers/{provider}/connection', Domain\ActivateProvider::class) - ->delete('/providers/{provider}/connection', Domain\DeactivateProvider::class) - ->get('/providers/{provider}/configuration', Domain\GetProviderConfiguration::class) - ->put('/providers/{provider}/configuration', Domain\ChangeProviderConfiguration::class) + ->any('/', Action\Providers::class) + ->get('/providers', Action\ListProviders::class) + ->get('/providers/{provider}', Action\GetProvider::class) + ->post('/providers/{provider}', Action\CreateProvider::class) + ->put('/providers/{provider}', Action\UpdateProvider::class) + ->post('/providers/{provider}/connection', Action\ActivateProvider::class) + ->delete('/providers/{provider}/connection', Action\DeactivateProvider::class) + ->get('/providers/{provider}/configuration', Action\GetProviderConfiguration::class) + ->put('/providers/{provider}/configuration', Action\UpdateProviderConfiguration::class) ; // End of routing }) ->run(); @@ -256,19 +254,6 @@ Equip\Application::build() *It is very important to remember that __the `Directory` object is immutable__! You must __always__ return the directory or changes will be lost.* -It is also possible to provide an [`Action`](https://github.com/equip/framework/blob/master/src/Action.php) object instead of a domain class if you want to modify the responder or input class that will be used to handle the action: - -```php -$directory->get('/login', new Equip\Action( - Domain\Login::class, - Acme\Responder::class, - Acme\Input::class -)); -``` - -If an `Action` object is not provided one will be constructed with the provided `Domain` reference instead. - - #### Object Routing An alternative way to implement routing configuration involves using an [invokable](http://php.net/manual/en/language.oop5.magic.php#object.invoke) object. This encapsulation allows for niceties such as non-public methods that can make routing code more concise. @@ -277,7 +262,7 @@ An alternative way to implement routing configuration involves using an [invokab // src/Routing.php namespace Acme; -use Acme\Domain; +use Acme\Action; use Equip\Directory; class Routing @@ -285,13 +270,10 @@ class Routing public function __invoke(Directory $directory) { return $directory - ->get('/providers', Domain\GetProviders::class) - ->get('/providers/{provider}', Domain\GetProvider::class) - ->post('/providers/{provider}', Domain\SynchronizeProvider::class) - ->post('/providers/{provider}/connection', Domain\ActivateProvider::class) - ->delete('/providers/{provider}/connection', Domain\DeactivateProvider::class) - ->get('/providers/{provider}/configuration', Domain\GetProviderConfiguration::class) - ->put('/providers/{provider}/configuration', Domain\ChangeProviderConfiguration::class) + ->any('/', Action\Providers::class) + ->get('/providers', Action\ListProviders::class) + // ... + ->put('/providers/{provider}/configuration', Action\UpdateProviderConfiguration::class) ; // End of routing } } @@ -335,11 +317,11 @@ For a Equip application to handle requests properly it will require some middlew The following middlewares are typically used by default, in this order: -* [`Relay\Middleware\ResponseSender`](https://github.com/relayphp/Relay.Middleware/blob/master/src/ResponseSender.php) - Outputs data from the [PSR-7 Response object](https://github.com/php-fig/http-message/blob/master/src/ResponseInterface.php) to be sent back to the client -* [`Equip\Handler\ExceptionHandler`](https://github.com/equip/framework/blob/master/src/Handler/ExceptionHandler.php) - Handles exceptions thrown by subsequent middlewares and domains by [logging the exception](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md#13-context) and returning an appropriate application-level response -* [`Equip\Handler\RouteHandler`](https://github.com/equip/framework/blob/master/src/Handler/RouteHandler.php) - Resolves the request route to the corresponding action to execute -* [`Equip\Handler\ContentHandler`](https://github.com/equip/framework/blob/master/src/Handler/ContentHandler.php) - Parses request bodies encoded in common formats and makes the parsed version available via the `getParsedBody()` method of the [PSR-7 Request object](https://github.com/php-fig/http-message/blob/master/src/ServerRequestInterface.php) -* [`Equip\Handler\ActionHandler`](https://github.com/equip/framework/blob/master/src/Handler/ActionHandler.php) - Invokes the [domain](https://github.com/pmjones/adr#model-vs-domain) corresponding to the resolved [action](https://github.com/pmjones/adr#controller-vs-action), applies the [responder](https://github.com/pmjones/adr#view-vs-responder) to the resulting payload, and returns the resulting response +* [`Relay\Middleware\ResponseSender`](https://github.com/relayphp/Relay.Middleware/blob/master/src/ResponseSender.php) - Outputs data from the [PSR-7 Response object](https://github.com/php-fig/http-message/blob/master/src/ResponseInterface.php) to be sent back to the client. +* [`Equip\Handler\ExceptionHandler`](https://github.com/equip/framework/blob/master/src/Handler/ExceptionHandler.php) - Handles exceptions thrown by subsequent middlewares and domains by [logging the exception](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md#13-context) and returning an appropriate application response. +* [`Equip\Handler\RouteHandler`](https://github.com/equip/framework/blob/master/src/Handler/RouteHandler.php) - Resolves the request route to the corresponding action to execute. +* [`Equip\Handler\ContentHandler`](https://github.com/equip/framework/blob/master/src/Handler/ContentHandler.php) - Parses request bodies encoded in common formats and makes the parsed version available via the `getParsedBody()` method of the [PSR-7 Request object](https://github.com/php-fig/http-message/blob/master/src/ServerRequestInterface.php). +* [`Equip\Handler\ActionHandler`](https://github.com/equip/framework/blob/master/src/Handler/ActionHandler.php) - Provides a boundary between HTTP details and your [domain logic](https://en.wikipedia.org/wiki/Business_logic). This is the entry point into the [ADR triad](http://pmjones.io/adr/) that invokes [actions](#actions) to read the request and populate the HTTP response. #### Custom Middleware @@ -382,110 +364,185 @@ Equip\Application::build() *Typically you will want to place your custom middleware immediately before the `ActionHandler`.* -## Domains +## Actions -[Domain](https://github.com/pmjones/adr#model-vs-domain) classes are the application entry point into your project-specific code. They implement [`DomainInterface`](https://github.com/equip/domain/blob/master/src/DomainInterface.php), which contains a single method `__invoke()` that takes in an array and returns an instance of a class implementing [`PayloadInterface`](https://github.com/equip/domain/blob/master/src/PayloadInterface.php). +Actions provide the boundary between the HTTP request/response life cycle and your domain logic. All actions must implement [`ActionInterface`](https://github.com/equip/framework/blob/master/src/Contracts/ActionInterface.php), which contains a single `__invoke()` method that takes a request and a response and returns a modified response. -The array accepted by `__invoke()` is created internally via the Equip [`Input`](https://github.com/equip/adr/blob/master/src/Input.php) class, which aggregates data from the request in a fashion similar to how PHP itself aggregates request data into the [`$_REQUEST`](http://php.net/manual/en/reserved.variables.request.php) superglobal. +How you choose to implement your actions is entirely up to you. The following is an example of how a user login action could be defined. -Equip provides a native implementation of [`PayloadInterface`](https://github.com/equip/domain/blob/master/src/PayloadInterface.php) in the form of its [`Payload`](https://github.com/equip/framework/blob/master/src/Payload.php) class. Once the domain class has returned the payload instance, Equip then passes it off to the appropriate [responder](https://github.com/pmjones/adr#view-vs-responder) to be used in constructing the application response. - -Rather than having the domain class directly instantiate [`Payload`](https://github.com/equip/framework/blob/master/src/Payload.php) or another implementation of [`PayloadInterface`](https://github.com/equip/domain/blob/master/src/PayloadInterface.php), it's recommended that you make domain classes accept an initial payload instance as a constructor parameter, ideally [typehinted](http://php.net/manual/en/functions.arguments.php#functions.arguments.type-declaration) against [`PayloadInterface`](https://github.com/equip/domain/blob/master/src/PayloadInterface.php). This allows domains to be unit tested independently of any particular payload implementation. - -Here's an example of a domain class. +### Action Example ```php -namespace Acme\Domain; +namespace Acme\Action; -use Equip\Adr\DomainInterface; -use Equip\Adr\PayloadInterface; +use Acme\Domain\Authentication; +use Acme\Input\LoginInput; +use Equip\Contract\ActionInterface; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; -class Foo implements DomainInterface +class LoginAction implements ActionInterface { - protected $pdo; - protected $payload; + /** + * @var Authentication + */ + private $auth; + + /** + * @var LoginResponder + */ + private $responder; public function __construct( - \PDO $pdo, - PayloadInterface $payload + Authentication $auth, + LoginResponder $responder ) { - $this->pdo = $pdo; - $this->payload = $payload; + $this->auth = $auth; + $this->responder = $responder; } - public function __invoke(array $input) + public function __invoke(RequestInterface $request, ResponseInterface $response) { - // ... - return $this->payload; + $input = $this->input($request); + $errors = $this->auth->validate($input); + + if (!empty($errors)) { + return $this->responder->incomplete($response, $errors); + } + + if (!$this->auth->canLogin($input)) { + return $this->responder->invalid($response, $input->toArray(true)); + } + + $token = $this->auth->token($input); + + return $this->responder->success($response, $token); + } + + private function input(ServerRequestInterface $request) + { + $body = $request->getParsedBody(); + + return new LoginInput($body); } } ``` -Note that the constructor of this domain class declares two parameters, a `\PDO` instance and a payload instance. If a request is made for the URI corresponding to this domain class in the [router configuration](#router), Equip will use the [Auryn configuration](#dependency-injection-container) to instantiate the domain class with the dependencies declared in its constructor. Typically, the constructor is used to store references to dependencies in instance properties so as to be able to use them later in `__invoke()`. - -Also note that `__invoke()` returns the payload. The core Equip implementation [`Payload`](https://github.com/equip/framework/blob/master/src/Payload.php) provides an immutable implementation of [`PayloadInterface`](https://github.com/equip/adr/blob/master/src/PayloadInterface.php), which also allows for code like this: +This action can only be used with a server request that contains a body. It uses a domain-specific class `Authentication` that contains this application's rules for handling logins. It operates on a value object called `LoginInput` that contains this logic: ```php -return $this->payload - ->withStatus(Payload::OK) - ->withOutput(['foo' => 'bar']); -``` +namespace Acme\Input; + +class LoginInput +{ + private $email; + private $password; + + public function __construct(array $input) + { + if (!empty($input['email'])) { + $this->email = filter_var($input['email'], FILTER_VALIDATE_EMAIL); + } + if (!empty($input['password'])) { + $this->password = $input['password']; + } + } + + public function email() + { + return $this->email; + } -## Responders + public function password() + { + return $this->password; + } -[Responders](https://github.com/pmjones/adr#view-vs-responder) accept the payload returned by the domain and return a [PSR-7 Response object](https://github.com/php-fig/http-message/blob/master/src/ResponseInterface.php). They implement [`ResponderInterface`](https://github.com/equip/adr/blob/master/src/ResponderInterface.php), which like [`DomainInterface`](https://github.com/equip/domain/blob/master/src/DomainInterface.php) declares a single method `__invoke()`. Instead of a third callable parameter, however, it receives an instance of [`PayloadInterface`](https://github.com/equip/domain/blob/master/src/PayloadInterface.php). + public function toArray($public = false) + { + $values = get_object_vars($this); -Equip provides a few native responder implementations. + if ($public) { + $values['password'] = '*********'; + } -### Chained Responder + return $values; + } +} +``` -[`ChainedResponder`](https://github.com/equip/framework/blob/master/src/Responder/ChainedResponder.php) is the default responder which allows multiple responders to be applied to the same response instance. This is intended to allow for [separation of concerns](https://en.wikipedia.org/wiki/Separation_of_concerns) in configuring different areas of the response. The `ChainedResponder` extends [`Equip\Structure\Set`](https://github.com/equip/structure/blob/master/src/Set.php). +The details of the `Authentication` class are not important here. Just know that each of its methods take `LoginInput` as their only parameter and operate on the values within it. -By default [`ChainedResponder`](https://github.com/equip/framework/blob/master/src/Responder/ChainedResponder.php) includes [all the default responders](https://github.com/equip/framework/blob/master/src/Responder). Responders can be added using its `withValue()` method or overwritten entirely using its `withValues()` method. +Finally, we have a custom responder that handles every possible state of the login action: incomplete, invalid, and success. -### Formatted Responder +```php +namespace Acme\Action; -[`FormattedResponder`](https://github.com/equip/framework/blob/master/src/Responder/FormattedResponder.php) uses the [Negotiation](https://github.com/willdurand/negotiation) library to support [content negotiation](https://en.wikipedia.org/wiki/Content_negotiation). When a desirable format has been founded, it uses an appropriate implementation of [`FormatterInterface`](https://github.com/equip/framework/blob/master/src/Formatter/FormatterInterface.php) to encode the payload data and return it as a string. The `FormattedResponder` extends [`Equip\Structure\Dictionary`](https://github.com/equip/structure/blob/master/src/Dictionary.php). +use Equip\Formatter\JsonFormatter; +use Psr\Http\Message\ResponseInterface; -Here are the formatter implementations that are natively supported: +class LoginResponder +{ + /** + * @var JsonFormatter + */ + private $formatter; -* [`JsonFormatter`](https://github.com/equip/framework/blob/master/src/Formatter/JsonFormatter.php) - Encodes the payload as [JSON](http://www.json.org/) -* [`PlatesFormatter`](https://github.com/equip/framework/blob/master/src/Formatter/PlatesFormatter.php) - Applies the payload data to a [Plates](http://platesphp.com/) template specified in the payload and returns the result + public function __construct( + JsonFormatter $formatter + ) { + $this->formatter = $formatter; + } -By default [`FormattedResponder`](https://github.com/equip/framework/blob/master/src/Responder/FormattedResponder.php) includes `JsonFormatter`. Responders can be added using its `withValue()` method or overwritten entirely using its `withValues()` method. + public function incomplete(ResponseInterface $response, array $errors) + { + return $this + ->write($response, $errors) + ->withStatus(400); + } -### Redirect + public function invalid(ResponseInterface $response, array $input) + { + return $this + ->write($response, [ + 'error' => 'Could not validate credentials', + 'input' => $input, + ]) + ->withStatus(403); + } -[`RedirectResponder`](https://github.com/equip/framework/blob/master/src/Responder/RedirectResponder.php) allows a redirection to be embedded in the payload. In order to activate it requires a `redirect` message to be attached to the [`PayloadInterface`](https://github.com/equip/adr/blob/master/src/PayloadInterface.php). Optionally the `status` can be set when a [`302 Found`](https://en.wikipedia.org/wiki/HTTP_302) is not the correct response. + public function success(ResponseInterface $response, $token) + { + return $this + ->write($response, compact('token')) + ->withStatus(200); + } + + private function write(ResponseInterface $response, $output) + { + $body = $response->getBody(); -Modifying the payload to trigger a redirect is very easy: + $body->write($this->formatter->format($output)); -```php -// For a basic redirect -return $payload->withMessages([ - 'redirect' => '/login', -]); - -// For a permanent redirect, also set "status" -return $payload->withMessages([ - 'redirect' => '/permalink', - 'status' => 301, -]); + return $response; + } +} ``` -### Default Setup +This simple responder has separate methods for each possible state that take the exact parameters necessary to generate the response. The response content will always be in JSON format. -Actions, responders, and formatters work together to generate a response. By default `Action` instances created by [routing](#routing) use [`ChainedResponder`](https://github.com/equip/framework/blob/master/src/Responder/ChainedResponder.php) as the responder. By using a custom action you can change the responder for that action. By default the [`ChainedResponder`](https://github.com/equip/framework/blob/master/src/Responder/ChainedResponder.php) includes the [`FormattedResponder`](https://github.com/equip/framework/blob/master/src/Responder/FormattedResponder.php) which delegates formatting to [`JsonFormatter`](https://github.com/equip/framework/blob/master/src/Formatter/JsonFormatter.php). +This loose structure around actions, input, and responders allows each project to define exactly the right response for the application. There are no hard rules about how the actions are defined, so long as each takes a request and produces a response. -### Using Plates +## Formatters -Using [`PlatesFormatter`](https://github.com/equip/framework/blob/master/src/Formatter/PlatesFormatter.php) requires changing the formatters used by [`FormattedResponder`](https://github.com/equip/framework/blob/master/src/Responder/FormattedResponder.php). The easiest way to do this is by using the `PlatesResponderConfiguration` as in the example below: +Equip includes formatters that can be used in responders to simplify the formatting of domain output for the response: -```php -Equip\Application::build() -->setConfiguration([ - // ... - Equip\Configuration\PlatesResponderConfiguration::class -]) -// ... -``` +* [`JsonFormatter`](https://github.com/equip/framework/blob/master/src/Formatter/JsonFormatter.php) - Encodes the content as [JSON](http://www.json.org/). +* [`PlatesFormatter`](https://github.com/equip/framework/blob/master/src/Formatter/PlatesFormatter.php) - Encodes the content using a [Plates](http://platesphp.com/) template set by the responder. + +All formatters must implement [`FormatterInterface`](https://github.com/equip/framework/blob/master/src/Formatter/FormatterInterface.php). + +### Content Negotiation + +Equip does not include any form of [content negotiation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation). However, all formatters include a static `accepts()` method that returns a list of the content types supported by the formatter. This list can be used to help determine which formatter supports a requested content type. diff --git a/src/Action.php b/src/Action.php deleted file mode 100644 index 6a661da..0000000 --- a/src/Action.php +++ /dev/null @@ -1,79 +0,0 @@ -domain = $domain; - - if ($responder) { - $this->responder = $responder; - } - - if ($input) { - $this->input = $input; - } - } - - /** - * Returns the domain specification. - * - * @return DomainInterface - */ - public function getDomain() - { - return $this->domain; - } - - /** - * Returns the responder specification. - * - * @return ResponderInterface - */ - public function getResponder() - { - return $this->responder; - } - - /** - * Returns the input specification. - * - * @return InputInterface - */ - public function getInput() - { - return $this->input; - } -} diff --git a/src/Configuration/PayloadConfiguration.php b/src/Configuration/PayloadConfiguration.php deleted file mode 100644 index 1809266..0000000 --- a/src/Configuration/PayloadConfiguration.php +++ /dev/null @@ -1,22 +0,0 @@ -alias( - PayloadInterface::class, - Payload::class - ); - } -} - diff --git a/src/Configuration/PlatesResponderConfiguration.php b/src/Configuration/PlatesResponderConfiguration.php deleted file mode 100644 index 4fed65d..0000000 --- a/src/Configuration/PlatesResponderConfiguration.php +++ /dev/null @@ -1,17 +0,0 @@ -prepare(FormattedResponder::class, function (FormattedResponder $responder) { - return $responder->withValue(PlatesFormatter::class, 1.0); - }); - } -} diff --git a/src/Contract/ActionInterface.php b/src/Contract/ActionInterface.php new file mode 100644 index 0000000..f3b6b70 --- /dev/null +++ b/src/Contract/ActionInterface.php @@ -0,0 +1,25 @@ +action(self::ANY, $path, $domainOrAction); + return $this->action(self::ANY, $path, $action); } /** * @param string $path - * @param string|Action $domainOrAction + * @param string $action * * @return static */ - public function get($path, $domainOrAction) + public function get($path, $action) { - return $this->action(self::GET, $path, $domainOrAction); + return $this->action(self::GET, $path, $action); } /** * @param string $path - * @param string|Action $domainOrAction + * @param string $action * * @return static */ - public function post($path, $domainOrAction) + public function post($path, $action) { - return $this->action(self::POST, $path, $domainOrAction); + return $this->action(self::POST, $path, $action); } /** * @param string $path - * @param string|Action $domainOrAction + * @param string $action * * @return static */ - public function put($path, $domainOrAction) + public function put($path, $action) { - return $this->action(self::PUT, $path, $domainOrAction); + return $this->action(self::PUT, $path, $action); } /** * @param string $path - * @param string|Action $domainOrAction + * @param string $action * * @return static */ - public function patch($path, $domainOrAction) + public function patch($path, $action) { - return $this->action(self::PATCH, $path, $domainOrAction); + return $this->action(self::PATCH, $path, $action); } /** * @param string $path - * @param string|Action $domainOrAction + * @param string $action * * @return static */ - public function head($path, $domainOrAction) + public function head($path, $action) { - return $this->action(self::HEAD, $path, $domainOrAction); + return $this->action(self::HEAD, $path, $action); } /** * @param string $path - * @param string|Action $domainOrAction + * @param string $action * * @return static */ - public function delete($path, $domainOrAction) + public function delete($path, $action) { - return $this->action(self::DELETE, $path, $domainOrAction); + return $this->action(self::DELETE, $path, $action); } /** * @param string $path - * @param string|Action $domainOrAction + * @param string $action * * @return static */ - public function options($path, $domainOrAction) + public function options($path, $action) { - return $this->action(self::OPTIONS, $path, $domainOrAction); + return $this->action(self::OPTIONS, $path, $action); } /** * @param string $method * @param string $path - * @param string|Action $domainOrAction + * @param string $action * * @return static */ - public function action($method, $path, $domainOrAction) + public function action($method, $path, $action) { - if ($domainOrAction instanceof Action) { - $action = $domainOrAction; - } else { - $action = new Action($domainOrAction); - } - return $this->withValue(sprintf('%s %s', $method, $path), $action); } @@ -178,7 +172,7 @@ protected function assertValid(array $data) parent::assertValid($data); foreach ($data as $value) { - if (!is_object($value) || !$value instanceof Action) { + if (!is_subclass_of($value, ActionInterface::class)) { throw DirectoryException::invalidEntry($value); } } diff --git a/src/Exception/DirectoryException.php b/src/Exception/DirectoryException.php index bc8c614..ea98cb7 100644 --- a/src/Exception/DirectoryException.php +++ b/src/Exception/DirectoryException.php @@ -2,13 +2,13 @@ namespace Equip\Exception; -use Equip\Action; +use Equip\Contract\ActionInterface; use InvalidArgumentException; class DirectoryException extends InvalidArgumentException { /** - * @param mixed $value + * @param string|object $value * * @return static */ @@ -19,9 +19,9 @@ public static function invalidEntry($value) } return new static(sprintf( - 'Directory entry `%s` is not an `%s` instance', + 'Directory entry `%s` must be an `%s` instance', $value, - Action::class + ActionInterface::class )); } } diff --git a/src/Formatter/FormatterInterface.php b/src/Formatter/FormatterInterface.php index 85b6198..82dfa55 100644 --- a/src/Formatter/FormatterInterface.php +++ b/src/Formatter/FormatterInterface.php @@ -2,8 +2,6 @@ namespace Equip\Formatter; -use Equip\Adr\PayloadInterface; - interface FormatterInterface { /** @@ -14,18 +12,18 @@ interface FormatterInterface public static function accepts(); /** - * Get the content type of the response body. + * Get the content type this formatter will generate. * * @return string */ public function type(); /** - * Get the response body from the payload. + * Get the formatted version of provided content. * - * @param PayloadInterface $payload + * @param mixed $content * * @return string */ - public function body(PayloadInterface $payload); + public function format($content); } diff --git a/src/Formatter/JsonFormatter.php b/src/Formatter/JsonFormatter.php index 61a9e98..60a639d 100644 --- a/src/Formatter/JsonFormatter.php +++ b/src/Formatter/JsonFormatter.php @@ -2,8 +2,6 @@ namespace Equip\Formatter; -use Equip\Adr\PayloadInterface; - class JsonFormatter implements FormatterInterface { /** @@ -25,9 +23,9 @@ public function type() /** * @inheritDoc */ - public function body(PayloadInterface $payload) + public function format($content) { - return json_encode($payload->getOutput(), $this->options()); + return json_encode($content, $this->options()); } /** diff --git a/src/Formatter/PlatesFormatter.php b/src/Formatter/PlatesFormatter.php index c02d6a2..72cf7ed 100644 --- a/src/Formatter/PlatesFormatter.php +++ b/src/Formatter/PlatesFormatter.php @@ -2,7 +2,6 @@ namespace Equip\Formatter; -use Equip\Adr\PayloadInterface; use Equip\Formatter\HtmlFormatter; use League\Plates\Engine; @@ -13,6 +12,11 @@ class PlatesFormatter extends HtmlFormatter */ private $engine; + /** + * @var string + */ + private $template; + /** * @param Engine $engine */ @@ -22,23 +26,25 @@ public function __construct(Engine $engine) } /** - * @inheritDoc + * Get a copy that uses a different template. + * + * @param string $template + * + * @return static */ - public function body(PayloadInterface $payload) + public function withTemplate($template) { - return $this->render($payload); + $copy = clone $this; + $copy->template = $template; + + return $copy; } /** - * @param PayloadInterface $payload - * - * @return string + * @inheritDoc */ - private function render(PayloadInterface $payload) + public function format($content) { - $template = $payload->getSetting('template'); - $output = $payload->getOutput(); - - return $this->engine->render($template, $output); + return $this->engine->render($this->template, $content); } } diff --git a/src/Handler/ActionHandler.php b/src/Handler/ActionHandler.php index c65974b..686f2ae 100644 --- a/src/Handler/ActionHandler.php +++ b/src/Handler/ActionHandler.php @@ -2,11 +2,7 @@ namespace Equip\Handler; -use Equip\Action; -use Equip\Adr\DomainInterface; -use Equip\Adr\InputInterface; -use Equip\Adr\PayloadInterface; -use Equip\Adr\ResponderInterface; +use Equip\Contract\ActionInterface; use Equip\Resolver\ResolverTrait; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; @@ -39,68 +35,29 @@ public function __invoke( $action = $request->getAttribute(self::ACTION_ATTRIBUTE); $request = $request->withoutAttribute(self::ACTION_ATTRIBUTE); + if (is_string($action)) { + $action = $this->resolve($action); + } + $response = $this->handle($action, $request, $response); return $next($request, $response); } /** - * Use the action collaborators to get a response. + * Invoke the action to prepare the response. * - * @param Action $action + * @param ActionInterface $action * @param ServerRequestInterface $request * @param ResponseInterface $response * * @return ResponseInterface */ private function handle( - Action $action, + ActionInterface $action, ServerRequestInterface $request, ResponseInterface $response ) { - $domain = $this->resolve($action->getDomain()); - $input = $this->resolve($action->getInput()); - $responder = $this->resolve($action->getResponder()); - - $payload = $this->payload($domain, $input, $request); - $response = $this->response($responder, $request, $response, $payload); - - return $response; - } - - /** - * Execute the domain to get a payload using input from the request. - * - * @param DomainInterface $domain - * @param InputInterface $input - * @param ServerRequestInterface $request - * - * @return PayloadInterface - */ - private function payload( - DomainInterface $domain, - InputInterface $input, - ServerRequestInterface $request - ) { - return $domain($input($request)); - } - - /** - * Execute the responder to marshall the payload into the response. - * - * @param ResponderInterface $responder - * @param ServerRequestInterface $request - * @param ResponseInterface $response - * @param PayloadInterface $payload - * - * @return ResponseInterface - */ - private function response( - ResponderInterface $responder, - ServerRequestInterface $request, - ResponseInterface $response, - PayloadInterface $payload - ) { - return $responder($request, $response, $payload); + return $action($request, $response); } } diff --git a/src/Input.php b/src/Input.php deleted file mode 100644 index ca79a9e..0000000 --- a/src/Input.php +++ /dev/null @@ -1,58 +0,0 @@ -getAttributes(); - $body = $this->body($request); - $cookies = $request->getCookieParams(); - $query = $request->getQueryParams(); - $uploads = $request->getUploadedFiles(); - - // Order matters here! Important values go last! - return array_replace( - $query, - $body, - $uploads, - $cookies, - $attrs - ); - } - - /** - * @param ServerRequestInterface $request - * - * @return array - */ - private function body(ServerRequestInterface $request) - { - $body = $request->getParsedBody(); - - if (empty($body)) { - return []; - } - - if (is_object($body)) { - // Because the parsed body may also be represented as an object, - // additional parsing is required. This is a bit dirty but works - // very well for anonymous objects. - $body = json_decode(json_encode($body), true); - } - - return $body; - } -} diff --git a/src/Payload.php b/src/Payload.php deleted file mode 100644 index 802dc93..0000000 --- a/src/Payload.php +++ /dev/null @@ -1,151 +0,0 @@ -status = $status; - - return $copy; - } - - /** - * @inheritDoc - */ - public function getStatus() - { - return $this->status; - } - - /** - * @inheritDoc - */ - public function withInput(array $input) - { - $copy = clone $this; - $copy->input = $input; - - return $copy; - } - - /** - * @inheritDoc - */ - public function getInput() - { - return $this->input; - } - - /** - * @inheritDoc - */ - public function withOutput(array $output) - { - $copy = clone $this; - $copy->output = $output; - - return $copy; - } - - /** - * @inheritDoc - */ - public function getOutput() - { - return $this->output; - } - - /** - * @inheritDoc - */ - public function withMessages(array $messages) - { - $copy = clone $this; - $copy->messages = $messages; - - return $copy; - } - - /** - * @inheritDoc - */ - public function getMessages() - { - return $this->messages; - } - - /** - * @inheritDoc - */ - public function withSetting($name, $value) - { - $copy = clone $this; - $copy->settings[$name] = $value; - - return $copy; - } - - /** - * @inheritDoc - */ - public function withoutSetting($name) - { - $copy = clone $this; - unset($copy->settings[$name]); - - return $copy; - } - - /** - * @inheritDoc - */ - public function getSetting($name) - { - if (isset($this->settings[$name])) { - return $this->settings[$name]; - } - - return null; - } - - /** - * @inheritDoc - */ - public function getSettings() - { - return $this->settings; - } -} diff --git a/src/Responder/ChainedResponder.php b/src/Responder/ChainedResponder.php deleted file mode 100644 index 1ee78b6..0000000 --- a/src/Responder/ChainedResponder.php +++ /dev/null @@ -1,67 +0,0 @@ -resolver = $resolver; - - parent::__construct($responders); - } - - /** - * @inheritDoc - */ - public function __invoke( - ServerRequestInterface $request, - ResponseInterface $response, - PayloadInterface $payload - ) { - foreach ($this as $responder) { - $responder = $this->resolve($responder); - $response = $responder($request, $response, $payload); - } - - return $response; - } - - /** - * @inheritDoc - * - * @throws ResponderException - * If $classes does not implement the correct interface. - */ - protected function assertValid(array $classes) - { - parent::assertValid($classes); - - foreach ($classes as $responder) { - if (!is_subclass_of($responder, ResponderInterface::class)) { - throw ResponderException::invalidClass($responder); - } - } - } -} diff --git a/src/Responder/FormattedResponder.php b/src/Responder/FormattedResponder.php deleted file mode 100644 index 0f0b73b..0000000 --- a/src/Responder/FormattedResponder.php +++ /dev/null @@ -1,168 +0,0 @@ - 1.0, - ] - ) { - $this->negotiator = $negotiator; - $this->resolver = $resolver; - - parent::__construct($formatters); - } - - /** - * @inheritDoc - */ - public function __invoke( - ServerRequestInterface $request, - ResponseInterface $response, - PayloadInterface $payload - ) { - if ($this->hasOutput($payload)) { - $response = $this->format($request, $response, $payload); - } - - return $response; - } - - /** - * Determine if the payload has usable output - * - * @param PayloadInterface $payload - * - * @return boolean - */ - protected function hasOutput(PayloadInterface $payload) - { - return (bool) $payload->getOutput(); - } - - /** - * Retrieve a map of accepted priorities with the responsible formatter. - * - * @return array - */ - protected function priorities() - { - $priorities = []; - - foreach ($this as $formatter => $quality) { - foreach ($formatter::accepts() as $type) { - $priorities[$type] = $formatter; - } - } - - return $priorities; - } - - /** - * Retrieve the formatter to use for the current request. - * - * Uses content negotiation to find the best available output format for - * the requested content type. - * - * @param ServerRequestInterface $request - * - * @return FormatterInterface - */ - protected function formatter(ServerRequestInterface $request) - { - $accept = $request->getHeaderLine('Accept'); - $priorities = $this->priorities(); - - if (!empty($accept)) { - $preferred = $this->negotiator->getBest($accept, array_keys($priorities)); - } - - if (!empty($preferred)) { - $formatter = $priorities[$preferred->getValue()]; - } else { - $formatter = array_shift($priorities); - } - - return $this->resolve($formatter); - } - - /** - * Update the response by formatting the payload. - * - * @param ServerRequestInterface $request - * @param ResponseInterface $response - * @param PayloadInterface $payload - * - * @return ResponseInterface - */ - protected function format( - ServerRequestInterface $request, - ResponseInterface $response, - PayloadInterface $payload - ) { - $formatter = $this->formatter($request); - - $response = $response->withHeader('Content-Type', $formatter->type()); - // Overwrite the body instead of making a copy and dealing with the stream. - $response->getBody()->write($formatter->body($payload)); - - return $response; - } - - /** - * @inheritDoc - * - * @throws FormatterException - * If $classes does not implement the correct interface, - * or does not have a quality value. - */ - protected function assertValid(array $classes) - { - parent::assertValid($classes); - - foreach ($classes as $formatter => $quality) { - if (!is_subclass_of($formatter, FormatterInterface::class)) { - throw FormatterException::invalidClass($formatter); - } - - if (!is_float($quality)) { - throw FormatterException::needsQuality($formatter); - } - } - } - - /** - * @inheritDoc - */ - protected function sortValues() - { - arsort($this->values); - } -} diff --git a/src/Responder/RedirectResponder.php b/src/Responder/RedirectResponder.php deleted file mode 100644 index d043631..0000000 --- a/src/Responder/RedirectResponder.php +++ /dev/null @@ -1,28 +0,0 @@ -getSetting('redirect'); - - if (!empty($location)) { - $response = $response->withHeader('Location', $location); - } - - return $response; - } -} diff --git a/src/Responder/StatusResponder.php b/src/Responder/StatusResponder.php deleted file mode 100644 index 37144f4..0000000 --- a/src/Responder/StatusResponder.php +++ /dev/null @@ -1,70 +0,0 @@ -http_status = $http_status; - } - - /** - * @inheritDoc - */ - public function __invoke( - ServerRequestInterface $request, - ResponseInterface $response, - PayloadInterface $payload - ) { - if ($this->hasStatus($payload)) { - $response = $this->status($response, $payload); - } - - return $response; - } - - /** - * Determine if the payload has a status. - * - * @param PayloadInterface $payload - * - * @return boolean - */ - private function hasStatus(PayloadInterface $payload) - { - return (bool) $payload->getStatus(); - } - - /** - * Get the response with the status code from the payload. - * - * @param ResponseInterface $response - * @param PayloadInterface $payload - * - * @return ResponseInterface - */ - private function status( - ResponseInterface $response, - PayloadInterface $payload - ) { - $status = $payload->getStatus(); - $code = $this->http_status->getStatusCode($status); - - return $response->withStatus($code); - } -} diff --git a/tests/ActionTest.php b/tests/ActionTest.php deleted file mode 100644 index f4d5d20..0000000 --- a/tests/ActionTest.php +++ /dev/null @@ -1,35 +0,0 @@ -createMock(DomainInterface::class)); - $action = new Action($domain); - - $this->assertSame($domain, $action->getDomain()); - $this->assertSame(Input::class, $action->getInput()); - $this->assertSame(ChainedResponder::class, $action->getResponder()); - - $responder = get_class($this->createMock(ResponderInterface::class)); - $action = new Action($domain, $responder); - - $this->assertSame($responder, $action->getResponder()); - - $input = get_class($this->createMock(InputInterface::class)); - $action = new Action($domain, null, $input); - - $this->assertSame($input, $action->getInput()); - $this->assertSame(ChainedResponder::class, $action->getResponder()); - } -} diff --git a/tests/Configuration/PayloadConfigurationTest.php b/tests/Configuration/PayloadConfigurationTest.php deleted file mode 100644 index 7d09084..0000000 --- a/tests/Configuration/PayloadConfigurationTest.php +++ /dev/null @@ -1,24 +0,0 @@ -injector->make(PayloadInterface::class); - - $this->assertInstanceOf(Payload::class, $payload); - } -} diff --git a/tests/Configuration/PlatesResponderConfigurationTest.php b/tests/Configuration/PlatesResponderConfigurationTest.php deleted file mode 100644 index 867ac5f..0000000 --- a/tests/Configuration/PlatesResponderConfigurationTest.php +++ /dev/null @@ -1,32 +0,0 @@ -markTestSkipped('Plates is not installed'); - } - - return [ - new AurynConfiguration, - new PlatesResponderConfiguration, - ]; - } - - public function testApply() - { - $responder = $this->injector->make(FormattedResponder::class); - - $this->assertArrayHasKey(PlatesFormatter::class, $responder); - $this->assertSame(1.0, $responder[PlatesFormatter::class]); - } -} diff --git a/tests/DirectoryTest.php b/tests/DirectoryTest.php index a5770d3..ffa6c83 100644 --- a/tests/DirectoryTest.php +++ b/tests/DirectoryTest.php @@ -2,13 +2,14 @@ namespace EquipTests; -use Equip\Adr\DomainInterface; +use Equip\Contract\ActionInterface; use Equip\Directory; use Equip\Exception\DirectoryException; use Equip\Input; use Equip\Structure\Dictionary; +use PHPUnit_Framework_TestCase as TestCase; -class DirectoryTest extends DirectoryTestCase +class DirectoryTest extends TestCase { /** * @var Directory @@ -29,7 +30,7 @@ public function testInvalidAction() { $this->setExpectedExceptionRegExp( DirectoryException::class, - '/Directory entry .* is not an .* instance/i' + '/Directory entry .* must be an .* instance/i' ); $this->directory->withValue('GET /', $this); @@ -37,28 +38,19 @@ public function testInvalidAction() public function testAction() { - $action = $this->getMockAction(); + $action = $this->createMock(ActionInterface::class); $directory = $this->directory->action('LIST', '/', $action); $this->assertTrue($directory->hasValue('LIST /')); $this->assertSame($action, $directory->getValue('LIST /')); } - public function testActionWithDomain() - { - $domain = get_class($this->getMockDomain()); - $directory = $this->directory->action('LIST', '/', $domain); - $action = $directory->getValue('LIST /'); - - $this->assertSame($domain, $action->getDomain()); - } - /** * @dataProvider dataHttpMethods */ public function testActionMethods($method) { - $action = $this->getMockAction(); + $action = $this->createMock(ActionInterface::class); $callback = [$this->directory, strtolower($method)]; $directory = call_user_func($callback, '/', $action); $match = constant(get_class($directory).'::'.$method); diff --git a/tests/DirectoryTestCase.php b/tests/DirectoryTestCase.php deleted file mode 100644 index 5ea87bf..0000000 --- a/tests/DirectoryTestCase.php +++ /dev/null @@ -1,45 +0,0 @@ -getMockDomain()); - } - - $action = $this->getMockBuilder(Action::class); - - $action->setConstructorArgs([ - $domain, - $input, - $responder, - ]); - - return $action->getMock(); - } - - /** - * @return DomainInterface - */ - protected function getMockDomain() - { - return $this->createMock(DomainInterface::class); - } -} diff --git a/tests/Dispatching/DispatchingSetTest.php b/tests/Dispatching/DispatchingSetTest.php index 5d98a52..00f5b13 100644 --- a/tests/Dispatching/DispatchingSetTest.php +++ b/tests/Dispatching/DispatchingSetTest.php @@ -3,7 +3,7 @@ namespace EquipTests\Dispatching; use Auryn\Injector; -use Equip\Adr\DomainInterface; +use Equip\Contract\ActionInterface; use Equip\Directory; use Equip\Exception\DispatchingException; use Equip\Dispatching\DispatchingSet; @@ -35,7 +35,7 @@ public function testPrepareDirectory() { $dispatchers = [ function (Directory $directory) { - return $directory->get('/', DomainInterface::class); + return $directory->get('/', $this->createMock(ActionInterface::class)); } ]; diff --git a/tests/Fake/FakeDomain.php b/tests/Fake/FakeDomain.php deleted file mode 100644 index 05b3611..0000000 --- a/tests/Fake/FakeDomain.php +++ /dev/null @@ -1,17 +0,0 @@ -withStatus(Payload::STATUS_OK) - ->withOutput(['success' => true, 'input' => $input]); - } -} diff --git a/tests/Fake/FakeExceptionHandler.php b/tests/Fake/FakeExceptionHandler.php deleted file mode 100644 index e2bce37..0000000 --- a/tests/Fake/FakeExceptionHandler.php +++ /dev/null @@ -1,17 +0,0 @@ -withOutput([ + $content = [ 'success' => true, - ]); + ]; - $body = $this->formatter->body($payload); + $body = $this->formatter->format($content); $this->assertEquals('{"success":true}', $body); } diff --git a/tests/Formatter/PlatesFormatterTest.php b/tests/Formatter/PlatesFormatterTest.php index e5550b1..10b100d 100644 --- a/tests/Formatter/PlatesFormatterTest.php +++ b/tests/Formatter/PlatesFormatterTest.php @@ -2,7 +2,6 @@ namespace EquipTests\Formatter; -use Equip\Adr\PayloadInterface; use Equip\Formatter\PlatesFormatter; use League\Plates\Engine; use PHPUnit_Framework_TestCase as TestCase; @@ -38,23 +37,15 @@ public function testType() public function testResponse() { $template = 'test'; - $output = [ + $content = [ 'header' => 'header', 'body' => 'body', 'footer' => 'footer' ]; - $payload = $this->createMock(PayloadInterface::class); - - $payload->expects($this->any()) - ->method('getSetting') - ->willReturn($template); - - $payload->expects($this->any()) - ->method('getOutput') - ->willReturn($output); - - $body = $this->formatter->body($payload); + $body = $this->formatter + ->withTemplate($template) + ->format($content); $this->assertEquals("

header

\n

body

\nfooter\n", $body); } diff --git a/tests/Handler/ActionHandlerTest.php b/tests/Handler/ActionHandlerTest.php index 31a252a..a732dfe 100644 --- a/tests/Handler/ActionHandlerTest.php +++ b/tests/Handler/ActionHandlerTest.php @@ -1,7 +1,7 @@ injector->make(Response::class); $handler = $this->injector->make(ActionHandler::class); - $action = new Action(FakeDomain::class); + $action = $this->createMock(ActionInterface::class); + + $action + ->expects($this->once()) + ->method('__invoke') + ->willReturn($response); $request = $request->withAttribute(ActionHandler::ACTION_ATTRIBUTE, $action); - $request = $request->withAttribute('test', true); $response = $handler($request, $response, function ($request, $response) { $this->assertInstanceOf(Response::class, $response); return $response; }); - - $body = json_decode($response->getBody(), true); - - $this->assertTrue($body['success']); - $this->assertTrue($body['input']['test']); } } diff --git a/tests/Handler/DispatchHandlerTest.php b/tests/Handler/DispatchHandlerTest.php index 3b66fbe..d9e371d 100644 --- a/tests/Handler/DispatchHandlerTest.php +++ b/tests/Handler/DispatchHandlerTest.php @@ -2,7 +2,7 @@ namespace EquipTests\Handler; -use EquipTests\DirectoryTestCase; +use Equip\Contract\ActionInterface; use Equip\Directory; use Equip\Exception\HttpException; use Equip\Handler\ActionHandler; @@ -10,8 +10,9 @@ use Zend\Diactoros\Response; use Zend\Diactoros\ServerRequest; use Zend\Diactoros\Uri; +use PHPUnit_Framework_TestCase as TestCase; -class DispatchHandlerTest extends DirectoryTestCase +class DispatchHandlerTest extends TestCase { /** * @var Directory @@ -25,7 +26,7 @@ protected function setUp() public function testHandle() { - $action = $this->getMockAction(); + $action = $this->createMock(ActionInterface::class); $directory = $this->directory->get('/[{name}]', $action); $request = $this->getRequest('GET', '/tester'); $response = new Response; @@ -41,7 +42,7 @@ public function testHandle() public function testPrefixed() { - $action = $this->getMockAction(); + $action = $this->createMock(ActionInterface::class); $directory = $this->directory->withPrefix('prefix'); $directory = $directory->get('/[{name}]', $action); $request = $this->getRequest('GET', '/prefix/tester'); @@ -84,11 +85,12 @@ public function testMethodNotAllowedException() '/cannot access resource .* using method/i' ); + $action = $this->createMock(ActionInterface::class); $handler = new DispatchHandler($this->directory); $request = $this->getRequest('POST'); $response = new Response; - $directory = $this->directory->get('/', $this->getMockAction()); + $directory = $this->directory->get('/', $action); return $this->dispatch( $directory, diff --git a/tests/InputTest.php b/tests/InputTest.php deleted file mode 100644 index bd019eb..0000000 --- a/tests/InputTest.php +++ /dev/null @@ -1,155 +0,0 @@ -execute(new ServerRequest); - $this->assertEmpty($found); - } - - public function testQueryParams() - { - $query = [ - 'query' => 'string', - ]; - - $request = new ServerRequest; - $request = $request->withQueryParams($query); - - $found = $this->execute($request); - $this->assertSame($query, $found); - } - - public function dataParsedBody() - { - $data = []; - - // Null body should be an empty array - $body = null; - $expected = []; - $data[] = [$expected, $body]; - - // Array body should remain the same - $body = [ - 'body' => 'parsed', - 'has' => [ - 'other' => 'values', - ], - ]; - $expected = $body; - $data[] = [$expected, $body]; - - // Object body should be converted to an array - $body = json_decode(json_encode($body)); - $data[] = [$expected, $body]; - - return $data; - } - /** - * @dataProvider dataParsedBody - */ - public function testParsedBody($expected, $body) - { - $request = new ServerRequest; - $request = $request->withParsedBody($body); - - $input = $this->execute($request); - $this->assertSame($expected, $input); - } - - public function testUploadedFiles() - { - $files = [ - 'file' => $this->createMock(UploadedFileInterface::class), - ]; - - $request = new ServerRequest; - $request = $request->withUploadedFiles($files); - - $found = $this->execute($request); - $this->assertSame($files, $found); - } - - public function testCookieParams() - { - $cookies = [ - 'cookie' => 'nomnomnom', - ]; - - $request = new ServerRequest; - $request = $request->withCookieParams($cookies); - - $found = $this->execute($request); - $this->assertSame($cookies, $found); - } - - public function testAttributes() - { - $attrs = [ - 'attr' => 'stored', - ]; - - $request = new ServerRequest; - foreach ($attrs as $name => $value) { - $request = $request->withAttribute($name, $value); - } - - $found = $this->execute($request); - $this->assertSame($attrs, $found); - } - - public function testMerge() - { - $request = new ServerRequest; - - $value = [ - 'merge' => 'query', - ]; - $request = $request->withQueryParams($value); - $this->assertSame($value, $this->execute($request)); - - $value = [ - 'merge' => 'body', - ]; - $request = $request->withParsedBody($value); - $this->assertSame($value, $this->execute($request)); - - $value = [ - 'merge' => $this->createMock(UploadedFileInterface::class), - ]; - $request = $request->withParsedBody($value); - $this->assertSame($value, $this->execute($request)); - - $value = [ - 'merge' => 'cookie', - ]; - $request = $request->withCookieParams($value); - $this->assertSame($value, $this->execute($request)); - - $value = [ - 'merge' => 'attr', - ]; - $request = $request->withAttribute(key($value), current($value)); - $this->assertSame($value, $this->execute($request)); - } - - /** - * Collect input from the request - * - * @param ServerRequest $request - * - * @return array - */ - private function execute(ServerRequest $request) - { - return call_user_func(new Input, $request); - } -} diff --git a/tests/PayloadTest.php b/tests/PayloadTest.php deleted file mode 100644 index ac2c427..0000000 --- a/tests/PayloadTest.php +++ /dev/null @@ -1,76 +0,0 @@ -withStatus(Payload::STATUS_OK); - - $this->assertEmpty($load->getStatus()); - $this->assertSame(Payload::STATUS_OK, $copy->getStatus()); - } - - public function testInput() - { - $input = [ - 'test' => true - ]; - - $load = new Payload; - $copy = $load->withInput($input); - - $this->assertEmpty($load->getInput()); - $this->assertSame($input, $copy->getInput()); - } - - public function testMessages() - { - $messages = [ - 'username' => 'not found', - 'password' => 'invalid' - ]; - - $load = new Payload; - $copy = $load->withMessages($messages); - - $this->assertEmpty($load->getMessages()); - $this->assertSame($messages, $copy->getMessages()); - } - - public function testOutput() - { - $output = [ - 'collection' => [] - ]; - - $load = new Payload; - $copy = $load->withOutput($output); - - $this->assertEmpty($load->getOutput()); - $this->assertSame($output, $copy->getOutput()); - } - - public function testSettings() - { - $name = 'template'; - $value = 'index'; - - $load = new Payload; - $copy = $load->withSetting($name, $value); - - $this->assertEmpty($load->getSettings()); - $this->assertSame($value, $copy->getSetting($name)); - $this->assertSame([$name => $value], $copy->getSettings()); - - $empty = $copy->withoutSetting($name); - - $this->assertNull($empty->getSetting($name)); - $this->assertEmpty($empty->getSettings()); - } -} diff --git a/tests/Responder/ChainedResponderTest.php b/tests/Responder/ChainedResponderTest.php deleted file mode 100644 index 030c835..0000000 --- a/tests/Responder/ChainedResponderTest.php +++ /dev/null @@ -1,96 +0,0 @@ -responder = $this->injector->make(ChainedResponder::class); - } - - public function testDefaultResponders() - { - $responders = $this->responder->toArray(); - - $this->assertContains(FormattedResponder::class, $responders); - $this->assertContains(RedirectResponder::class, $responders); - } - - public function testInvalidResponder() - { - $this->setExpectedExceptionRegExp( - ResponderException::class, - '/Responder class .* must implement .*ResponderInterface/i' - ); - - $this->responder->withValue(get_class($this)); - } - - public function testAddResponder() - { - $responder = $this->getMockResponder(); - $chained = $this->responder->withValue($responder); - - $this->assertContains($responder, $chained); - - $response = $this->execute($chained); - $this->assertInstanceOf(ResponseInterface::class, $response); - } - - public function testResponse() - { - $response = $this->execute($this->responder); - $this->assertInstanceOf(ResponseInterface::class, $response); - } - - private function getMockResponder() - { - $responder = $this->getMockBuilder(ResponderInterface::class)->getMock(); - $responder - ->expects($this->once()) - ->method('__invoke') - ->with( - $this->isInstanceOf(ServerRequestInterface::class), - $this->isInstanceOf(ResponseInterface::class), - $this->isInstanceOf(PayloadInterface::class) - ) - ->will($this->returnArgument(1)); - - return $responder; - } - - private function execute($responder) - { - $request = $this->getMockBuilder(ServerRequestInterface::class)->getMock(); - $response = $this->getMockBuilder(ResponseInterface::class)->getMock(); - $payload = $this->getMockBuilder(PayloadInterface::class)->getMock(); - - return call_user_func($responder, $request, $response, $payload); - } -} diff --git a/tests/Responder/FormattedResponderTest.php b/tests/Responder/FormattedResponderTest.php deleted file mode 100644 index faabdee..0000000 --- a/tests/Responder/FormattedResponderTest.php +++ /dev/null @@ -1,129 +0,0 @@ -responder = $this->injector->make(FormattedResponder::class); - } - - public function testFormatters() - { - $formatters = $this->responder->toArray(); - - $this->assertArrayHasKey(JsonFormatter::class, $formatters); - - unset($formatters[JsonFormatter::class]); - - $formatters = $this->responder->withValues($formatters)->toArray(); - - $this->assertArrayNotHasKey(JsonFormatter::class, $formatters); - - // Append another one with high quality - $formatters[JsonFormatter::class] = 1.0; - - $formatters = $this->responder->withValues($formatters)->toArray(); - $sortedcopy = $formatters; - } - - public function testSorting() - { - $a = $this->getMockBuilder(FormatterInterface::class) - ->setMockClassName('FooFormatter') - ->getMock(); - - $b = $this->getMockBuilder(FormatterInterface::class) - ->setMockClassName('BarFormatter') - ->getMock(); - - $values = [ - get_class($a) => 0.5, - get_class($b) => 1.0, - ]; - - $responder = $this->responder->withValues($values); - $formatters = $responder->toArray(); - - $this->assertNotSame($values, $formatters); - - arsort($values); - - $this->assertSame($values, $formatters); - } - - public function testInvalidResponder() - { - $this->setExpectedExceptionRegExp( - FormatterException::class, - '/Formatter class .* must implement .*FormatterInterface/i' - ); - - $this->responder->withValue(get_class($this), 1.0); - } - - public function testInvalidResponderQuality() - { - $this->setExpectedExceptionRegExp( - FormatterException::class, - '/No quality have been set for the .*/ii' - ); - - $this->responder->withValue(JsonFormatter::class, false); - } - - public function testResponse() - { - $request = new ServerRequest; - $request = $request->withHeader('Accept', 'application/json'); - - $response = new Response; - - $payload = new Payload; - $payload = $payload - ->withOutput(['test' => 'test']); - - $response = call_user_func($this->responder, $request, $response, $payload); - - $this->assertInstanceOf(ResponseInterface::class, $response); - $this->assertEquals(['application/json'], $response->getHeader('Content-Type')); - $this->assertEquals('{"test":"test"}', (string) $response->getBody()); - } - - public function testEmptyPayload() - { - $payload = new Payload; - $request = $this->createMock(ServerRequestInterface::class); - $response = $this->createMock(ResponseInterface::class); - $returned = call_user_func($this->responder, $request, $response, $payload); - $this->assertSame($returned, $response); - } -} diff --git a/tests/Responder/RedirectResponderTest.php b/tests/Responder/RedirectResponderTest.php deleted file mode 100644 index abc86e1..0000000 --- a/tests/Responder/RedirectResponderTest.php +++ /dev/null @@ -1,46 +0,0 @@ -responder = new RedirectResponder; - } - - public function testRedirect() - { - $payload = (new Payload())->withSetting('redirect', '/'); - - $request = $this->getMockBuilder(ServerRequestInterface::class)->getMock(); - $response = new Response; - - $response = call_user_func($this->responder, $request, $response, $payload); - - $this->assertInstanceOf(ResponseInterface::class, $response); - $this->assertEquals('/', $response->getHeaderLine('Location')); - } - - public function testEmptyPayload() - { - $payload = new Payload; - $request = $this->getMockBuilder(ServerRequestInterface::class)->getMock(); - $response = $this->getMockBuilder(ResponseInterface::class)->getMock(); - $returned = call_user_func($this->responder, $request, $response, $payload); - $this->assertSame($returned, $response); - } -} diff --git a/tests/Responder/StatusResponderTest.php b/tests/Responder/StatusResponderTest.php deleted file mode 100644 index 618b57d..0000000 --- a/tests/Responder/StatusResponderTest.php +++ /dev/null @@ -1,49 +0,0 @@ -responder = new StatusResponder( - new Httpstatus - ); - } - - public function testStatus() - { - $payload = new Payload; - $payload = $payload->withStatus(Payload::STATUS_OK); - - $request = $this->getMockBuilder(ServerRequestInterface::class)->getMock(); - $response = new Response; - - $response = call_user_func($this->responder, $request, $response, $payload); - - $this->assertInstanceOf(ResponseInterface::class, $response); - $this->assertEquals(200, $response->getStatusCode()); - } - - public function testEmptyPayload() - { - $payload = new Payload; - $request = $this->getMockBuilder(ServerRequestInterface::class)->getMock(); - $response = $this->getMockBuilder(ResponseInterface::class)->getMock(); - $returned = call_user_func($this->responder, $request, $response, $payload); - $this->assertSame($returned, $response); - } -} From edc1d96eff4c01be5bedb5181bfdffd104a6537b Mon Sep 17 00:00:00 2001 From: Ben Visness Date: Tue, 16 Aug 2016 20:58:53 -0500 Subject: [PATCH 6/6] Make ActionInterface use ServerRequestInterface (#85) Although ActionHandler deals exclusively with ServerRequestInterface instead of RequestInterface, ActionInterface used just RequestInterface. This made it impossible for actions to use the helpful features that ServerRequestInterface offers. In particular, this bug actually rendered the example Action code in the docs incorrect, because the variable $request (of type RequestInterface) was passed as a parameter to input(), which expected ServerRequestInterface. --- docs/index.md | 3 +-- src/Contract/ActionInterface.php | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/index.md b/docs/index.md index b06ec9d..311a05b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -378,7 +378,6 @@ namespace Acme\Action; use Acme\Domain\Authentication; use Acme\Input\LoginInput; use Equip\Contract\ActionInterface; -use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; @@ -402,7 +401,7 @@ class LoginAction implements ActionInterface $this->responder = $responder; } - public function __invoke(RequestInterface $request, ResponseInterface $response) + public function __invoke(ServerRequestInterface $request, ResponseInterface $response) { $input = $this->input($request); $errors = $this->auth->validate($input); diff --git a/src/Contract/ActionInterface.php b/src/Contract/ActionInterface.php index f3b6b70..ffbc591 100644 --- a/src/Contract/ActionInterface.php +++ b/src/Contract/ActionInterface.php @@ -2,7 +2,7 @@ namespace Equip\Contract; -use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; interface ActionInterface @@ -13,13 +13,13 @@ interface ActionInterface * Parses request input and invokes domain logic. Formats domain output for * the response. * - * @param RequestInterface $request + * @param ServerRequestInterface $request * @param ResponseInterface $response * * @return ResponseInterface */ public function __invoke( - RequestInterface $request, + ServerRequestInterface $request, ResponseInterface $response ); }