My thinking of a new cakephp (v4)


#1

Plugin as a Service Provider

Plugins are to be assumed to be service providers instead of what they used to be.

The idea behind this term is that its going to provide the developer and plugins authors
a very cute ability to change when things come in.

I think this new architectural design would be of very great performanace boost to cakephp apps.

To implement this, some new conventions and how we view what plugins are, has to come in.
We create a new file in the app/config named ‘services.php’ containing codes similar to this

<?php
return [
    'providers' => [
        \Cake\Queue\Services\QueueService::class,
        ...
    ],
    'defers' => [
        \DebugKit\Services\DebugService::class,
        ...
    ]
]

The idea behind the file is that, we create Service.php maybe under namespace Cake\Core\Service

which every service provide should extend. The class should contain this set of methods

...

class Service implements ServiceInterface
{
    public function boot(\Cake\Http\MiddlewareQueue $queue, \Psr\Http\Message\ServerRequestInterface $request);
    public function bind();
    public function when(): bool;
    ...
}

The main aim of having this is to be able to bootstrap the registered service.
The when method is an important method in every service, it lets us decide if the framework
should keep on bootstraping the service. If when() returns true then we invoke the other 2 methods in the order boot($middlewareQueue, $serverRequest) and bind(),
if the when method returns false the service doesn’t go beyond that, so we move to next service in line and perform same check.

Now that comes to the point where i said we have to stop using the Event system to leverage the internal binding capabilities

Instead of having to use EventManager::instance()->dispatch('Server.buildMiddleware', ...) in the src/Http/Server.php,
we are going to have something similar to Services::bootstrap($middleware, $request),
which loads up the app/config/services.php file and picks up the ‘providers’ key first, loop through all services and instantiate,
invokes the when method to decide if to keep on bootstraping the service.

Method Summarization

when() method

The service provider gets to check if the necessary or required necessities are met. e.g using the DebugKit\Services\DebugService

...
use Cake\Core\Configure;

public function when()
{
    return Configure::read('App.debug')
    // maybe it depends on an environment variable to finalize its setup
    if (env(...)) {
        return true;
    }

    return false;
}
...

This is very performant in my opinion, instead of having to the developer, check that himself the service provider can leverage that itself.

boot($middleware, $request)

This should be used by services needing to inject their middlewares or changing the request(i think there is some sort of
ambiguity here, not sure if the $request object is needed here, someone can help.:smile:)
This is to inform the service that the framework is preparing to process the request, anything you want to do before the framework starts
processing the request, do it now.:smile:

public function boot($middleware, $request)
{
    $middleware
        ->add(CakeLoverMiddleware::class);

    Connection::connect(Configure::read('LovingCake'));

    if (Configure::check(...)) {
        $config = array_merge($defaultConfig, Configure::read(...));
        Configure::write(..., $config);
    }
}

bind()

This is meant to replace the Plugin::load(..., ['bootstrap' => true]), the main reason behind this new design is to reduce the number
of require_once call made by the framework. You can get to bind your global listeners in here,

public function bind()
{
    EventManager::instance()->on(new ILoveCakeListener());

    // if it has routes files
    $this->enableRoutes();

    // maybe you have just a few route to add to the app
    Router::scope(['plugin' => 'CakeLover', 'path' => '/lovers'], function () {
        Router::post('/i-love-cakephp', 'CakeLovers@add')
            ->name('cakelover');

        Router::get('/get-lover/{id}', 'CakeLovers@view')
            ->whereParam(['id' => '[0-9]+'])
            ->middleware([...]);
    });
    // or maybe define a middleware group which the developer can use
    Router::middlewareGroup('web', [...]);
}

I know you will be surprised with the routing system up there, don’t worry, i will be talking about it.:smile:

Looking at the example, we’ve been able eliminate the need for require_once 'plugin_path/bootstrap.php'

‘defer’ key

The reason i think this would be great, is the ability the consumer of the plugin has, to decide when a service comes in, by doing this;

// app/config/bootstrap.php
use Cake\Core\Services;
use Cake\Http\Server;

Services::defer(\Any\Lovely\PluginService::class)
    ->untilWhen(function () {
        return Server::getRequest()->inApiContext();
    })

Mind you, the defered service should be added in the ‘defer’ key. The function gets called when the framework bootstraping the 'defer’
services needed by the app. It simple calls the callback, before invoking when() on the service,
if the callback should return false then the service doesn’t need to be instanstiated, and that gives a performance boost by not accessing the
filesystem at all.

I also thought if we can eliminate the need for requiring the bootstrap file
but instead do all bindings in app/src/Application.php boot() method

Routing Proposal

I also thought that the routing system should be very cute and expressive, just like example i gave above.
There will be a great performance boost by having 2 routes files config/routing/web.php and config/routing/api.php,
that reduces the number of routes registered when a request is coming in.

I think the community will be very glad if we can have such routing style, separating my web interface from api.

I will be very glad if we can have a routing syntax similar to;

use Cake\Routing\Router;

// get verb
Router::get('/lovely-cake', 'Lovely@showHome');
// post verb
Router::post('/lovely-cake', 'Lovely@showHome');
// put verb
Router::put('/lovely-cake', 'Lovely@showHome');
// patch verb
Router::patch('/lovely-cake', 'Lovely@showHome');
// while connect matches every http verb coming through
Router::connect('/lovely-cake', 'Lovely@showHome');

Router::scope(['path' => '/cakephp'], function () {
    Router::get('/docs/{version}', 'Docs@view')
        ->whereParam(['version' => '[0-9]']);

    // 'Tutorials' matches a subnamespace in the controller namespace
    Router::post('/cake-lessons/register', 'Tutorials/Users@register')
        ->middleware([...])
        ->name('lessons:register');
});

// defining a plugin
Router::scope([
    'plugin' => 'DebugKit',
    'path' => '/debug',
    'middleware' => '...' // will be attached to all routes
], function () {
    // ...
})

// or
Router::get(...)->plugin('DebugKit');

// resource routing can be like
Router::resource('Users')
    ->only(['edit', 'add'])
    ->addRoute(['path' => '...', 'action' => 'Api/Users@...'])
    // extension gets added to all generated routes
    ->extensions('json' || ['json']);

// and so on

In the routing service maybe we can have this

use Cake\Routing;

public function boot($middleware, $request) {
    $this->inApiContext = $request->inApiContext();
}

public function bind()
{
    // this loads routes files related to the request context
    if ($this->inApiContext) {
        return Router::loadApiRoutes();
    }
    Router::loadWebRoutes();
}

or maybe in the RoutingMiddleware.

I haven’t figured a way to detect if the incoming request is to hit an api or web context, i need someone to help with that.:smile:

Need For An App Command

I think we should provide an app command which can be used in doing the following by running

bin/cake app optimize

This should concatenate all routes files into their specific scope and configuration files,
like app.php, services.php e.t.c, so that we end up having

/config
    /cache
        app.php
        web.php
        api.php

All defered services should not get their routes optimized. The cache files could contain something similar to

<?php
return [
    'App' => [
        // ...
    ],
    'services' => [
        'provider' => [
            // ...
        ],
        'defer' => [
            // ...
        ]
    ],
    // ...
]

bin/cake app unoptimize

This clears the cache files

bin/cake app namespace [argument]

The changing the project namespace with ease.

This are just my thoughts, but will be happy if they can get implemented.:smile:


#2

I’m going to lock this thread. Could we please keep discussions together in the issue you’ve already created at https://github.com/cakephp/cakephp/issues/10852


#3