diff --git a/psalm-baseline.xml b/psalm-baseline.xml index cb8cfab..82aa0a8 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,18 +1,29 @@ - + + + + new CallableStreamFactoryDecorator($streamFactory) + + + + get(StreamInterface::class))]]> + + + detectStreamFactory + - $container->get(StreamInterface::class) + get(StreamInterface::class)]]> - $this->services[$id] + services[$id]]]> - new CallableResponseFactoryDecorator(fn (): ResponseInterface => $this->response) + $this->response)]]> diff --git a/src/Middleware/ImplicitHeadMiddleware.php b/src/Middleware/ImplicitHeadMiddleware.php index f82bdf3..6be238c 100644 --- a/src/Middleware/ImplicitHeadMiddleware.php +++ b/src/Middleware/ImplicitHeadMiddleware.php @@ -7,12 +7,16 @@ use Fig\Http\Message\RequestMethodInterface as RequestMethod; use Mezzio\Router\RouteResult; use Mezzio\Router\RouterInterface; +use Mezzio\Router\Stream\CallableStreamFactoryDecorator; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\StreamFactoryInterface; use Psr\Http\Message\StreamInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; +use function is_callable; + /** * Handle implicit HEAD requests. * @@ -44,19 +48,20 @@ class ImplicitHeadMiddleware implements MiddlewareInterface { public const FORWARDED_HTTP_METHOD_ATTRIBUTE = 'forwarded_http_method'; - /** @var callable(): StreamInterface */ - private $streamFactory; + private StreamFactoryInterface $streamFactory; /** - * @param callable(): StreamInterface $streamFactory A factory capable of returning an empty - * StreamInterface instance to inject in a response. + * @param (callable(): StreamInterface)|StreamFactoryInterface $streamFactory A factory capable of returning + * an empty StreamInterface instance to + * inject in a response. */ - public function __construct(private RouterInterface $router, callable $streamFactory) + public function __construct(private RouterInterface $router, callable|StreamFactoryInterface $streamFactory) { - // Factory is wrapped in closure in order to enforce return type safety. - $this->streamFactory = function () use ($streamFactory): StreamInterface { - return $streamFactory(); - }; + if (is_callable($streamFactory)) { + $streamFactory = new CallableStreamFactoryDecorator($streamFactory); + } + + $this->streamFactory = $streamFactory; } /** @@ -99,7 +104,6 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface ->withMethod(RequestMethod::METHOD_GET) ); - $body = ($this->streamFactory)(); - return $response->withBody($body); + return $response->withBody($this->streamFactory->createStream()); } } diff --git a/src/Middleware/ImplicitHeadMiddlewareFactory.php b/src/Middleware/ImplicitHeadMiddlewareFactory.php index ec88dc6..55fede3 100644 --- a/src/Middleware/ImplicitHeadMiddlewareFactory.php +++ b/src/Middleware/ImplicitHeadMiddlewareFactory.php @@ -6,7 +6,9 @@ use Mezzio\Router\Exception\MissingDependencyException; use Mezzio\Router\RouterInterface; +use Mezzio\Router\Stream\CallableStreamFactoryDecorator; use Psr\Container\ContainerInterface; +use Psr\Http\Message\StreamFactoryInterface; use Psr\Http\Message\StreamInterface; /** @@ -14,10 +16,9 @@ * * This factory depends on two other services: * - * - Mezzio\Router\RouterInterface, which should resolve to an - * instance of that interface. - * - Psr\Http\Message\StreamInterface, which should resolve to a callable - * that will produce an empty Psr\Http\Message\StreamInterface instance. + * - Mezzio\Router\RouterInterface, which should resolve to an instance of that interface. + * - Either Psr\Http\Message\StreamFactoryInterface or Psr\Http\Message\StreamInterface, which should resolve to a + * callable that will produce an empty Psr\Http\Message\StreamInterface instance. * * @final */ @@ -36,16 +37,37 @@ public function __invoke(ContainerInterface $container): ImplicitHeadMiddleware ); } - if (! $container->has(StreamInterface::class)) { + return new ImplicitHeadMiddleware( + $container->get(RouterInterface::class), + $this->detectStreamFactory($container), + ); + } + + /** + * BC Preserving StreamFactoryInterface Retrieval + * + * Preserves existing behaviour in the 3.x series by fetching a `StreamInterface` callable and wrapping it in a + * decorator that implements StreamFactoryInterface. If `StreamInterface` callable is unavailable, attempt to + * fetch a `StreamFactoryInterface`, throwing a MissingDependencyException if neither are found. + * + * @deprecated Will be removed in version 4.0.0 + */ + private function detectStreamFactory(ContainerInterface $container): StreamFactoryInterface + { + $hasStreamFactory = $container->has(StreamFactoryInterface::class); + $hasDeprecatedCallable = $container->has(StreamInterface::class); + + if (! $hasStreamFactory && ! $hasDeprecatedCallable) { throw MissingDependencyException::dependencyForService( StreamInterface::class, ImplicitHeadMiddleware::class ); } - return new ImplicitHeadMiddleware( - $container->get(RouterInterface::class), - $container->get(StreamInterface::class) - ); + if ($hasDeprecatedCallable) { + return new CallableStreamFactoryDecorator($container->get(StreamInterface::class)); + } + + return $container->get(StreamFactoryInterface::class); } } diff --git a/src/Middleware/ImplicitOptionsMiddleware.php b/src/Middleware/ImplicitOptionsMiddleware.php index 20ce511..51f29cc 100644 --- a/src/Middleware/ImplicitOptionsMiddleware.php +++ b/src/Middleware/ImplicitOptionsMiddleware.php @@ -45,15 +45,14 @@ */ class ImplicitOptionsMiddleware implements MiddlewareInterface { - /** @var ResponseFactoryInterface */ - private $responseFactory; + private ResponseFactoryInterface $responseFactory; /** * @param (callable():ResponseInterface)|ResponseFactoryInterface $responseFactory A factory capable of returning an * empty ResponseInterface instance to return for implicit OPTIONS * requests. */ - public function __construct($responseFactory) + public function __construct(callable|ResponseFactoryInterface $responseFactory) { if (is_callable($responseFactory)) { // Factories are wrapped in a closure in order to enforce return type safety. diff --git a/src/Middleware/MethodNotAllowedMiddleware.php b/src/Middleware/MethodNotAllowedMiddleware.php index b0eb86e..e812450 100644 --- a/src/Middleware/MethodNotAllowedMiddleware.php +++ b/src/Middleware/MethodNotAllowedMiddleware.php @@ -33,13 +33,12 @@ */ class MethodNotAllowedMiddleware implements MiddlewareInterface { - /** @var ResponseFactoryInterface */ - private $responseFactory; + private ResponseFactoryInterface $responseFactory; /** * @param (callable():ResponseInterface)|ResponseFactoryInterface $responseFactory */ - public function __construct($responseFactory) + public function __construct(callable|ResponseFactoryInterface $responseFactory) { if (is_callable($responseFactory)) { // Factories are wrapped in a closure in order to enforce return type safety. diff --git a/src/Stream/CallableStreamFactoryDecorator.php b/src/Stream/CallableStreamFactoryDecorator.php new file mode 100644 index 0000000..4db838f --- /dev/null +++ b/src/Stream/CallableStreamFactoryDecorator.php @@ -0,0 +1,43 @@ +streamFactory = $streamFactory; + } + + /** @inheritDoc */ + public function createStream(string $content = ''): StreamInterface + { + return ($this->streamFactory)(); + } + + /** @inheritDoc */ + public function createStreamFromFile(string $filename, string $mode = 'r'): StreamInterface + { + throw new RuntimeException('This method will not be implemented'); + } + + /** @inheritDoc */ + public function createStreamFromResource($resource): StreamInterface + { + throw new RuntimeException('This method will not be implemented'); + } +} diff --git a/test/Middleware/ImplicitHeadMiddlewareFactoryTest.php b/test/Middleware/ImplicitHeadMiddlewareFactoryTest.php index d710ce1..1a22289 100644 --- a/test/Middleware/ImplicitHeadMiddlewareFactoryTest.php +++ b/test/Middleware/ImplicitHeadMiddlewareFactoryTest.php @@ -4,6 +4,7 @@ namespace MezzioTest\Router\Middleware; +use Laminas\Diactoros\StreamFactory; use Mezzio\Router\Exception\MissingDependencyException; use Mezzio\Router\Middleware\ImplicitHeadMiddlewareFactory; use Mezzio\Router\RouterInterface; @@ -11,6 +12,7 @@ use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Container\ContainerInterface; +use Psr\Http\Message\StreamFactoryInterface; use Psr\Http\Message\StreamInterface; use function in_array; @@ -47,33 +49,41 @@ public function testFactoryRaisesExceptionIfRouterInterfaceServiceIsMissing(): v public function testFactoryRaisesExceptionIfStreamFactoryServiceIsMissing(): void { $this->container - ->expects(self::exactly(2)) + ->expects(self::exactly(3)) ->method('has') ->with(self::callback(function (string $arg): bool { - self::assertTrue(in_array($arg, [RouterInterface::class, StreamInterface::class])); + self::assertTrue(in_array($arg, [ + RouterInterface::class, + StreamFactoryInterface::class, + StreamInterface::class, + ])); return true; })) - ->willReturnOnConsecutiveCalls(true, false); + ->willReturnOnConsecutiveCalls(true, false, false); $this->expectException(MissingDependencyException::class); ($this->factory)($this->container); } - public function testFactoryProducesImplicitHeadMiddlewareWhenAllDependenciesPresent(): void + public function testFactoryProducesImplicitHeadMiddlewareWithCallableStreamFactory(): void { $router = $this->createMock(RouterInterface::class); $streamFactory = static function (): void { }; $this->container - ->expects(self::exactly(2)) + ->expects(self::exactly(3)) ->method('has') ->with(self::callback(function (string $arg): bool { - self::assertTrue(in_array($arg, [RouterInterface::class, StreamInterface::class])); + self::assertTrue(in_array($arg, [ + RouterInterface::class, + StreamFactoryInterface::class, + StreamInterface::class, + ])); return true; })) - ->willReturn(true); + ->willReturnOnConsecutiveCalls(true, false, true); $this->container ->expects(self::exactly(2)) @@ -86,4 +96,34 @@ public function testFactoryProducesImplicitHeadMiddlewareWhenAllDependenciesPres ($this->factory)($this->container); } + + public function testFactoryProducesImplicitHeadMiddlewareWithStreamFactoryInterface(): void + { + $router = $this->createMock(RouterInterface::class); + $streamFactory = new StreamFactory(); + + $this->container + ->expects(self::exactly(3)) + ->method('has') + ->with(self::callback(function (string $arg): bool { + self::assertTrue(in_array($arg, [ + RouterInterface::class, + StreamFactoryInterface::class, + StreamInterface::class, + ])); + return true; + })) + ->willReturn(true, true, false); + + $this->container + ->expects(self::exactly(2)) + ->method('get') + ->with(self::callback(function (string $arg): bool { + self::assertTrue(in_array($arg, [RouterInterface::class, StreamFactoryInterface::class])); + return true; + })) + ->willReturnOnConsecutiveCalls($router, $streamFactory); + + ($this->factory)($this->container); + } } diff --git a/test/Stream/CallableStreamFactoryDecoratorTest.php b/test/Stream/CallableStreamFactoryDecoratorTest.php new file mode 100644 index 0000000..674fc2c --- /dev/null +++ b/test/Stream/CallableStreamFactoryDecoratorTest.php @@ -0,0 +1,61 @@ +stream = (new StreamFactory())->createStream(); + + $this->decorator = new CallableStreamFactoryDecorator(fn (): StreamInterface => $this->stream); + } + + public function testThatCreateStreamWillProduceStream(): void + { + self::assertSame($this->stream, $this->decorator->createStream()); + } + + public function testThatTheStreamDoesNotReceiveContentArgument(): void + { + $result = $this->decorator->createStream('some content'); + + self::assertSame('', $result->getContents()); + } + + public function testCreateStreamFromFileIsNotImplemented(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('This method will not be implemented'); + + $this->decorator->createStreamFromFile('/foo'); + } + + public function testCreateStreamFromResourceIsNotImplemented(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('This method will not be implemented'); + + $this->decorator->createStreamFromResource(fopen(__FILE__, 'r')); + } +}