Overview

An enhanced PHP programming environment that provides inversion of control of a web application or any function

Summary

The Mvc5 Framework provides a fresh approach to PHP programming. It is written from the ground up as a functional object-oriented system using immutable components. At its core is a next generation dependency injection management plugin system.

Web Application

The mvc5-application demonstrates its usage as a web application.

include __DIR__ . '/../vendor/autoload.php';
return [
    'cache'      => __DIR__ . '/../tmp',
    'cookie'     => include __DIR__ . '/cookie.php',
    'events'     => include __DIR__ . '/event.php',
    'middleware' => include __DIR__ . '/middleware.php',
    'routes'     => include __DIR__ . '/route.php',
    'services'   => include __DIR__ . '/service.php',
    'session'    => include __DIR__ . '/session.php',
    'templates'  => include __DIR__ . '/template.php',
    'view'       => __DIR__ . '/../view',
    'web'        => include __DIR__ . '/web.php',
];
(new Web(include __DIR__ . '/../config/config.php', null, true))();

A default configuration is provided with the minimum configuration required to run a web application. It contains configurations for PSR-7 compatible request and response classes, templates and routes. The third parameter passed to the web class binds the scope of the anonymous functions, within the service configuration, to the application class.

Console Application

A simple console application can be created by passing command line arguments to the service call method.

./app.php 'Console\Example' Monday January
include './init.php';

(new App('./config/config.php'))->call($argv[1], array_slice($argv, 2));

The first argument is the name of the function to call and the remaining arguments are its parameters, e.g Console\Example.

namespace Console;

use Home\ViewModel;

class Example
{
    /**
     * @var ViewModel
     */
    protected $model;

    /**
     * @param ViewModel $model
     */
    function __construct(ViewModel $model)
    {
        $this->model = $model;
    }

    /**
     * @param $day
     * @param $month
     */
    function __invoke($day, $month)
    {
        var_dump($this->model);
        echo $day . ' ' . $month . "\n";
    }
}

An application can also work without a configuration.

(new App)->call($argv[1], array_slice($argv, 2));;

Read more about dependency injection, autowiring and named arguments for how required arguments can automatically be resolved.

Environment Aware

Development configuration settings can override production values using array_merge as each config file returns an array of values.

return array_merge(
    include __DIR__ . '/../config.php',
    [
        'db_name' => 'dev'
    ]
);

For example, the development config file config/dev/config.php can include the production config file config/config.php and override the name of the database to use in the development environment.

Models and ArrayAccess

Value objects use a model interface to provide a common set of access methods.

namespace Mvc5\Config;

interface Model
    extends \ArrayAccess, \Countable, \Iterator
{
    /**
     * @param string $name
     */
    function get($name);

    /**
     * @param string $name
     */
    function has($name);

    /**
     * @param array|string $name
     * @param mixed $value
     */
    function with($name, $value = null);

    /**
     * @param array|string $name
     */
    function without($name);
}

The model interface is then extended to provide a configuration interface containing the methods set and remove.

namespace Mvc5\Config;

interface Configuration
    extends Model
{
    /**
     * @param array|string $name
     */
    function remove($name);
    
    /**
     * @param array|string $name
     * @param mixed $value
     */
    function set($name, $value = null);
}

Set and Remove

Values can also be removed or set at once by passing an array of key values.

$config->set(['foo' => 'bar', 'baz' => 'bat']);
$config->remove(['foo', 'baz']);

Immutable

By protecting access to the mutable ArrayAccess methods and object magic methods an immutable interface can be implemented.

namespace Mvc5\Config;

trait ReadOnly
{
    use Config {
        remove as protected;
        set as protected;
    }

    function offsetSet($name, $value)
    {
        throw new \Exception;
    }

    function offsetUnset($name)
    {
        throw new \Exception;
    }

    function __set($name, $value)
    {
        throw new \Exception;
    }

    function __unset($name)
    {
        throw new \Exception;
    }
}

Implementing the model interface allows a component to only have to specify its immutable methods.

interface Route
    extends Model
{
    function controller();
    function path();
}

With and Without

A copy of the model can be created with and without specific values. It can also accept an array of key values so that only one clone operation is performed.

$this->with(['foo' => 'bar', 'baz' => 'bat']);
$this->without(['foo', 'baz']);

ArrayAccess

The ArrayAccess interface also enables the service manager to retrieve composite configuration values. E.g.

new Param('templates.error');

Resolves to

$config['templates']['error'];

This makes it possible to use an array or a configuration class when a reference is required.

Polymorphism

Models can also be made mutable by applying their traits to an instance of a configuration object.

class ViewModel
    extends \Mvc5\Config
    implements \Mvc5\View\ViewModel
{
    /**
     *
     */
    use \Mvc5\View\Config\ViewModel;
}

Routes

A collection of routes are used to match a request using middleware route components. Each aspect of matching a route to a request is a separate function. For example, scheme, hostname, path, method, wildcard and any other function can be configured. If a route does not contain a regular expression, it will be created from the path configuration before being matched.

return [
    'home' => [
        'route' => '/{$}'
        'regex' => '/$'
        'controller' => 'Home\Controller'
    ],
    'app' => [
        'path' => '/{controller::*$}' //{controller:[a-zA-Z0-9/]+[a-zA-Z0-9]$}
    ],
]

Routes can be configured with a regular expression or a path. If the route is only used to match the request path, then only a regular expression is required. If the route is also used to create a url, then a path configuration is required.

The regular expression can use group names that are assigned as request parameters when the route is matched. Consequently, the parameter names must be alphanumeric. The path configuration provides a simpler format for specifying regular expressions and group names. For example, if the matched url is /about, the app route configuration assigns the value about as the controller request parameter.

Short names can also be assigned to regular expressions and be used in a path configuration by prefixing them with a single colon or two colons when assigned to a parameter name. Below are the default short-named regular expressions available to use.

[
    'a' => '[a-zA-Z0-9]++',
    'i' => '[0-9]++',
    'n' => '[a-zA-Z][a-zA-Z0-9]++',
    's' => '[a-zA-Z0-9_-]++',
    '*' => '.++',
    '*$' => '[a-zA-Z0-9/]+[a-zA-Z0-9]$'
]

The regular expression, or short-name, for a path configuration can also be specified as a constraint.

return [
    'app' => [
        'route' => '/{controller}',
        'constraints' => [
            'controller' => '[a-zA-Z0-9/]+[a-zA-Z0-9]$'
        ]
    ],
]

Optional parameters are enclosed with square brackets []. The syntax of the path configuration is based on the FastRoute library and the short-name regular expressions are based on Klien.php. However, the route component is mainly based on the Dash library.

Custom routes can also be configured by adding a class name to the array, or the configuration can be a route object.

Automatic Routes

A controller can be automatically matched to a url with the controller match function and using a single route configuration.

return [
    'app' => [
        'defaults' => ['controller' => 'home'],
        'options' => [
            'prefix' => 'App',
            'suffix' => '\Controller'
            'strict' => false,
        ],
        'path' => '/[{controller::*$}]'
    ]
];

The controller match function will change the first letter of each word separated by a forward slash in the url into an uppercase letter. It then replaces the forward slash with a back slash to create a fully qualified class name and will try to load a matching controller. In order to ensure that it is a valid controller, the configuration should prepend a namespace and append a suffix.

Strict mode does not change the case sensitivity of the controller name. However, most urls are lower case and file names and directories typically begin with an uppercase. This prevents controllers from being auto-loaded. This can be resolved by using a service loader and having a service configuration with a matching lower case name. The service configuration will then specify the name of the class to use, e.g 'home\controller' => Home\Controller::class.

When a controller is specified, it must have a service configuration value (or real value) that resolves to a callable type. If it does not have a plugin configuration and its class exists, a new instance will be created and autowired.

Controller names prefixed with the @ symbol will be directly invoked because they are either a function or a static class method.

Url Generator

The url plugin can generate urls with or without a route configuration and are RFC 3986 encoded.

'dashboard' => [
    'path'      => '/dashboard/{user}',
    'controller' => 'dashboard',
    'children' => [
        'add' => [
            'path' => '/add[/{wildcard::*$}]',
            'controller' => 'dashboard\add',
            'wildcard'   => true,
        ]
    ]
]
echo $this->url(['dashboard', 'user' => 'phpdev']);

Route configurations must be named and child routes of the current parent route can automatically use their parent route parameters, e.g /dashboard/phpdev/add.

echo $this->url('dashboard/add');

Wild card parameters can be added by using the {wildcard::*$} route expression and enabling it with 'wildcard' => true. The parameters are appended to the url and will be added to the parameters for that request, e.g /dashboard/phpdev/add/type/tasks.

echo $this->url(['dashboard/add', 'type' => 'tasks']);

Urls can also be generated without having or using a route configuration by prefixing the path with a forward slash.

echo $this->url('/dashboard/phpdev/list', ['order' => 'desc'], '', ['absolute' => true]);

The second parameter of the url plugin function is for query string arguments, e.g /dashboard/phpdev/list?order=desc. The third parameter is for the fragment and the fourth parameter can be used to generate an absolute url; the current scheme, host and port will be used if not provided. The url plugin class can also be configured to always generate an absolute url.

REST API Methods

Routes can be configured with actions for specific HTTP methods. The default action is specified with the controller configuration.

'resource' => [
    'route' => '/resource',
    'method' => ['GET', 'POST'],
    'controller' => 'Resource\Controller'
    'action' => [
        'POST' => function(Url $url) {
            return new Response\RedirectResponse($url(), 201);
        }
    ]
]

Action Controller

The action controller is used to control the invocation of the controller specified by the request.

function __invoke($controller, array $args = [])
{
    return $this->action($controller, $args);
}

A controller is a function, it can also be an event or a plugin that resolves to a callable function. If the value returned from the controller is not a Http\Response and it is not null, it will be set as the value of the response body for the remaining components to transform into a value that can be sent to the client.

'web' => [
    'route\dispatch',
    'request\error',
    'request\service',
    'controller\action',
    'view\layout',
    'view\render',
    'response\status',
    'response\version',
    'response\send'
],

Middleware

Middleware applications can be created with a configuration that supports anonymous functions, plugins and dependency injection.

'web' => [
    'web\route',
    'web\error',
    'web\service',
    'web\controller',
    'web\layout',
    'web\render',
    'web\status',
    'web\version',
    'web\send',
],

The web\controller calls the controller and if the returned value is not a Http\Response and it is not null, it will be set as the value of the response body for the remaining components to transform into a value that can be sent to the client.

function __invoke(Request $request, Response $response, callable $next)
{
    return $next($request, $this->send($response));
}

The PSR-7 Middleware demo can be enabled by uncommenting the web configuration in the web application service config file.

//middleware demo
//'web' => 'web\middleware',

Pipelines

Routes can be configured with Middleware pipelines and (optionally) a controller. During the route match process the child stack is appended to the parent stack. When the route is matched, the controller is appended to the stack. A controller placeholder can also be used to indicate where to insert the controller. The Middleware stack then becomes the controller for the request.

'explore' => [
    'path' => '/explore',
    'middleware' => ['web\authenticate'],
    'defaults' => [
        'controller' => 'explore'
    ],
    'children' => [
        'more' => [
            'path' => '/more',
            'middleware' => ['controller', 'web\log'],
            'defaults' => [
                'controller' => 'more'
            ]
        ]
    ]
]

Middleware configurations must resolve to a callable type and can include plugins and anonymous functions.

View Models

Controllers can return a view model that is rendered using a specified template. For convenience, controllers can use a view model trait that contains methods for returning a view model with assigned variables. It has two methods model and view. Either can be used depending on if the name of the template is specified. If a view model is injected, a copy of it is returned with the assigned variables; otherwise a default view model is created.

use Mvc5\View\Model;

class Controller
{
    use Model;
    
    public function __invoke()
    {
        return $this->model(['message' => 'Hello World']);
        // or
        return $this->view('home', ['message' => 'Hello World']);
    }
}

Rendering View Models

View models specify the name of the template to be rendered with. Template names can also have an associated template configuration specifying the full path to the template file. If the template name contains a dot it is considered to be a full path to the template file. Otherwise it is the file path relative to the application view directory without the .phtml file extension.

function __invoke($model, array $vars = [])
{
    return $this->view->render($model, $vars);
}

The renderer function accepts two arguments. The first argument is the name of the view model or the name of the relative template path. The second argument is the array of variables to assign to the view model and template.

echo $this->render('/home/index', ['request' => $request]);

By prefixing the template name with the directory separator, the view renderer will not use the service locator to find an associated view model and instead will create a default view model with the assigned variables.

View Engine

The default view engine will bind the view model to a closure and extract its variables before including the template. The scope of the template is the view model itself.

function render(TemplateModel $template)
{
    return (function() {
        /** @var TemplateModel $this */

        extract($this->vars(), EXTR_SKIP);

        ob_start();

        try {

            include $this->template();

            return ob_get_clean();

        } catch(\Throwable $exception) {
            throw $exception;
        }
    })->call($template);
}

Template Layouts

If a layout is required, the view model will be assigned to it as part of the web function.

protected function layout(TemplateLayout $layout, $model)
{
    return !$model instanceof TemplateModel || $model instanceof TemplateLayout ? $model : 
        $layout->withModel($model);
}

View Plugins

The default view model supports plugins and requires a service locator to be injected prior to being rendered. However, because a view model can be created by a controller, this may not of happened. To overcome this, the current service locator will be injected into the view model if it does not already have one.

<?php

/** @var Mvc5\ViewModel $this */

echo $this->url('home');

Events

An event is a function. However, instead of being implemented as a single function, it is implemented across multiple functions; which can be easily extended via its configuration.

function action($controller, array $args = [])
{
    return $this->call($controller, $args);
}

The call function can also generate an event. However, sometimes it may be preferable to pass the event arguments directly to its constructor, in which case the trigger method can be used.

function match($route, $request)
{
    return $this->trigger([Arg::ROUTE_MATCH, Arg::ROUTE => $route, Arg::REQUEST => $request]);
}

When the call method is used to generate an event that does not have a plugin configuration, an instance of the default event model will be created. This allows a common model parameter to be used by the functions of the event to contain a value that is not null.

function __invoke(callable $callable, array $args = [], callable $callback = null)
{
    $model = $this->signal(
        $callable, 
        !$args ? $this->args() : (!is_string(key($args)) ? $args : $this->args() + $args), $callback
    );

    null !== $model
        && $this->model = $model;

    return $model;
}

Event Configuration

Events are configurable and can be an array or an iterator. Each item returned must resolve to a callable type.

'web' => [
    'route\dispatch',
    'request\error',
    'request\service',
    'controller\action',
    function($response) { //named args
        var_dump(__FILE__, $response);
    },
    'view\layout',
    'view\render',
    'response\status',
    'response\version',
    'response\send'
],

Dependency Injection

When a class object is created and it can not be autowired, then a service configuration is required. Different types of configurations can be used depending on the requirements of the class. These configurations can be either a string, an array, an anonymous function, a resolvable plugin or a real value.

[
    'home'          => Home\Controller::class,
    'request'       => Mvc5\Request\HttpRequest::class,
    'response'      => Mvc5\Response\HttpResponse::class,
    'url'           => new Shared('url\plugin'),
    'url\generator' => [Mvc5\Url\Generator::class, new Param('routes')],
    'url\plugin'    => [Mvc5\Url\Plugin::class, new Shared('request'), new Plugin('url\generator')],
    'web'           => new Response('web')
];

A string configuration is used to map a class name, or a short name, or an interface name to a fully qualified class name, or another configuration name. Because it is a string configuration, the class either has no dependencies or it can be autowired. An array configuration is used when there are required dependencies and its configuration can be further reduced by using array key names for the arguments that can not be autowired. An anonymous function is used when the class instantiation requires custom logic and various plugins are available as named arguments.

However, when the dependencies of a class require their own dependencies, then the depth of the dependency graph increases. In this case, a class object can also be created using a resolvable plugin instead of using an anonymous function or factory method. This provides inversion of control and is a configuration domain specific language. Each plugin must implement the resolvable interface so that the system can distinguish them from other objects and invoke their associated function. Plugins can be used by each other and chained together to form a composite plugin and are only required when an explicit configuration is needed.

Service Container

Plugins can be grouped together and accessed via the ArrayAccess interface or using the arrow notation, e.g. dashboard->home.

use Mvc5\App;
use Mvc5\Plugin\Plugins;
use Mvc5\Plugin\Value;

$app = new App([
    'services' => [
        'dashboard' => new Plugins(
            [
                'home' => function($template) {
                    return function($view, $form) use($template) {
                        return $this->call($view, [$template, $form + ['message' => 'Demo Page']]);
                    };
                },
                'template' => new Value('dashboard/index')
            ],
            new Link, //reference to parent container
            true //use current container as the scope for anonymous functions
        ),
        'view' => function() {
            return function($template, $var) {
                include $template;
            };
        },
    ]
]);

//$app['dashboard']['home'];
//$app['dashboard->home'];

$app->call('dashboard->home', ['form' => []]);

A container can contain any type of value, except for null. A parent container can also pass itself to a child container as the service provider to use when the child container can not retrieve or resolve a particular value. The parent container can also specify what object to use as the scope of an anonymous service function within the child container.

Autowiring

The required arguments of a class constructor are automatically resolved by a service manager when it instantiates a class that

  • does not have a service plugin configuration,
  • or no arguments are passed to the plugin method,
  • or the arguments are named.

The service locator will resolve the required constructor arguments either by their type hint, or parameter name.

For a function or class method being invoked by the call method, the missing required arguments are also automatically resolved. However, they are resolved in the opposite order, first by their parameter name, then by their type hint. Otherwise, an exception will be thrown since the required parameter has not been provided.

Plugins

Various types of plugins are available to use and custom plugins can be created.

App

'dashboard' => new App(new FileInclude(__DIR__ . '/dashboard.php')),

The app plugin is used to provide a scoped instance of the Mvc5\App and uses the current application as its fallback service provider.

Args

'request' => [
    Request\Config::class,
    'config' => new Args([
        'hostname' => new Call('request.getHost'),
        'method'   => new Call('request.getMethod'),
        'path'     => new Call('request.getPathInfo'),
        'scheme'   => new Call('request.getScheme')
    ])
],

The args plugin is used to return an array of values that are resolved just in time, e.g. when the class is being instantiated.

Call

new Call('Home\Controller', ['response' => new Plugin('Response')])

The call plugin is used to invoke an object or method and supports named arguments and plugins.

Callback

new Callback(function() {
    $messages = $this->plugin('messages');
})

A callback plugin binds the scope of the application to a closure. The closure can then access the application’s public service methods.

Calls

'route\dispatch' => new Calls(new Plugin(Mvc5\Route\Dispatch::class), ['service' => new Link])

The calls plugin is similar to a hydrator and is used to resolve a plugin with a set of function calls and supports named arguments.

Child

'manager'         => new Plugin(null),
'service\manager' => new Child(Service\Manager::class, 'manager'),

A child plugin is used to extend a parent plugin. The first parameter is the name of the class to create and the second is the name of the parent plugin. Custom child configurations can also be created to allow another plugin to be used without having to specify the name of its parent. Examples are the controller, factory, form and manager plugins.

Config

'Home\Controller' => [Home\Controller::class, 'config' => new Config]

The config plugin is used to provide the main configuration array or object.

Controller

'controller'      => new Hydrator(null, ['request' => new Plugin('request')]),
'Home\Controller' => new Controller(Home\Controller::class),

A controller plugin is used to provide the constructor arguments and call methods for a controller without having to specify the name of its parent controller plugin. This is a convenience plugin for when controllers have a similar method of instantiation.

Copy

new Copy(new Plug('response'))

The copy plugin can be used to resolve and clone an object.

End

new End(new Call('@session_start'), new Plugin(Session\Config::class));

The end plugin will resolve a list of plugins and return the result of the last plugin.

Factory

'factory'         => new Service(null),
'Home\Controller' => new Factory(Home\Controller::class),

A factory plugin is used to create a class object without having to specify the name of its parent factory plugin.

FileInclude

new FileInclude('config/templates.php'),

A file include plugin is used to include and evaluate a specified file. The name of the file can also be resolved via another plugin.

Filter

'response' => new Filter(
    new Plugin(Response::class), [function($response) { return $response;  }], [], 'response'
)

A filter plugin is used to pass a value from one function to the next and returns the result of the last function called. If a function returns null, the iteration is stopped and null is returned. If a function returns false, the iteration is stopped and the previous value, or current object, is returned. The third parameter contains any additional arguments and the fourth parameter specifies the name of the argument for the value that is being filtered.

Form

'form'    => new Service(null),
'my\form' => new Form(My\Form::class),

A form plugin is used create a class object without having to specify the name of its parent form plugin.

GlobalVar

new GlobalVar('_COOKIE')

A global var plugin is a value plugin that returns the value assigned to the PHP $GLOBALS array for the specified parameter name.

Hydrator

'route\dispatch' => new Hydrator(Mvc5\Route\Dispatch::class, ['service' => new Link])

A hydrator plugin is used to create an object with a resolvable plugin name and a set of calls to invoke. Using null for the parameter name is a convenient way for it to be used as a parent plugin. When the array key of its calls configuration is a string, it is used as the name of the method to call on the newly created object and passes its array value as a single resolvable argument. However, if the string is prefixed with the $ symbol, the string is used as the name of the object property to set. If a method needs to be called more than once, then an array of methods can be used.

'route'  => new Hydrator(
    Mvc5\Route\Config::class, [['set', 'controller', 'Home\Controller'], ['set', 'name', 'home']]
)

Additionally, any resolvable plugin can be called.

'route'  => new Hydrator(Mvc5\Route\Config::class, [new Call('response.setStatus', [500])]),

When an array configuration is used, the current object is passed to the called methods as a named argument called item. This can be changed by adding a value to the beginning of the array with the name of the parameter to use.

'service'  => new Hydrator(
    ArrayObject::class, ['$current', new Object, 'index' => 'foo', 'bar' => 'baz']
)
class Object
{
    function __invoke($index, $current, $bar)
    {
        return $current[$index] = $bar; //i.e $current['foo'] = 'baz'
    }
}

Invokable

$response = $app->call(new Invokable(new Plugin('response')));

An invokable plugin is used to return an anonymous function. When invoked, it will resolve and return its configured value.

Invoke

new Invoke('response.setStatusCode', [500]),
new Invoke(new Args([new Plugin('response'), 'setStatusCode']), [500]),
new Invoke(function() { var_dump(func_get_args()); }),

An invoke plugin is used to return an anonymous function. When invoked, it will resolve and call its configured value with the optional array of parameters passed to the anonymous function. The parameters are merged with the plugin’s args parameters.

$app->call(new Invoke(new Plugin('Home\Controller')), ['request' => new Plugin('Request')])
'request\service' => [Mvc5\Request\Service::class, new Link]

A link plugin is used to return the current service object. It can also be used as a configuration object to delay the creation of a particular value until it is required.

NullValue

new NullValue

A null value plugin is used to return a null value. It can be used to prevent a callback provider from being invoked.

Param

new Param('templates.home')

A param is used to retrieve a configuration value and uses a dot notation for arrays and objects with ArrayAccess.

Plug

new Plug('controller\exception')

A plug is used to return the value of another plugin configuration.

Plugin

'router' => new Plugin(Mvc5\Route\Router:class, [new Param('routes')], ['service' => new Link])

A plugin is used to instantiate a class object. It requires the name of a resolvable plugin and optionally, its constructor arguments and a set of calls to invoke. See the hydrator plugin for details on how to specify the calls to invoke.

Plugins

new Plugins(__DIR__.'/services.php')

A plugins config is used to instantiate an application class. The first parameter is an array of service plugins, the second parameter defaults to using the current application as the provider for any missing services. The third parameter sets the application as the scope for any anonymous functions within service array plugin configuration.

Provide

new Provide($config, array $args = [])

The provide plugin is used to retrieve a value from its parent container.

Register

'user' => new Register('user', 'session', new Plugin(Mvc5\Config::class))

The register plugin will create an object if it is not already registered with a specified (configuration) object. The first parameter is the registered name, the second parameter is the name of the service configuration that should contain the registered object. The third parameter is the plugin configuration for the object to create and register if it does not already exist.

Response

'web' => new Response('web')

A response plugin is used to dispatch a response. It configures the response dispatch plugin with the name of the event to use and an optional request and response object. Each event function can return a request or response object for the the next function to use.

Scope

new Scope(
  Request\Config::class, 
  [new _Plugin(App::class, [[Arg::SERVICES => $plugins], null, true, true]), new Link]
)

The scope plugin is used to set the scope of an application to the class that is being created. A Closure within the application’s configuration will then have the same scope as the class being created and can access its protected properties.

Scoped

new Scoped($this)

The scoped plugin is used to provide a Closure with the scope of the application. The first parameter of the plugin is a function that is called when the plugin is being resolved. The function returns a Closure and its scope is set to the scope of the application.

ScopedCall

new ScopedCall($this)

The scoped call plugin extends the call plugin and uses the scoped plugin to set the scope of the closure for it to be called with.

Service

'router' => new Service(Mvc5\Route\Router:class, [new Param('routes')])

A service plugin is similar to a plugin and is used to add a call to a service method to set the current service object.

Session

'user' => new Session('user', new Plugin(Mvc5\Config::class))

The session plugin is a register plugin that retrieves a session variable. The first parameter is the name of the session variable, the optional second parameter is the plugin configuration for the object to create if it does not already exist in the session. If a value already exists in the session, it will be returned and a new object will not be created.

Shared

new Shared('response')

A shared plugin is used to create a shared value. When used as a configuration value, it should specify another configuration in order to prevent a recursion error. Alternatively, a configuration can be passed as a second argument to its constructor.

'response' => new Shared('response', new Plugin(Http\Response::class))

Value

new Value('A demo web page')

A value plugin is used to return a string value from the services container instead of the main configuration. Otherwise the container will assume that the string is the name of a class to instantiate.

ViewModel

new Model('error/404', ['message' => 'A 404 error occurred'])

A view model plugin is used to create a view model. Its first parameter is the template name and the second parameter contains its values.

Service Providers

Custom plugins can implement the resolvable interface and extend an existing plugin or have their own service provider. A service provider is a callable function that is invoked when a plugin can not be resolved by default.

use Mvc5\Plugin\Config;
use Plugin\Controller;
use Service\Provider;

return [
    'Home\Controller'  => new Controller(Home\Controller::class),
    'service\provider' => [Service\Provider::class, new Config],
];

For example, the home controller uses a custom controller plugin with a service provider for the service resolver event.

function resolve($config, array $args = [])
{
    return $this->resolvable($config, $args, function($config) {
        if ($config instanceof Controller) {
            return $this->make($config->config());
        }

        return $config;
    });
}

The service resolver event is used to call the service provider and an exception is thrown if the plugin can not be resolved.

'service\resolver' => [
    'service\provider',
    'resolver\exception'
],

Named Arguments

The call method can invoke a function with named arguments when they are named or none are provided.

$this->call(Arg::VIEW_RENDER, [Arg::MODEL => $model] + $args);

This allows a function to be called without having to provide all of its required parameters. Typically an exception would be thrown, but before it occurs, a callback function can be used to provide the missing arguments. Consequently, a service manager can provide itself as the callback function to use.

$this->signal($config, $args, $callback ?? $this);

An event can also control the parameters that are provided to its functions, as well as the outcome of each function.

function __invoke(callable $callable, array $args = [], callable $callback = null)
{
    $model = $this->signal($callable, $this->args() + $args, $callback);

    null !== $model
        && $this->model = $model;

    return $model;
}

For example, the dashboard:remove event uses three functions to create a model and to return a layout object. It does not have its own event class, so an instance of the default event model is used.

'dashboard:remove' => [
    function() {
        return $model = '<h1>Validate</h1>';
    },
    function($model) {
        return $model . '<h1>Remove</h1>';
    },
    function(TemplateLayout $layout, $model = null) {
        $model .= '<h1>Respond</h1>';

        return $layout->withModel($model);
    }
]

The default event model will store the result of the first function and pass it as the value of the model parameter of the second function. If the first function had required the model parameter, its value would of been null, because no value was given as an argument to the event and no plugin exists for the name model. In this example, the model parameter is a string, so the second function appends to it and returns it as the value of the model parameter of the third function. When the third function is invoked, the signal method recognizes that the layout parameter is missing and uses the callback function to create it. As the model parameter is an argument of the default event model, it can also be used as an optional parameter.


© 2017 Gregory Baboolal