`MiddlewarePipeInterface#getPipeline` to retrieve an iterable of the pipeline
boesing opened this issue · 4 comments
Feature Request
Q | A |
---|---|
New Feature | yes |
RFC | yes |
BC Break | yes |
Summary
I'd love to see MiddlewarePipeInterface#getPipeline
to retrieve an iterable of middlewares to be executed.
Most preferably in the same order the middlewares were actually enqueued.
This method would help me to get rid of using ReflectionProperty
in unit tests to actually grab the pipeline so that we can verify that all middlewares are actually instantiable via ContainerInterface
:
<?php
declare(strict_types=1);
namespace ApplicationTest\DependencyInjection;
use ApplicationTest\AbstractContainerAwareTestCase;
use InvalidArgumentException;
use Laminas\Stratigility\MiddlewarePipe;
use Mezzio\Application;
use Mezzio\Middleware\LazyLoadingMiddleware;
use Mezzio\MiddlewareContainer;
use Mezzio\Router\FastRouteRouter;
use Mezzio\Router\Route;
use Mezzio\Router\RouterInterface;
use Psr\Container\ContainerInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use ReflectionClass;
use RuntimeException;
use Webmozart\Assert\Assert;
use function array_unique;
use function assert;
use function class_implements;
use function get_class;
use function in_array;
use function is_string;
use function sprintf;
final class DependencyFactoryIntegrationTest extends AbstractContainerAwareTestCase
{
/**
* @psalm-assert class-string<MiddlewareInterface|RequestHandlerInterface> $middlewareName
*/
private static function assertIsMiddlewareOrRequestHandler(string $middlewareName): void
{
Assert::classExists($middlewareName);
$implements = class_implements($middlewareName) ?: [];
if (in_array(MiddlewareInterface::class, $implements, true)) {
return;
}
if (in_array(RequestHandlerInterface::class, $implements, true)) {
return;
}
throw new InvalidArgumentException(sprintf(
'Provided middleware "%s" name does not implement MiddlewareInterface nor RequestHandlerInterface.',
$middlewareName,
));
}
/**
* @return non-empty-list<Route>
*/
private static function extractRoutesFromConfiguration(ContainerInterface $container): array
{
/**
* Load application as the applications delegator is actually adding routes to the router.
*
* @see \Mezzio\Container\ApplicationConfigInjectionDelegator
*/
$container->get(Application::class);
$router = $container->get(RouterInterface::class);
self::assertInstanceOf(FastRouteRouter::class, $router);
// Extract known routes
$routerReflection = new ReflectionClass($router);
$routesToInjectProperty = $routerReflection->getProperty('routesToInject');
$routes = $routesToInjectProperty->getValue($router);
Assert::isNonEmptyList($routes);
Assert::allIsInstanceOf($routes, Route::class);
return $routes;
}
/**
* @dataProvider middlewares
*/
public function testCanGetServiceFromContainer(ContainerInterface $container, string $serviceNameOrAlias): void
{
$this->expectNotToPerformAssertions();
$container->get($serviceNameOrAlias);
}
private static function getContainerWithModifiedServices(): ContainerInterface
{
return self::getContainer();
}
/**
* @return list<non-empty-string>
*/
private static function extractServiceNamesFromRouteConfiguration(ContainerInterface $container): array
{
$routes = self::extractRoutesFromConfiguration($container);
/** @var list<non-empty-string> $middlewareNames */
$middlewareNames = [];
foreach ($routes as $route) {
assert($route instanceof Route);
$middleware = $route->getMiddleware();
$middlewareNamesFromMiddleware = self::extractMiddlewareNamesFromMiddleware($middleware);
foreach ($middlewareNamesFromMiddleware as $middlewareName) {
if (in_array($middlewareName, $middlewareNames, true)) {
continue;
}
$middlewareNames[] = $middlewareName;
}
}
return $middlewareNames;
}
/**
* @return non-empty-string
*/
private static function extractMiddlewareNameFromLazyLoadingMiddleware(LazyLoadingMiddleware $middleware): string
{
// Extract middleware name from lazy loading middleware
$middlewareReflection = new ReflectionClass($middleware);
$middlewareNameProperty = $middlewareReflection->getProperty('middlewareName');
$middlewareName = $middlewareNameProperty->getValue($middleware);
assert(is_string($middlewareName) && $middlewareName !== '');
return $middlewareName;
}
/**
* @return list<non-empty-string>
*/
private static function extractMiddlewareNamesFromMiddleware(MiddlewareInterface $middleware): array
{
if ($middleware instanceof LazyLoadingMiddleware) {
return [
self::extractMiddlewareNameFromLazyLoadingMiddleware($middleware),
];
}
if ($middleware instanceof MiddlewarePipe) {
return self::extractMiddlewareNamesFromPipeline($middleware);
}
throw new RuntimeException(sprintf('Unhandled middleware type `%s`.', get_class($middleware)));
}
/**
* @return list<non-empty-string>
*/
private static function extractMiddlewareNamesFromPipeline(MiddlewarePipe $middleware): array
{
// Extract middleware name from lazy loading middleware
$middlewareReflection = new ReflectionClass($middleware);
$pipelineProperty = $middlewareReflection->getProperty('pipeline');
$pipeline = $pipelineProperty->getValue($middleware);
self::assertIsIterable($pipeline);
$middlewareNames = [];
foreach ($pipeline as $middleware) {
assert($middleware instanceof MiddlewareInterface);
$middlewareNames[] = self::extractMiddlewareNamesFromMiddleware($middleware);
}
$flattenedMiddlewareNames = array_merge(...$middlewareNames);
return array_values(array_unique($flattenedMiddlewareNames));
}
/**
* @return iterable<class-string<RequestHandlerInterface|MiddlewareInterface>,array{ContainerInterface,class-string<MiddlewareInterface|RequestHandlerInterface>}>
*/
public static function middlewares(): iterable
{
$container = self::getContainerWithModifiedServices();
$middlewareContainer = $container->get(MiddlewareContainer::class);
$middlewares = [];
foreach (self::extractServiceNamesFromRouteConfiguration($container) as $middlewareName) {
if (in_array($middlewareName, $middlewares, true)) {
continue;
}
self::assertIsMiddlewareOrRequestHandler($middlewareName);
$middlewares[] = $middlewareName;
yield $middlewareName => [$middlewareContainer, $middlewareName];
}
}
/**
* @param class-string<RequestHandlerInterface> $requestHandler
* @dataProvider requestHandlers
*/
public function testRequestHandlerInterfaceIsRegisteredToAnyRoute(
string $requestHandler
): void {
$routes = self::extractRoutesFromConfiguration(self::getContainerWithModifiedServices());
foreach ($routes as $route) {
$middlewareNames = self::extractMiddlewareNamesFromMiddleware($route->getMiddleware());
if (!in_array($requestHandler, $middlewareNames, true)) {
continue;
}
return;
}
self::fail(sprintf('Could not find any route using request handler: %s', $requestHandler));
}
/**
* @return iterable<class-string<RequestHandlerInterface>,array{0: class-string<RequestHandlerInterface>}>
*/
public static function requestHandlers(): iterable
{
$middlewares = self::middlewares();
foreach ($middlewares as [, $middleware]) {
if (!in_array(RequestHandlerInterface::class, class_implements($middleware) ?: [], true)) {
continue;
}
yield $middleware => [$middleware];
}
}
}
More specifically the extractMiddlewareNamesFromPipeline
method.
With mezzio/mezzio#159 it is now possible to fetch the middleware name which will be lazy-loaded by LazyLoadingMiddleware
and thus, the only thing where I would need ReflectionProperty
for would be the pipeline from stratigility (well, and the routesToInject
to access FastRouteRouter
pending route property...).
Is it worth introducing a BC break? Will new interface that our pipe implements be sufficient?
Say,
interface InspectableMiddlewarePipeInterface extends MiddlewarePipeInterface
{
public function inspectPipedMiddleware(): array
}
Since we are releasing a major anyways, I'd say yes. Since every middleware Pipeline has a Pipeline, I think that is sufficient.
Or implement IteratorAggregate
too?
This is explicitly to inspect pipe and has nothing to do with its function. I don't think it should provide iterator.