RFC: simplify the usage of Expressive and help migrating from v.2


#1

The goal of this RFC is to simplify the usage of Expressive.

I would like to write a simple Expressive application similar to the approach of Slim (see the code reported here). The idea is to offer a very quick way for newbies to use Expressive, without all the classes, folders and configuration provided by zend-expressive-skeleton. For example, I would like to write a code something like this:

require 'vendor/autoload.php';
use Zend\Expressive\Application;

$app = new Application(/* ... */);
$app->get('/', function($req, $delegate){
   echo "Hello World!";
});
$app->run();

I figured out a simple way to accomplish this without any BC break of the existing zend-expressive-skeleton. The idea is to move the $app instance of public/index.php in a separate file and remove the usage of anonymous functions in config/pipeline.php and config/routes.php. Basically, my proposal is to go back to v.2 of skeleton implementation for these 2 files. This will also facilitate the migration of existing applications from v.2 to v.3.

This is the code change that I’m proposing:

config/app.php

$container = require 'container.php';
$app = $container->get(\Zend\Expressive\Application::class);

require 'pipeline.php';

return $app;

config/pipeline.php

$app->pipe(Zend\Stratigility\Middleware\ErrorHandler::class);
$app->pipe(Zend\Expressive\Helper\ServerUrlMiddleware::class);
$app->pipe(Zend\Expressive\Router\Middleware\RouteMiddleware::class);
$app->pipe(Zend\Expressive\Router\Middleware\ImplicitHeadMiddleware::class);
$app->pipe(Zend\Expressive\Router\Middleware\ImplicitOptionsMiddleware::class);
$app->pipe(Zend\Expressive\Router\Middleware\MethodNotAllowedMiddleware::class);
$app->pipe(Zend\Expressive\Helper\UrlHelperMiddleware::class);
$app->pipe(Zend\Expressive\Router\Middleware\DispatchMiddleware::class);
$app->pipe(Zend\Expressive\Handler\NotFoundHandle::class);

config/routes.php

$app->get('/', App\Handler\HomePageHandler::class, 'home');
$app->get('/api/ping', App\Handler\PingHandler::class, 'api.ping');

With these new configuration files, the public/index.php becomes:

if (PHP_SAPI === 'cli-server' && $_SERVER['SCRIPT_FILENAME'] !== __FILE__) {
    return false;
}
chdir(dirname(__DIR__));
require 'vendor/autoload.php';

(function () {
    $app = require 'config/app.php';
    require 'config/routes.php';
    $app->run();
})();

Using this simple configuration approach, we can write a simple application as follows:

require 'vendor/autoload.php';
$app = require 'config/app.php';

$app->get('/', function($request, $handler) {
    echo "Hello World!";
});
$app->run();

Note that in the config/app.php I’m not including the routing specification (that remains in the public/index.php file). The $app instance benefits from all the configuration services of the zend-expressive-skeleton application, including a routing adapter, a template system, an error handler, etc.

The proposed code change for the skeleton application is published here.


#2

I like it. I do wish we could go one step further and get rid of these:

(require ‘pipeline.php’)($app, $factory, $container);
(require ‘routes.php’)($app, $factory, $container);

Instead simply pull from the container, since it’s loaded previously anyway.


#3

I was writing a really long post about all that you did was swapping this:

$container = require 'config/container.php';
$app = $container->get(\Zend\Expressive\Application::class);

to

$app = require 'config/app.php';

But reading the contents of config/app.php again I saw you moved the pipeline in there as well. Which makes sense.

The only problem is that you can’t have this anymore in public\index.php:

(require 'config/func_routes.php')($app, $factory, $container);

The container is not available and so the factory can’t be loaded from it:

(require 'config/func_routes.php')($app);

Unless I misunderstood you, these can’t be pulled from the container unless you create your pipeline and routes from configuration. This was done in the Expressive skeleton v1, but turned out to be more confusing. That’s why the default method in the skeleton is adding those programatically since version 2.


#4

@xtreamwayz you right, I need to find a workaround for this.


#5

@wshafer, @xtreamwayz I changed the RFC, removing the usage of anonymous functions in config/pipeline.php and config/routes.php. Everything is cleaner and simple now.
Thoughts?


#6

Except for config/app.php, that’s basically what we had in version 2. We added this construction for typhinting purposes etc.


#7

My main point is to have a $app instance available for quick use cases, showing how is simple to use Expressive. I’m in favor of anonymous functions in the skeleton, of course. I would like to have feedback from the community on this approach, it’s very smart but can be confusing for some folks.


#8

Thanks for the reply. I do understand this has gone through some iterations to get where it’s at now. But I aldo know that having to rebuild the the index.php for things like the swoole-expressive package this becomes problematic. You end up with something like this:

protected $pipelineFile = DIR . ‘/…/…/…/…/…/config/pipeline.php’;
protected $routeFile = DIR . ‘/…/…/…/…/…/config/routes.php’;

Which only works if the user is using the default vendor directory for Composer.

Seems it might be a little cleaner if instead these files were invokable classes and added to composers autoloaders in the skeleton. Then they certainly could be pulled out of the container, or at worst simply created with a ‘new Routes();’

While I’m thinking about it, the same could be done for the container.php file making the whole thing a little easier to grab.


#9

Looks interesting. But are you thinking about creating a new minimal skeleton besides the current skeleton or exchanging the current skeleton with this new approach?


#10

Same skeleton, only making it easier to kickstart the application with less (complex) code.


#11

How about this?

// config/app.php
<?php

use Zend\Expressive\Application;
use Zend\Expressive\MiddlewareFactory;

class App
{
    /** @var \Psr\Container\ContainerInterface */
    private static $container;

    public static function init() : Application
    {
        $container = require __DIR__ . '/container.php';

        /** @var \Zend\Expressive\Application $app */
        $app = $container->get(Application::class);
        $factory = $container->get(MiddlewareFactory::class);

        // Execute programmatic/declarative middleware pipeline and routing
        // configuration statements
        (require __DIR__ . '/pipeline.php')($app, $factory, $container);
        (require __DIR__ . '/routes.php')($app, $factory, $container);

        return $app;
    }

    public static function getFactory() : MiddlewareFactory
    {
        return self::$container->get(MiddlewareFactory::class);
    }
}
// public/index.php
<?php

declare(strict_types=1);

// Delegate static file requests back to the PHP built-in webserver
if (PHP_SAPI === 'cli-server' && $_SERVER['SCRIPT_FILENAME'] !== __FILE__) {
    return false;
}

chdir(dirname(__DIR__));
require __DIR__ . '/../vendor/autoload.php';

(function () {
    require __DIR__ . '/../config/app.php';

    $app = App::init();

    $app->get('/hi', function($request, $handler) {
        echo 'Hello World!';
    });

    $app->run();
})();

For a quick start you need only these lines:

require __DIR__ . '/../vendor/autoload.php';
require __DIR__ . '/../config/app.php';

$app = App::init();

// Setup pipeline and routes

// For advanced use:
// $factory = App::getFactory();

$app->run();

#12

Ooo. I like that even better @xtreamwayz

Wonder if we could go one step further and add that app factory to the class map autoloader in Composer. Then you’d only need 1 require line.

Westin


#13

Sure, we can even add it to zend-expressive as Zend\Expressive\App or maybe Zend\Expressive\Bootstrap to avoid confusion with other Application named classes, plus we could push bugfixes and improvements when needed.


#14

Hello @xtreamwayz.
In my opinion it’s not a good idea to hardcode configuration options inside the init method. The init() method is assuming that everybody is using programmatic pipelines/routes and wrapping this inside an elsewhere defined method hides that assumption.
To be honest I don’t find anything wrong or difficult for newbies in the current public/index.php file. Every line has a very meaningful comment. Using config pipelines/routes makes it even simpler:

declare(strict_types=1);

use Zend\Expressive\Application;

// Delegate static file requests back to the PHP built-in webserver
if (PHP_SAPI === 'cli-server' && $_SERVER['SCRIPT_FILENAME'] !== __FILE__) {
    return false;
}

chdir(dirname(__DIR__));
require 'vendor/autoload.php';
/**
 * Self-called anonymous function that creates its own scope and keep the global namespace clean.
 */
(function () {
    /** @var \Psr\Container\ContainerInterface $container */
    $container = require 'config/container.php';

 /** @var Application $app */
    $app = $container->get(Application::class);
    $app->run();
 })();

#15

Same here, but apparently there are some who still find it difficult. to be honest, it would be nice to know what exactly is difficult. Is it the code, maybe the comments need improvement or the docs?

I agree with it. I was just playing with examples to get the result as requested in the opening post.

Having said that, I finally clicked the link to slim and digged into the slim code. All that Slim\Application() does is return an application and an empty container with no configuration or what so ever.

We had exactly the same in Expressive 2:

require 'vendor/autoload.php';

$app = \Zend\Expressive\AppFactory::create();

$app->pipe('/', function() {
    return new \Zend\Diactoros\Response\HtmlResponse('Hello world!');
});

$app->run();

This returns the application, with no config and it runs out of the box, just like slim. To load configuration in slim, you do need to inject the container yourself, the same as in the v2 AppFactory:

require 'vendor/autoload.php';

$container = require 'config/container.php';
$app = Zend\Expressive\AppFactory::create($container);

$app->pipe('/', function() {
    return new \Zend\Diactoros\Response\HtmlResponse('Hello world!');
});

$app->run();

And as @pine3ree is saying, it might not be a good idea to hardcode the lines to require pipelines and routes (Thanx for reminding me :D) since we support programmatic and configuration options for both and users may have move the configuration somewhere else. So if you use configuration for the pipeline and routes, you are done with example 2. If you want to go full programmatic mode you can do it like this:

require 'vendor/autoload.php';

$container = require 'config/container.php';
$app = Zend\Expressive\AppFactory::create($container);

$factory = $container->get(\Zend\Expressive\MiddlewareFactory::class);
(require 'config/pipeline.php')($app, $factory, $container);
(require 'config/routes.php')($app, $factory, $container);

$app->pipe('/', function() {
    return new \Zend\Diactoros\Response\HtmlResponse('Hello world!');
});

$app->run();

So bring back the AppFactory from version 2, add what I wrote here to the quickstart and you are done I guess.

… Or use what we have in version 3 and stick to these lines and maybe improve the comments and / or docs:

require 'vendor/autoload.php';

$container = require 'config/container.php';
$app = $container->get(\Zend\Expressive\Application::class);

$app->pipe('/', function() {
    return new \Zend\Diactoros\Response\HtmlResponse('Hello world!');
});

$app->run();

At the end of the day, without a container you can’t do much and the only real difference between Slim and Expressive is this:

$app = new \Slim\App($container);
$app = $container->get(\Zend\Expressive\Application::class);