Migrating form expressive 2 to 3 - problem with sub-applications

expressive

#1

In expressive 2 I was setting up sub-applications for different parts of the application like so:

// Pseudo code
$app = Factory::create(); // old factory method, which I had to recreate now

$web = ...; // created a new app as the one above, populates it with it's own middleware and routes
$api = ...; // same as web, but different routes and middleware

$app->pipe('api', $api);
$app->pipe('web', $web); 

$app->run();

And it allowed me to have separate middlewares for both sub-applications

Now I have some problems with this approach - for some reason the middlewares get shared between both sub-applications, as well as the main one. If I remove sharing from the ApplicationPipeline I get the following exception:

Zend\Stratigility\MiddlewarePipe cannot handle request; no middleware available to process the request

Even if I run the $web application separately.

Was the ability to use this approach silently removed in favor of the one in the skeleton with one file with pipelines and one with routes?


#2

By shared middlewares I mean that if I have the same middleware piped on $api and $web apps it get’s shared twice for both. Routes registered for web/ are reachable in api/ and vice-versa. E.G. api/ping is available as web/ping as well.


#3

Here’s an example:

$app = \Eco\Core\ApplicationFactory::create($injector);

$app->pipe(\Zend\Expressive\Router\Middleware\RouteMiddleware::class);
$app->pipe(\Zend\Expressive\Router\Middleware\DispatchMiddleware::class);

$apiRouting = \Eco\Core\ApplicationFactory::create($injector);
$webRouting = \Eco\Core\ApplicationFactory::create($injector);

$webRouting->get('/dashboard', function ($request) { return new \Zend\Diactoros\Response\JsonResponse('hello'); });

$app->pipe('/web', $webRouting);
$app->pipe('/api', $apiRouting);

// var_dump([
//     \spl_object_id($apiRouting),
//     \spl_object_id($webRouting),
//     \spl_object_id($app)
// ]);
// die();
// ^ this returns: array(3) { [0]=> int(146) [1]=> int(150) [2]=> int(140) }
// so it's not the same app instance
$app->run();

The result is that both /web/dashboard and /api/dashboard have the route defined for $webRouting.


#4

As for the Container, I use Auryn. Here’s the setup:

// container.php
$shares = [
    \Zend\Expressive\Router\RouterInterface::class,
    \Zend\Expressive\Middleware\DispatchMiddleware::class,
    \Zend\Expressive\Middleware\ImplicitHeadMiddleware::class,
    \Zend\Expressive\Middleware\ImplicitOptionsMiddleware::class,
    \Zend\Expressive\Middleware\RouteMiddleware::class,
    \Zend\Expressive\ApplicationPipeline::class,
    \Zend\HttpHandlerRunner\RequestHandlerRunner::class,
    \Zend\HttpHandlerRunner\Emitter\EmitterInterface::class,
    \Zend\Expressive\Response\ServerRequestErrorResponseGenerator::class,

    \Psr\Http\Message\ServerRequestInterface::class,
    \Psr\Http\Message\ResponseInterface::class,
];

$aliases = [
    \Zend\Expressive\Router\RouterInterface::class => \Zend\Expressive\Router\FastRouteRouter::class,
    \Zend\Expressive\Middleware\DispatchMiddleware::class => \Zend\Expressive\Router\Middleware\DispatchMiddleware::class,
    \Zend\Expressive\Middleware\ImplicitHeadMiddleware::class => \Zend\Expressive\Router\Middleware\ImplicitHeadMiddleware::class,
    \Zend\Expressive\Middleware\ImplicitOptionsMiddleware::class => \Zend\Expressive\Router\Middleware\ImplicitOptionsMiddleware::class,
    \Zend\Expressive\Middleware\RouteMiddleware::class => \Zend\Expressive\Router\Middleware\RouteMiddleware::class,
];
$factories = [
    \Zend\Expressive\ApplicationPipeline::class => \Zend\Expressive\Container\ApplicationPipelineFactory::class,
    \Zend\HttpHandlerRunner\RequestHandlerRunner::class => \Zend\Expressive\Container\RequestHandlerRunnerFactory::class,
    \Zend\HttpHandlerRunner\Emitter\EmitterInterface::class => \Zend\Expressive\Container\EmitterFactory::class,
    \Zend\Expressive\Response\ServerRequestErrorResponseGenerator::class => \Zend\Expressive\Container\ServerRequestErrorResponseGeneratorFactory::class,

    \Psr\Http\Message\ServerRequestInterface::class => \Zend\Expressive\Container\ServerRequestFactoryFactory::class,
    \Psr\Http\Message\ResponseInterface::class => \Zend\Expressive\Container\ResponseFactoryFactory::class,
];

foreach ($aliases as $k => $v) {
    $injector->alias($k, $v);
    if (in_array($k, $shares, true)) {
        $injector->share($k, $v);
    }
}

foreach ($factories as $k => $v) {
    $injector->delegate($k, $v);
    if (in_array($k, $shares, true)) {
        $injector->share($k, $v);
    }
}

And the app factory:
public static function create(
\Psr\Container\ContainerInterface $injector,
\Zend\Expressive\Router\RouterInterface $router = null
) {
$injector->alias(\Psr\Container\ContainerInterface::class, \Eco\Core\Injector::class);

    $container = require \FW_BOOTSTRAP_PATH.'/container.php'; // code above

    $middlewareFactory = $container->make(MiddlewareFactory::class);
    $applicationPipeline = $container->make(ApplicationPipeline::class);
    $routeCollector = $container->make(RouteCollector::class);
    $requestHandlerRunner = $container->make(RequestHandlerRunner::class);

    $app = new \Zend\Expressive\Application(
        $middlewareFactory,
        $applicationPipeline,
        $routeCollector,
        $requestHandlerRunner
    );

    return $app;
}

Yes, it’s inefficient in many ways, just trying to get it working first.


#5

I guess this is a key. Is it creating new instance or using shared one?


#6

@webimpress thanks for the quick answer, but I get

int(100) int(145) int(149)

when I’ve added

var_dump(\spl_object_id($routeCollector));

one line below the

$routeCollector = $container->make(RouteCollector::class);

Also, this problem is affecting both - middlewares and routes, so this could be only one of two places if it was the case.

You can see all the shares related to express in the file container.php (in the comment above). I have made it as explicit as possible while debugging this problem.


#7

@Sergey_Telshevsky you can check also if your instance of Zend\Expressive\Router\RouterInterface is shared. This is injected into RouteMiddleware and RouteCollector so I guess you have to create to RouterInterface instances and inject them accordingly for both apps.


#8

It’s shared indeed, if I remove it from shared instances I receive the following exception:

Zend\Stratigility\MiddlewarePipe cannot handle request; no middleware available to process the request


#9

@Sergey_Telshevsky I’m not sure if I understand what you try to do. Why you want to have to separate applications connected to one? Do you know that you can have piped middlewares for path, right?

I can see many issues with this two separate apps and one shared container.


#10

Yes, because then you create new RouterInterface instance for RouteMiddleware… so probably in case above you create 4 RouterInterface instances…


#11

Let me explain what I’m trying to do (AFAIR I’ve read it somewhere when beginning my journey with expressive-2).

I have some application, it has different “parts”:

  • Web - frontend part
  • API - self-explanatory
  • Admin - backend
  • Some other.

Each of these parts should have multiple (hundreds) of routes (not wildcard ones).

For each part I want to have separate middlewares, like some simple auth for Web, some more secure auth for Admin, Tokenauth or something similar for API.
I also want to log all requests made in the Admin part and have some logs for slow API route executions.

These are the perfect middleware use-cases, but they should be separate.

As I have mentioned, the project is a big one, and explicitly defining the same Tokenauth middleware for 100+ routes in API part is a bit creepy.

Maybe you mean something else by saying

Do you know that you can have piped middlewares for path, right?

Can you please elaborate on this?


#12

See:

Basically you can do:

$app->pipe('/path', [
   MiddlewareSpecificForPath::class
]);

(and this middleware is going to be used for all request with uri /path/*).


#13

I see, I’m not sure I can find where I’ve gotten an idea of creating multiple apps from, but it worked really well.

Is any other type of grouping available?

Do I understand correctly I’ll have to explicitly write down full path for each route?


#14

You may want to try creating a container per sub-application. This will ensure that you have a different router instance and middleware available for each.

Ideally, I’d move creation of the sub-applications into a factory, and do the container creation in there; that way you only incur the cost of populating a new container when that particular sub-path is selected.

If you were to go with this approach, you might also consider having separate container configuration per sub-app; this would help minimize the expense of container creation even more.


#15

@matthew thanks for the answer.
The ideal situation for me would be the one where I use only one container with lazy loading as I’m really not into controlling different ones but I see what you’re about and the pros of it.

I’ll try what @webimpress suggested first and see if it works for me.


#16

@webimpress can’t get it to work for some reason

$app = \Eco\Core\ApplicationFactory::create($injector);
$app->pipe('/web', [
    \App\Middleware\Test::class,
    \Zend\Expressive\Router\Middleware\RouteMiddleware::class,
    \Zend\Expressive\Router\Middleware\DispatchMiddleware::class,
]);
$app->pipe('/api', [
    \Zend\Expressive\Router\Middleware\RouteMiddleware::class,
    \Zend\Expressive\Router\Middleware\DispatchMiddleware::class,
]);

$app->get('/web/dashboard', function ($request) { return new \Zend\Diactoros\Response\JsonResponse('hello web'); });
$app->get('/api/dashboard', function ($request) { return new \Zend\Diactoros\Response\JsonResponse('hello api'); });

$app->run();

Web works and load it’s Test middleware, but the API one doesn’t:

Zend\Stratigility\MiddlewarePipe cannot handle request; no middleware available to process the request

If I understand this right, it’s because the pipe requires routing too, but how can I set it up so I’ll have different error handlers for API/Web, like the ones in your link?

https://docs.zendframework.com/zend-expressive/v3/cookbook/autowiring-routes-and-pipelines/

    // Setup pipeline:
    $app->pipe(ErrorHandler::class);
    $app->pipe(ServerUrlMiddleware::class);
    $app->pipe(RouteMiddleware::class);
    $app->pipe(ImplicitHeadMiddleware::class);
    $app->pipe(ImplicitOptionsMiddleware::class);
    $app->pipe(MethodNotAllowedMiddleware::class);
    $app->pipe(UrlHelperMiddleware::class);
    $app->pipe(DispatchMiddleware::class);
    $app->pipe(NotFoundHandler::class);

#17

@Sergey_Telshevsky so… in pipeline.php you have for example:

   $app->pipe('/web', [
       \Web\WebMiddleware::class,
       ErrorHandler::class,
       ServerUrlMiddleware::class,
       RouteMiddleware::class,
       MethodNotAllowedMiddleware::class,
       DispatchMiddleware::class,
       NotFoundHandler::class,
   ]);

   $app->pipe('/api', [
       \Api\ApiMiddleware::class,
       ErrorHandler::class,
       ServerUrlMiddleware::class,
       RouteMiddleware::class,
       MethodNotAllowedMiddleware::class,
       DispatchMiddleware::class,
       NotFoundHandler::class,
   ]);

and ApiMiddleware is as follows:

namespace Api;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Zend\Diactoros\Response\JsonResponse;
use Zend\Expressive\Application;

class ApiMiddleware implements MiddlewareInterface
{
    private $app;

    public function __construct(Application $app)
    {
        $this->app = $app;
    }

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
    {
        $this->app->get('/dashboard', function ($request) {
            return new JsonResponse('hello api');
        });

        return $handler->handle($request);
    }
}

and ApiMiddlewareFactory:

namespace Api;

use Psr\Container\ContainerInterface;
use Zend\Expressive\Application;

class ApiMiddlewareFactory
{
    public function __invoke(ContainerInterface $container)
    {
        return new ApiMiddleware($container->get(Application::class));
    }
}

and of course in container you must have registered:

        'factories'  => [
            \Api\ApiMiddleware::class => \Api\ApiMiddlewareFactory::class,
...

The same for WebMiddleware.

Please note that in ApiMiddleware you can define all routes and you don’t need to add prefix ‘/api’.
Then all request /api/* and /web/* are independent. You can bind different error handler, middlewares, etc… You can of course improve it, this is just a basic example that all is possible :wink:

Hope it helps !


#18

Looks nice, why would you register a factory for such case?


#19

I’m passing application to middlewares, but just noted it’s bad idea, you can pass there just RouterInterface :slight_smile:


#20

Just dropping by to say thank you for help, this works really well. I’m not sure it’s the easiest approach in terms of usability, but it does the job. Wonder if I’ll manage to wrap it in some way to make it appear as some config-based routing :slight_smile: Gotta get some spare time for that :wink:
@webimpress thank you really much and @matthew thank you too!