diff --git a/src/Http/MessageTrait.php b/src/Http/MessageTrait.php index fa3a1459..e46d291e 100644 --- a/src/Http/MessageTrait.php +++ b/src/Http/MessageTrait.php @@ -4,7 +4,6 @@ namespace Engelsystem\Http; use Psr\Http\Message\StreamInterface; -use Symfony\Component\HttpFoundation\ResponseHeaderBag; use Zend\Diactoros\Stream; /** @@ -41,7 +40,12 @@ trait MessageTrait public function withProtocolVersion($version) { $new = clone $this; - $new->setProtocolVersion($version); + if (method_exists($new, 'setProtocolVersion')) { + $new->setProtocolVersion($version); + } else { + $new->server->set('SERVER_PROTOCOL', $version); + } + return $new; } @@ -72,7 +76,11 @@ trait MessageTrait */ public function getHeaders() { - return $this->headers->allPreserveCase(); + if (method_exists($this->headers, 'allPreserveCase')) { + return $this->headers->allPreserveCase(); + } + + return $this->headers->all(); } /** @@ -228,7 +236,12 @@ trait MessageTrait public function withBody(StreamInterface $body) { $new = clone $this; - $new->setContent($body); + + if (method_exists($new, 'setContent')) { + $new->setContent($body); + } else { + $new->content = $body; + } return $new; } diff --git a/src/Http/Request.php b/src/Http/Request.php index c6a9e5ad..fd3bff42 100644 --- a/src/Http/Request.php +++ b/src/Http/Request.php @@ -2,10 +2,15 @@ namespace Engelsystem\Http; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\UriInterface; use Symfony\Component\HttpFoundation\Request as SymfonyRequest; +use Zend\Diactoros\Uri; -class Request extends SymfonyRequest +class Request extends SymfonyRequest implements RequestInterface { + use MessageTrait; + /** * Get POST input * @@ -64,4 +69,128 @@ class Request extends SymfonyRequest { return rtrim(preg_replace('/\?.*/', '', $this->getUri()), '/'); } + + /** + * Retrieves the message's request target. + * + * + * Retrieves the message's request-target either as it will appear (for + * clients), as it appeared at request (for servers), or as it was + * specified for the instance (see withRequestTarget()). + * + * In most cases, this will be the origin-form of the composed URI, + * unless a value was provided to the concrete implementation (see + * withRequestTarget() below). + * + * If no URI is available, and no request-target has been specifically + * provided, this method MUST return the string "/". + * + * @return string + */ + public function getRequestTarget() + { + $query = $this->getQueryString(); + return '/' . $this->path() . (!empty($query) ? '?' . $query : ''); + } + + /** + * Return an instance with the specific request-target. + * + * If the request needs a non-origin-form request-target — e.g., for + * specifying an absolute-form, authority-form, or asterisk-form — + * this method may be used to create an instance with the specified + * request-target, verbatim. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * changed request target. + * + * @link http://tools.ietf.org/html/rfc7230#section-5.3 (for the various + * request-target forms allowed in request messages) + * @param mixed $requestTarget + * @return static + */ + public function withRequestTarget($requestTarget) + { + return $this->create($requestTarget); + } + + /** + * Return an instance with the provided HTTP method. + * + * While HTTP method names are typically all uppercase characters, HTTP + * method names are case-sensitive and thus implementations SHOULD NOT + * modify the given string. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * changed request method. + * + * @param string $method Case-sensitive method. + * @return static + * @throws \InvalidArgumentException for invalid HTTP methods. + */ + public function withMethod($method) + { + $new = clone $this; + $new->setMethod($method); + + return $new; + } + + /** + * Returns an instance with the provided URI. + * + * This method MUST update the Host header of the returned request by + * default if the URI contains a host component. If the URI does not + * contain a host component, any pre-existing Host header MUST be carried + * over to the returned request. + * + * You can opt-in to preserving the original state of the Host header by + * setting `$preserveHost` to `true`. When `$preserveHost` is set to + * `true`, this method interacts with the Host header in the following ways: + * + * - If the Host header is missing or empty, and the new URI contains + * a host component, this method MUST update the Host header in the returned + * request. + * - If the Host header is missing or empty, and the new URI does not contain a + * host component, this method MUST NOT update the Host header in the returned + * request. + * - If a Host header is present and non-empty, this method MUST NOT update + * the Host header in the returned request. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * new UriInterface instance. + * + * @link http://tools.ietf.org/html/rfc3986#section-4.3 + * @param UriInterface $uri New request URI to use. + * @param bool $preserveHost Preserve the original state of the Host header. + * @return static + */ + public function withUri(UriInterface $uri, $preserveHost = false) + { + $new = $this->create($uri); + if ($preserveHost) { + $new->headers->set('HOST', $this->getHost()); + } + + return $new; + } + + /** + * Retrieves the URI instance. + * + * This method MUST return a UriInterface instance. + * + * @link http://tools.ietf.org/html/rfc3986#section-4.3 + * @return string|UriInterface Returns a UriInterface instance + * representing the URI of the request. + */ + public function getUri() + { + $uri = parent::getUri(); + + return new Uri($uri); + } } diff --git a/tests/Unit/Http/MessageTraitRequestTest.php b/tests/Unit/Http/MessageTraitRequestTest.php new file mode 100644 index 00000000..7430b5d7 --- /dev/null +++ b/tests/Unit/Http/MessageTraitRequestTest.php @@ -0,0 +1,50 @@ +withProtocolVersion('0.1'); + $this->assertNotEquals($message, $newMessage); + $this->assertEquals('0.1', $newMessage->getProtocolVersion()); + } + + /** + * @covers \Engelsystem\Http\MessageTrait::getHeaders + */ + public function testGetHeaders() + { + $message = new MessageTraitRequestImplementation(); + $newMessage = $message->withHeader('lorem', 'ipsum'); + + $this->assertNotEquals($message, $newMessage); + $this->assertArraySubset(['lorem' => ['ipsum']], $newMessage->getHeaders()); + } + + /** + * @covers \Engelsystem\Http\MessageTrait::withBody + */ + public function testWithBody() + { + /** @var Stream $stream */ + $stream = new Stream('php://memory', 'wb+'); + $stream->write('Test content'); + $stream->rewind(); + + $message = new MessageTraitRequestImplementation(); + $newMessage = $message->withBody($stream); + + $this->assertNotEquals($message, $newMessage); + $this->assertEquals('Test content', $newMessage->getContent()); + } +} diff --git a/tests/Unit/Http/MessageTraitTest.php b/tests/Unit/Http/MessageTraitResponseTest.php similarity index 84% rename from tests/Unit/Http/MessageTraitTest.php rename to tests/Unit/Http/MessageTraitResponseTest.php index 46076a67..f60360a3 100644 --- a/tests/Unit/Http/MessageTraitTest.php +++ b/tests/Unit/Http/MessageTraitResponseTest.php @@ -2,21 +2,21 @@ namespace Engelsystem\Test\Unit\Http; -use Engelsystem\Test\Unit\Http\Stub\MessageTraitImplementation; +use Engelsystem\Test\Unit\Http\Stub\MessageTraitResponseImplementation; use PHPUnit\Framework\TestCase; use Psr\Http\Message\MessageInterface; use Psr\Http\Message\StreamInterface; use Symfony\Component\HttpFoundation\Response as SymfonyResponse; use Zend\Diactoros\Stream; -class MessageTraitTest extends TestCase +class MessageTraitResponseTest extends TestCase { /** * @covers \Engelsystem\Http\MessageTrait */ public function testCreate() { - $message = new MessageTraitImplementation(); + $message = new MessageTraitResponseImplementation(); $this->assertInstanceOf(MessageInterface::class, $message); $this->assertInstanceOf(SymfonyResponse::class, $message); } @@ -27,7 +27,7 @@ class MessageTraitTest extends TestCase */ public function testGetProtocolVersion() { - $message = new MessageTraitImplementation(); + $message = new MessageTraitResponseImplementation(); $newMessage = $message->withProtocolVersion('0.1'); $this->assertNotEquals($message, $newMessage); $this->assertEquals('0.1', $newMessage->getProtocolVersion()); @@ -38,7 +38,7 @@ class MessageTraitTest extends TestCase */ public function testGetHeaders() { - $message = new MessageTraitImplementation(); + $message = new MessageTraitResponseImplementation(); $newMessage = $message->withHeader('Foo', 'bar'); $this->assertNotEquals($message, $newMessage); @@ -53,7 +53,7 @@ class MessageTraitTest extends TestCase */ public function testHasHeader() { - $message = new MessageTraitImplementation(); + $message = new MessageTraitResponseImplementation(); $this->assertFalse($message->hasHeader('test')); $newMessage = $message->withHeader('test', '12345'); @@ -66,7 +66,7 @@ class MessageTraitTest extends TestCase */ public function testGetHeader() { - $message = new MessageTraitImplementation(); + $message = new MessageTraitResponseImplementation(); $newMessage = $message->withHeader('foo', 'bar'); $this->assertEquals(['bar'], $newMessage->getHeader('Foo')); @@ -78,7 +78,7 @@ class MessageTraitTest extends TestCase */ public function testGetHeaderLine() { - $message = new MessageTraitImplementation(); + $message = new MessageTraitResponseImplementation(); $newMessage = $message->withHeader('foo', ['bar', 'bla']); $this->assertEquals('', $newMessage->getHeaderLine('Lorem-Ipsum')); @@ -90,7 +90,7 @@ class MessageTraitTest extends TestCase */ public function testWithHeader() { - $message = new MessageTraitImplementation(); + $message = new MessageTraitResponseImplementation(); $newMessage = $message->withHeader('foo', 'bar'); $this->assertNotEquals($message, $newMessage); @@ -105,7 +105,7 @@ class MessageTraitTest extends TestCase */ public function testWithAddedHeader() { - $message = new MessageTraitImplementation(); + $message = new MessageTraitResponseImplementation(); $newMessage = $message->withHeader('foo', 'bar'); $this->assertNotEquals($message, $newMessage); @@ -120,7 +120,7 @@ class MessageTraitTest extends TestCase */ public function testWithoutHeader() { - $message = (new MessageTraitImplementation())->withHeader('foo', 'bar'); + $message = (new MessageTraitResponseImplementation())->withHeader('foo', 'bar'); $this->assertTrue($message->hasHeader('foo')); $newMessage = $message->withoutHeader('Foo'); @@ -133,7 +133,7 @@ class MessageTraitTest extends TestCase */ public function testGetBody() { - $message = (new MessageTraitImplementation())->setContent('Foo bar!'); + $message = (new MessageTraitResponseImplementation())->setContent('Foo bar!'); $body = $message->getBody(); $this->assertInstanceOf(StreamInterface::class, $body); @@ -150,7 +150,7 @@ class MessageTraitTest extends TestCase $stream->write('Test content'); $stream->rewind(); - $message = new MessageTraitImplementation(); + $message = new MessageTraitResponseImplementation(); $newMessage = $message->withBody($stream); $this->assertNotEquals($message, $newMessage); diff --git a/tests/Unit/Http/RequestTest.php b/tests/Unit/Http/RequestTest.php index f8444b84..f7d69aff 100644 --- a/tests/Unit/Http/RequestTest.php +++ b/tests/Unit/Http/RequestTest.php @@ -5,6 +5,8 @@ namespace Engelsystem\Test\Unit\Http; use Engelsystem\Http\Request; use PHPUnit\Framework\TestCase; use PHPUnit_Framework_MockObject_MockObject as MockObject; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\UriInterface; use Symfony\Component\HttpFoundation\Request as SymfonyRequest; class RequestTest extends TestCase @@ -16,6 +18,7 @@ class RequestTest extends TestCase { $response = new Request(); $this->assertInstanceOf(SymfonyRequest::class, $response); + $this->assertInstanceOf(RequestInterface::class, $response); } /** @@ -106,4 +109,91 @@ class RequestTest extends TestCase $this->assertEquals('http://foo.bar/bla/foo', $request->url()); $this->assertEquals('https://lorem.ipsum/dolor/sit', $request->url()); } + + /** + * @covers \Engelsystem\Http\Request::getRequestTarget + */ + public function testGetRequestTarget() + { + /** @var Request|MockObject $request */ + $request = $this + ->getMockBuilder(Request::class) + ->setMethods(['getQueryString', 'path']) + ->getMock(); + + $request->expects($this->exactly(2)) + ->method('getQueryString') + ->willReturnOnConsecutiveCalls(null, 'foo=bar&lorem=ipsum'); + $request->expects($this->exactly(2)) + ->method('path') + ->willReturn('foo/bar'); + + $this->assertEquals('/foo/bar', $request->getRequestTarget()); + $this->assertEquals('/foo/bar?foo=bar&lorem=ipsum', $request->getRequestTarget()); + } + + /** + * @covers \Engelsystem\Http\Request::withRequestTarget + */ + public function testWithRequestTarget() + { + $request = new Request(); + foreach ( + [ + '*', + '/foo/bar', + 'https://lorem.ipsum/test?lor=em' + ] as $target + ) { + $new = $request->withRequestTarget($target); + $this->assertNotEquals($request, $new); + } + } + + /** + * @covers \Engelsystem\Http\Request::withMethod + */ + public function testWithMethod() + { + $request = new Request(); + + $new = $request->withMethod('PUT'); + + $this->assertNotEquals($request, $new); + $this->assertEquals('PUT', $new->getMethod()); + } + + /** + * @covers \Engelsystem\Http\Request::withUri + */ + public function testWithUri() + { + /** @var UriInterface|MockObject $uri */ + $uri = $this->getMockForAbstractClass(UriInterface::class); + + $uri->expects($this->atLeastOnce()) + ->method('__toString') + ->willReturn('http://foo.bar/bla?foo=bar'); + + $request = Request::create('http://lor.em/'); + + $new = $request->withUri($uri); + $this->assertNotEquals($request, $new); + $this->assertEquals('http://foo.bar/bla?foo=bar', (string)$new->getUri()); + + $new = $request->withUri($uri, true); + $this->assertEquals('http://lor.em/bla?foo=bar', (string)$new->getUri()); + } + + /** + * @covers \Engelsystem\Http\Request::getUri + */ + public function testGetUri() + { + $request = Request::create('http://lor.em/test?bla=foo'); + + $uri = $request->getUri(); + $this->assertInstanceOf(UriInterface::class, $uri); + $this->assertEquals('http://lor.em/test?bla=foo', (string)$uri); + } } diff --git a/tests/Unit/Http/Stub/MessageTraitRequestImplementation.php b/tests/Unit/Http/Stub/MessageTraitRequestImplementation.php new file mode 100644 index 00000000..04d08913 --- /dev/null +++ b/tests/Unit/Http/Stub/MessageTraitRequestImplementation.php @@ -0,0 +1,12 @@ +