Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MiddlewarePipeInterface#getPipeline to retrieve an iterable of the pipeline #44

Open
boesing opened this issue May 9, 2023 · 4 comments

Comments

@boesing
Copy link
Member

boesing commented May 9, 2023

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...).

@Xerkus
Copy link
Member

Xerkus commented Jan 30, 2024

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
}

@boesing
Copy link
Member Author

boesing commented Jan 31, 2024

Since we are releasing a major anyways, I'd say yes. Since every middleware Pipeline has a Pipeline, I think that is sufficient.

@gsteel
Copy link
Member

gsteel commented Jan 31, 2024

Or implement IteratorAggregate too?

@Xerkus
Copy link
Member

Xerkus commented Jan 31, 2024

This is explicitly to inspect pipe and has nothing to do with its function. I don't think it should provide iterator.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants