From 211baf534f558701f3d2141fc64a87073868cbe4 Mon Sep 17 00:00:00 2001 From: stevewest Date: Wed, 19 Apr 2017 17:12:02 +0100 Subject: [PATCH 1/4] Initial ideas for output formatters --- src/Application.php | 3 + src/Formatter/AbstractFormatter.php | 43 ++++++++++++++ src/Formatter/FormatterInterface.php | 55 ++++++++++++++++++ src/Formatter/HttpAcceptJson.php | 53 +++++++++++++++++ src/Formatter/Noop.php | 46 +++++++++++++++ src/Request/Cli.php | 28 ++++++++- src/Request/RequestInterface.php | 17 ++++++ src/Response/Cli.php | 5 ++ src/Response/ResponseInterface.php | 18 ++++++ tests/unit/ApplicationTest.php | 7 ++- tests/unit/Formatter/HttpAcceptJsonTest.php | 64 +++++++++++++++++++++ tests/unit/Formatter/NoopTest.php | 46 +++++++++++++++ 12 files changed, 383 insertions(+), 2 deletions(-) create mode 100644 src/Formatter/AbstractFormatter.php create mode 100644 src/Formatter/FormatterInterface.php create mode 100644 src/Formatter/HttpAcceptJson.php create mode 100644 src/Formatter/Noop.php create mode 100644 tests/unit/Formatter/HttpAcceptJsonTest.php create mode 100644 tests/unit/Formatter/NoopTest.php diff --git a/src/Application.php b/src/Application.php index 4282945..3446f13 100755 --- a/src/Application.php +++ b/src/Application.php @@ -169,6 +169,9 @@ public function performRequest(RequestInterface $request) : ResponseInterface // route to and call controller $match = $this->getRouteMatch($request); + $this->dependencyContainer + ->add('fuel.application.routeMatch', $match); + $response = $this->getControllerResult($match); // trigger request ended event diff --git a/src/Formatter/AbstractFormatter.php b/src/Formatter/AbstractFormatter.php new file mode 100644 index 0000000..6053f02 --- /dev/null +++ b/src/Formatter/AbstractFormatter.php @@ -0,0 +1,43 @@ +container = $container; + } + + /** + * Gets the current dependency container. + * + * @return ContainerInterface + */ + public function getContainer(): ContainerInterface + { + return $this->container; + } +} diff --git a/src/Formatter/FormatterInterface.php b/src/Formatter/FormatterInterface.php new file mode 100644 index 0000000..bac9f8a --- /dev/null +++ b/src/Formatter/FormatterInterface.php @@ -0,0 +1,55 @@ +getContainer()->get('fuel.application.request'); + + return $request->hasHeader('Accept') && + in_array('application/json', $request->getHeader('Accept')); + } + + /** + * {@inheritdoc} + */ + public function format($data) : ResponseInterface + { + /** @var ResponseInterface $response */ + $response = $this->getContainer()->get('fuel.application.response'); + + return $response + ->withBody(new CallbackStream( + function() use ($data) { + return json_encode($data); + } + )) + ->withHeader('Content-Type', 'application/json'); + } +} diff --git a/src/Formatter/Noop.php b/src/Formatter/Noop.php new file mode 100644 index 0000000..63712e7 --- /dev/null +++ b/src/Formatter/Noop.php @@ -0,0 +1,46 @@ +getContainer()->get('fuel.application.response'); + + return $response->withBody(new CallbackStream( + function() use ($data) { + return $data; + } + )); + } +} diff --git a/src/Request/Cli.php b/src/Request/Cli.php index e61c586..8a0bae0 100755 --- a/src/Request/Cli.php +++ b/src/Request/Cli.php @@ -25,4 +25,30 @@ public function getMethod() : string { // TODO: Implement getMethod() method. } -} + + public function getHeaders() + { + // TODO: Implement getHeaders() method. + } + + /** + * @param string $header + * + * @return bool + */ + public function hasHeader($header) + { + // TODO: Implement hasHeader() method. + } + + /** + * @param string $header Case-insensitive header field name. + * + * @return string[] An array of string values as provided for the given + * header. If the header does not appear in the message, this method MUST + * return an empty array. + */ + public function getHeader($header) + { + // TODO: Implement getHeader() method. +}} diff --git a/src/Request/RequestInterface.php b/src/Request/RequestInterface.php index c70e399..ff1a1b1 100644 --- a/src/Request/RequestInterface.php +++ b/src/Request/RequestInterface.php @@ -22,4 +22,21 @@ interface RequestInterface public function getUri(); public function getMethod(); + + public function getHeaders(); + + /** + * @param string $header + * + * @return bool + */ + public function hasHeader($header); + + /** + * @param string $header Case-insensitive header field name. + * @return string[] An array of string values as provided for the given + * header. If the header does not appear in the message, this method MUST + * return an empty array. + */ + public function getHeader($header); } diff --git a/src/Response/Cli.php b/src/Response/Cli.php index 511a3ee..1a40d49 100755 --- a/src/Response/Cli.php +++ b/src/Response/Cli.php @@ -40,4 +40,9 @@ public function withStatus($status) $this->statusCode = $status; return $this; } + + public function withHeader($header, $value) + { + // TODO: Implement withHeader() method. + } } diff --git a/src/Response/ResponseInterface.php b/src/Response/ResponseInterface.php index 23e90c3..09f9e57 100644 --- a/src/Response/ResponseInterface.php +++ b/src/Response/ResponseInterface.php @@ -18,9 +18,27 @@ interface ResponseInterface { public function getStatusCode(); + /** + * @param StreamInterface $body + * + * @return ResponseInterface + */ public function withBody(StreamInterface $body); public function getBody(); + /** + * @param int $status + * + * @return ResponseInterface + */ public function withStatus($status); + + /** + * @param string $header + * @param array|string $value + * + * @return ResponseInterface + */ + public function withHeader($header, $value); } diff --git a/tests/unit/ApplicationTest.php b/tests/unit/ApplicationTest.php index 15ca3e1..c8684c1 100755 --- a/tests/unit/ApplicationTest.php +++ b/tests/unit/ApplicationTest.php @@ -114,7 +114,7 @@ public function testAppCreatedEvent() $this->assertSame($app, $event->getApplication()); } - public function testMakeRequest() + public function testPerformRequest() { $requestStartCalled = false; $requestStartApplication = null; @@ -155,6 +155,11 @@ public function testMakeRequest() $this->assertTrue($requestEndCalled); $this->assertSame($app, $requestEndApplication); + + $this->assertInstanceOf( + '\Fuel\Routing\Match', + $app->getDependencyContainer()->get('fuel.application.routeMatch') + ); } public function testRun() diff --git a/tests/unit/Formatter/HttpAcceptJsonTest.php b/tests/unit/Formatter/HttpAcceptJsonTest.php new file mode 100644 index 0000000..d50a782 --- /dev/null +++ b/tests/unit/Formatter/HttpAcceptJsonTest.php @@ -0,0 +1,64 @@ +setContainer($container); + + $container->add('fuel.application.request', new HttpRequest([], [], null, null, 'php://input', ['Accept' => 'application/json'])); + $container->add('fuel.application.response', new HttpResponse()); + + $this->assertTrue($json->canActivate(null)); + + $container->add('fuel.application.request', new HttpRequest([], [], null, null, 'php://input', ['Accept' => ['text/html', 'application/json']])); + $container->add('fuel.application.response', new HttpResponse()); + + $this->assertTrue($json->canActivate(null)); + + $container->add('fuel.application.request', new HttpRequest([], [], null, null, 'php://input', ['Accept' => 'text/html'])); + $container->add('fuel.application.response', new HttpResponse()); + + $this->assertFalse($json->canActivate(null)); + } + + public function testFormat() + { + $json = new HttpAcceptJson(); + + $container = new Container(); + $json->setContainer($container); + + $container->add('fuel.application.request', new HttpRequest([], [], null, null, 'php://input', ['Accept' => 'application/json'])); + $container->add('fuel.application.response', new HttpResponse()); + + $result = $json->format(['foo' => 'bar', 'true' => false]); + + $this->assertEquals( + '{"foo":"bar","true":false}', + (string) $result->getBody() + ); + } +} diff --git a/tests/unit/Formatter/NoopTest.php b/tests/unit/Formatter/NoopTest.php new file mode 100644 index 0000000..c0e83bd --- /dev/null +++ b/tests/unit/Formatter/NoopTest.php @@ -0,0 +1,46 @@ +assertTrue($noop->canActivate(null)); + } + + public function testFormat() + { + $noop = new Noop(); + + $response = new HttpResponse(); + + $container = new Container(); + $container->add('fuel.application.response', $response); + $noop->setContainer($container); + + $result = $noop->format('foobar'); + + $this->assertEquals( + 'foobar', + (string) $result->getBody() + ); + } +} From c5a83b1c8a853eb00d66a288e327a3a01e5ad535 Mon Sep 17 00:00:00 2001 From: stevewest Date: Wed, 3 May 2017 12:48:02 +0100 Subject: [PATCH 2/4] Creates initial formatter class for #31 --- src/Exception/Formatter.php | 18 +++++ src/Exception/FormatterLoad.php | 22 ++++++ src/ResponseFormatter.php | 95 +++++++++++++++++++++++ tests/unit/ResponseFormatterTest.php | 108 +++++++++++++++++++++++++++ 4 files changed, 243 insertions(+) create mode 100644 src/Exception/Formatter.php create mode 100755 src/Exception/FormatterLoad.php create mode 100644 src/ResponseFormatter.php create mode 100644 tests/unit/ResponseFormatterTest.php diff --git a/src/Exception/Formatter.php b/src/Exception/Formatter.php new file mode 100644 index 0000000..320398e --- /dev/null +++ b/src/Exception/Formatter.php @@ -0,0 +1,18 @@ +dependencyContainer = $dependencyContainer; + + foreach ($formatters as $formatter) { + $formatterInstance = $dependencyContainer->get($formatter); + + if (! $formatterInstance instanceof FormatterInterface) { + throw new FormatterLoad("FOU-003: Unable to load [$formatter]: Does not implement FormatterInterface"); + } + + $formatterInstance->setContainer($dependencyContainer); + $this->formatterClasses[$formatter] = $formatterInstance; + } + } + + /** + * @return FormatterInterface[] + */ + public function getFormatters() + { + return $this->formatterClasses; + } + + /** + * @param mixed $data Result returned by the controller. + * + * @return FormatterInterface + */ + public function getFormatter($data) + { + foreach ($this->formatterClasses as $formatter) { + if ($formatter->canActivate($data)) { + return $formatter; + } + } + + return null; + } + + /** + * Attempts to run a registered formatter on the given data. + * + * @param mixed $data + */ + public function format($data) + { + $formatter = $this->getFormatter($data); + + if ($formatter === null) { + throw new Formatter('FOU-004: No formatter could be found'); + } + + $formatter->format($data); + } +} diff --git a/tests/unit/ResponseFormatterTest.php b/tests/unit/ResponseFormatterTest.php new file mode 100644 index 0000000..5961514 --- /dev/null +++ b/tests/unit/ResponseFormatterTest.php @@ -0,0 +1,108 @@ +dependencyContainer = new Container(); + } + + public function testConstruct() + { + $this->dependencyContainer->add('fuel.response.formatter.noop', 'Fuel\Foundation\Formatter\Noop'); + + $instance = new ResponseFormatter(['fuel.response.formatter.noop'], $this->dependencyContainer); + + $this->assertInstanceOf( + 'Fuel\Foundation\Formatter\Noop', + $instance->getFormatters()['fuel.response.formatter.noop']); + } + + /** + * @expectedException \Fuel\Foundation\Exception\FormatterLoad + * @expectedExceptionMessage FOU-003: Unable to load [fuel.response.formatter.noop]: Does not implement FormatterInterface + */ + public function testConstructWithInvalidFormatter() + { + $this->dependencyContainer->add('fuel.response.formatter.noop', 'stdClass'); + + new ResponseFormatter(['fuel.response.formatter.noop'], $this->dependencyContainer); + } + + public function testCanFormatNegative() + { + $instance = new ResponseFormatter([], $this->dependencyContainer); + $this->assertNull($instance->getFormatter([])); + } + + public function testCanFormatPositive() + { + $this->dependencyContainer->add('fuel.response.formatter.noop', 'Fuel\Foundation\Formatter\Noop'); + + $instance = new ResponseFormatter(['fuel.response.formatter.noop'], $this->dependencyContainer); + + $this->assertInstanceOf( + 'Fuel\Foundation\Formatter\Noop', + $instance->getFormatter([]) + ); + } + + /** + * @expectedException \Fuel\Foundation\Exception\Formatter + * @expectedExceptionMessage FOU-004: No formatter could be found + */ + public function testFormatWithNoFormatters() + { + $instance = new ResponseFormatter([], $this->dependencyContainer); + $instance->format([]); + } + + public function testFormat() + { + /** @var \Mockery\Mock $formatterMock */ + $formatterMock = Mockery::mock('\Fuel\Foundation\Formatter\FormatterInterface'); + + $formatterMock + ->shouldReceive('setContainer') + ->with($this->dependencyContainer) + ->once(); + + $formatterMock + ->shouldReceive('canActivate') + ->with(['foo' => 'bar']) + ->once() + ->andReturn(true); + + $formatterMock + ->shouldReceive('format') + ->with(['foo' => 'bar']) + ->once(); + + $this->dependencyContainer->add('formatter.test', $formatterMock); + + $instance = new ResponseFormatter(['formatter.test'], $this->dependencyContainer); + $instance->format(['foo' => 'bar']); + } +} From d876bb933a4185d61b58eb20dfeee9469fb64177 Mon Sep 17 00:00:00 2001 From: stevewest Date: Thu, 25 May 2017 09:13:35 +0100 Subject: [PATCH 3/4] Adds in services for #31 --- src/ApplicationServicesProvider.php | 23 +++++++++++++++++++++++ src/ResponseFormatter.php | 7 ++++--- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/ApplicationServicesProvider.php b/src/ApplicationServicesProvider.php index f207c36..052d444 100755 --- a/src/ApplicationServicesProvider.php +++ b/src/ApplicationServicesProvider.php @@ -13,11 +13,13 @@ namespace Fuel\Foundation; use Fuel\Config\Container; +use Fuel\Foundation\Exception\Formatter; use Fuel\Foundation\Request\Http; use Fuel\Foundation\Request\RequestInterface; use Fuel\Foundation\Response\ResponseInterface; use Fuel\Routing\Router; use League\Container\ServiceProvider\AbstractServiceProvider; +use Symfony\Component\DomCrawler\Form; class ApplicationServicesProvider extends AbstractServiceProvider { @@ -40,6 +42,10 @@ class ApplicationServicesProvider extends AbstractServiceProvider 'fuel.application.component_manager', 'fuel.application.router', + + 'Fuel\Foundation\Formatter\Noop', + 'Fuel\Foundation\Formatter\HttpAcceptJson', + 'Fuel\Foundation\ResponseFormatter', ]; /** @@ -65,6 +71,11 @@ public function register() $this->getContainer()->add('fuel.application.component_manager', $this->constructComponentManager(), true); $this->getContainer()->add('fuel.application.router', $this->constructRouter(), true); + + // Add in the various formatters + $this->container->add('Fuel\Foundation\Formatter\Noop', 'Fuel\Foundation\Formatter\Noop', true); + $this->container->add('Fuel\Foundation\Formatter\HttpAcceptJson', 'Fuel\Foundation\Formatter\HttpAcceptJson', true); + $this->container->add('Fuel\Foundation\ResponseFormatter', $this->constructResponseFormatter(), true); } /** @@ -109,6 +120,18 @@ protected function constructResponse() : ResponseInterface return $this->getContainer()->get('Fuel\Foundation\Response\Http'); } + protected function constructResponseFormatter() : ResponseFormatter + { + /** @var Container $config */ + $config = $this->getContainer()->get('fuel.config'); + $config->load('output_formatters', 'output_formatters'); + + return new ResponseFormatter( + $config->get('output_formatters', ['Fuel\Foundation\Formatter\Noop']), + $this->getContainer() + ); + } + /** * @return bool */ diff --git a/src/ResponseFormatter.php b/src/ResponseFormatter.php index 99fbbcf..9a2e2bc 100644 --- a/src/ResponseFormatter.php +++ b/src/ResponseFormatter.php @@ -15,6 +15,7 @@ use Fuel\Foundation\Exception\Formatter; use Fuel\Foundation\Exception\FormatterLoad; use Fuel\Foundation\Formatter\FormatterInterface; +use League\Container\ContainerInterface; /** * Keeps track of active formatters and facilitates the formatting of a controller response. @@ -27,15 +28,15 @@ class ResponseFormatter protected $formatterClasses = []; /** - * @var \Fuel\Dependency\Container + * @var ContainerInterface */ protected $dependencyContainer; /** * ResponseFormatter constructor. * - * @param string[] $formatters List of class names or DIC instance names - * @param \Fuel\Dependency\Container $dependencyContainer + * @param string[] $formatters List of class names or DIC instance names + * @param ContainerInterface $dependencyContainer */ public function __construct($formatters, $dependencyContainer) { From 45af7f16f1113a0c27455849d58557d5d5a7b565 Mon Sep 17 00:00:00 2001 From: stevewest Date: Thu, 25 May 2017 09:21:48 +0100 Subject: [PATCH 4/4] Convert provider to use factory closures for on-demand construction #31 --- src/ApplicationServicesProvider.php | 61 ++++++++++++++++++++++++----- 1 file changed, 52 insertions(+), 9 deletions(-) diff --git a/src/ApplicationServicesProvider.php b/src/ApplicationServicesProvider.php index 052d444..c5401ef 100755 --- a/src/ApplicationServicesProvider.php +++ b/src/ApplicationServicesProvider.php @@ -56,26 +56,69 @@ public function register() $this->getContainer()->add('fuel.application.event', 'League\Event\Emitter', true); $this->getContainer()->add('Fuel\Foundation\Request\Cli', 'Fuel\Foundation\Request\Cli', false); - $this->getContainer()->add('Fuel\Foundation\Request\Http', Http::forge(), false); - $this->getContainer()->add('fuel.application.request', $this->constructRequest(), true); + $this->getContainer()->add( + 'Fuel\Foundation\Request\Http', + function() { + return Http::forge(); + }, + false + ); + + $this->getContainer()->add( + 'fuel.application.request', + function() { + return $this->constructRequest(); + }, + true + ); $this->getContainer()->add('Fuel\Foundation\Response\Cli', 'Fuel\Foundation\Response\Cli', false); $this->getContainer()->add('Fuel\Foundation\Response\Http', 'Fuel\Foundation\Response\Http', false); - $this->getContainer()->add('fuel.application.response', $this->constructResponse(), true); + $this->getContainer()->add( + 'fuel.application.response', + function() { + return $this->constructResponse(); + }, + true + ); $this->getContainer()->add('fuel.application.finder', 'Fuel\FileSystem\Finder', true); // Also create a config container for our services - $this->getContainer()->add('fuel.config', new Container(null, $this->getContainer()->get('fuel.application.finder')), true); + $this->getContainer()->add( + 'fuel.config', + function() { + return new Container(null, $this->getContainer()->get('fuel.application.finder')); + }, + true + ); - $this->getContainer()->add('fuel.application.component_manager', $this->constructComponentManager(), true); + $this->getContainer()->add( + 'fuel.application.component_manager', + function () { + return $this->constructComponentManager(); + }, + true + ); - $this->getContainer()->add('fuel.application.router', $this->constructRouter(), true); + $this->getContainer()->add( + 'fuel.application.router', + function() { + return $this->constructRouter(); + }, + true + ); // Add in the various formatters - $this->container->add('Fuel\Foundation\Formatter\Noop', 'Fuel\Foundation\Formatter\Noop', true); - $this->container->add('Fuel\Foundation\Formatter\HttpAcceptJson', 'Fuel\Foundation\Formatter\HttpAcceptJson', true); - $this->container->add('Fuel\Foundation\ResponseFormatter', $this->constructResponseFormatter(), true); + $this->getContainer()->add('Fuel\Foundation\Formatter\Noop', 'Fuel\Foundation\Formatter\Noop', true); + $this->getContainer()->add('Fuel\Foundation\Formatter\HttpAcceptJson', 'Fuel\Foundation\Formatter\HttpAcceptJson', true); + $this->getContainer()->add( + 'Fuel\Foundation\ResponseFormatter', + function() { + return $this->constructResponseFormatter(); + }, + true + ); } /**