Skip to content

efueger/framework

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Build Status Code Coverage Scrutinizer Code Quality
Build Status Test Coverage Code Climate
Total Downloads License
SensioLabsInsight
Gitter

http://mvc5.github.io

Welcome to an enhanced php programming environment that provides inversion of control of a web application or any function.

Features

  • Configuration
  • Maintainability
  • Controller Dispatch
  • Route Matching
  • Response
  • View
  • Exceptions
  • Dependency Injection
  • Constructor Autowiring
  • Plugins
  • Configurable events and custom behaviours
  • Calling methods using named arguments and plugin support
  • Controller Action - a Middleware like event.
  • Console Applications

All of the components require dependency injection and use configuration objects for consistency and ease of use. For example, the service manager uses it to manage service objects, and has additional service container methods for the underlying configuration of the services that it provides. The main service configuration array can contain real values, string names, callable types and configuration objects that are resolvable by the service manager.

Maintainability

Being able to makes changes to a project, especially beyond its initial development, is an important aspect of any project.

View the interactive PhpMetrics report

Benchmark

Current

HTML transferred:       7331150 bytes
Requests per second:    936.49 [#/sec] (mean)
Time per request:       10.678 [ms] (mean)
Time per request:       1.068 [ms] (mean, across all concurrent requests)

Other/Previous

HTML transferred:       5502000 bytes
Requests per second:    315.78 [#/sec] (mean)
Time per request:       31.667 [ms] (mean)
Time per request:       3.167 [ms] (mean, across all concurrent requests)

Source Lines of Code

SLOC	Directory	SLOC-by-Language (Sorted)
1144    Service         php=1144
1030    Route           php=1030
381     View            php=381
249     Controller      php=249
213     Mvc             php=213
199     Response        php=199
110     Event           php=110
92      Config          php=92
32      Application     php=32


Totals grouped by language (dominant language first):
php:           3450 (100.00%)

Generated using David A. Wheeler's SLOCCount.

Usage

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

include __DIR__ . '/../vendor/autoload.php';
use Mvc5\Service\Container\Container;

$config = [
    'alias'     => include __DIR__ . '/alias.php',
    'events'    => include __DIR__ . '/event.php',
    'services'  => new Container(include __DIR__ . '/service.php'),
    'routes'    => include __DIR__ . '/route.php',
    'templates' => include __DIR__ . '/templates.php'
];
(new App(include __DIR__ . '/../config/config.php'))->call('web');

Web Application

A default configuration is provided with the minimum configuration required to run a web application. Then, all that is required are the request and response objects, template configuration and the routes to use. Routes must have a name, so that they can be used to build urls in the view templates with the url plugin.

new App(include __DIR__ . '/../vendor/mvc5/framework/config/config.php')->call('web');

Console Application

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

./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 and the remaining arguments are its parameters. E.g.

namespace Console;

use Mvc5\View\Model\ViewModel;

class Example
{
    protected $model;
    
    public function __construct(ViewModel $model)
    {
        $this->model = $model;
    }
    
    public function __invoke($day, $month)
    {
        var_dump($this->model);
        echo $day . ' ' . $month . "\n";
    }
}

Read more about dependency injection and constructor autowiring on how the dependencies of a function can be resolved. Note, that it is also possible to create a console application similar to a web application with routes and controllers.

Environment Aware Configurations

Development configurations can override production values using array_merge since each configuration file returns an array of values. E.g

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

In the above example, the development configuration file config/dev/config.php includes the main production file config/config.php and overrides the name of the database to use in development.

Configuration and ArrayAccess

A standard configuration interface is used consistently throughout each component in order to provide a standard set of concrete configuration methods. Its ArrayAccess interface enables the service manager to retrieve nested configuration values, e.g.

new Param('templates.error');

Resolves to

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

Which makes it possible to use either an array or a configuration object when references are needed, e.g templates and aliases.

interface Configuration
    extends ArrayAccess
{
    function get($name);
    function has($name);
    function remove($name);
    function set($name, $config);
}

Implementing the configuration interface allows components to only have to specify their immutable methods and allows the component to choose whether or not to extend the configuration interface or to implement it separately. The idea is that most of the time, only the immutable methods are of interest and the configuration interface provides a consistent way of instantiating it.

interface Route
    extends Configuration
{
    const CONTROLLER = 'controller';
    const PATH = 'path';
    function controller();
    function path();
}

Constants can be used by other components to update the configuration object via ArrayAccess.

$route[Route::PATH] = '/home';
//or
$route->set(Route::PATH, '/home');

Routes

A route definiton is a configuration object that can also be configured as an array, and contains the properties required to match a route. Before matching, if the definiton does not have a regex, it will be compiled against the route's request uri path. Each aspect of matching a route has a dedicated function, e.g. scheme, hostname, path, method, wildcard, and any other function can be configured for the route match event.

In order to build a url using the route plugin, e.g. as a view helper plugin, the base route must have a name, which is typically the homepage for /, e.g home, or it can specify its own, e.g /application. Child routes except for the first level, will automatically have their parent name prepended to their name, e.g blog/create. First level routes will not have the base route prepended as it keeps their name simpler when specifying which route definition to build, e.g. blog instead of home/blog.

The controller param must be a service configuration value (which includes real values) that must resolve to a callable type. If no service configuration for the controller exists but its class does, a new instance will be created with constructor autowiring.

Controller configurations that are prefixed with an @ will be called, so they must resolve to a callable type or be the name of an event. In the example below, @Blog.test will call the test method on a shared instance of the Blog controller. @blog.remove will call the blog:remove event. And @blog:create is an alias to a blog create event.

Constraints have named keys that match their corresponding regex parameter and optional parameters are enclosed with square brackets []. This implementation is from the DASPRiD/Dash router project.

return [
    'name'       => 'home', //for the url plugin in view templates
    'route'      => '/',
    'controller' => 'Home\Controller', //callable
    'children' => [
        'blog' => [
            'route'      => 'blog',
            'controller' => '@Blog.test', //specific method
            'children' => [
                'remove' => [
                    'route' => '/remove',
                    'controller' => '@blog:remove'
                ],
                'create' => [
                    'route'      => '/:author[/:category]',
                    'defaults'   => [
                        'author'   => 'owner',
                        'category' => 'web'
                    ],
                    'wildcard'   => false,
                    'controller' => '@blog:create', //call event
                    //'controller'  => function($request) { //named args
                        //var_dump($request->getPathInfo());
                    //},
                    'constraints' => [
                        'author'   => '[a-zA-Z0-9_-]*',
                        'category' => '[a-zA-Z0-9_-]*'
                    ]
                ]
            ],
        ]
    ]
];

Route definition names are used by the url route generator, e.g

echo $this->url('blog/create', ['author' => 'owner', 'category' => 'oop']);

Model View Controller

Controllers can use a configuration object as a view model object that is rendered by the view using a specified template file name and an optional child model which is used by the layout model. For convenience, controllers can use an existing view model trait that has methods for setting the model and returning it. If no model is injected, then a new instance of a standard model will be created and returned. When a controller is invoked and returns a model, it is stored as the content of the response object and will be rendered prior to sending the response. The view model trait has two methods

This allows the controller to choose the view method when a specific template is known, or the controller can use the model method and pass an array of variables as the data for the view model.

use Mvc5\View\ViewModel;

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

Controller Action

The controller action service configuration is for an action controller event which accepts an array of functions that are called with named argument plugin support. If the response from the function is a view model, it will be stored and become available to the remaining functions. If the function returns a response, then the event is stopped and the response is returned.

'controller' => new ControllerAction([
    function(array $args = []) {
        return new Model(null, ['args' => $args]);
    },
    function(Model $model) {
        $model['__CONTROLLER__'] = __FUNCTION__;
        return $model;
    },
    function(Model $model) {
        $model[$model::TEMPLATE] = 'home';
        return $model;
    },
]),

Rendering View Models

When the content of the response is a view model, it is rendered prior to sending the response. Additionally, and prior to rendering the view model, if a layout model is to be used, it will add the current view model to itself as its child content and the layout model is then set as the content of the response.

function __invoke($model = null, ViewModel $layout = null)
{
    if (!$model || !$layout) {
        return $model;
    }
    
    if (!$model instanceof ViewModel || $model instanceof LayoutModel) {
        return $model;
    }
    
    $layout->child($model);
    
    return $layout;
}

The view model is then rendered via the view render event which allows other renderers to be configured and used instead of the default view renderer.

The view renderer will bind the view model to a closure that will extract the view model variables and then include the template file. The scope of the template is the view model itself which gives the template access to any of the view model's private and protected properties and functions.

$render = Closure::bind(function() {
    extract((array) $this->assigned());
    
    ob_start();
    
    try {
    
        include $this->path();
        
        return ob_get_clean();
    
    } catch(Exception $exception) {
    
        ob_get_clean();
    
        throw $exception;
    }

},
$model
);

return $render();

View Model Plugins

The default view model also supports plugins which require the view manager to be injected prior to rendering it. However, because they can be created by a controller, this may not of happened. To overcome this, the current view manager will be injected into the view model if it does not already have one.

<?php

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

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

Events

Events can be strings or classes that can manage the arguments used by the methods that invoked it.

class Event
{
    function args()
    {
        return [
        Args::EVENT      => $this,
        Args::RESPONSE   => $this->response(),
        Args::ROUTE      => $this->route(),
        Args::VIEW_MODEL => $this->viewModel(),
        Args::CONTROLLER => $this->route()->controller()
        ];
    }
    
    function __invoke(callable $listener, array $args = [], callable $callback = null)
    {
        $response = $this->signal($listener, $this->args() + $args, $callback);
        
        if ($response instanceof Response) {
        $this->stop();
        }
        
        return $response;
    }
}

The callable $callback parameter can be used to provide any additional parameters not in the named $args array. It can be a service manager, e.g $this, or any callable type.

$this->trigger([Dispatch::CONTROLLER, $controller], $args, $this);

Similar to $args in named arguments, adding $event will provide the current event.

The trigger() method of the event manager accepts either the string name of the event, the event object itself or an array containing the event class name and its constructor arguments. In the example above, $controller is a constructor argument for the controller dispatch event.

Event Configuration

Events and listeners are configurable and support various types of configuration that must resolve to being a callable type.

'Mvc' => [
    'Mvc\Route',
    'Mvc\Controller',
    'Mvc\Layout',
    'Mvc\View',
    function($event, $vm) { //named args
        var_dump(__FILE__, $event, $vm);
    },
    'Mvc\Response'
]

Dependency Injection

The service manager implements the configuration interface by extending the service container. The configuration interface provides access to existing services, and the service container provides access to the configuration object that contains the configuration values for the services that the service manager provides.

Typically the configuration is the application's main configuration object.

return [
    'alias'     => include __DIR__ . '/alias.php',
    'events'    => include __DIR__ . '/event.php',
    'services'  => new Container(include __DIR__ . '/service.php'),
    'routes'    => include __DIR__ . '/route.php',
    'templates' => include __DIR__ . '/templates.php'
];

This allows the service manager to use the param() method to retrieve other configuration values, e.g

new Param('templates.home')

When a service is called by the service manager's configuration interface, it will check that the service exists, and if it does not, it will use its configuration to create a new service. Configuration values can also be actual values e.g

'Request' => new HttpRequest($_GET, $_POST, $_SERVER ...)

They can also be strings that specify the FQCN of the class to instantiate and their required dependencies are automatically injected via constructor autowiring. E.g

'Route\Match\Wildcard' => Route\Match\Wildcard\Wildcard::class,

Constructor arguments can also be passed as arguments to the service manager when calling the service via the create or get method, e.g

$sm->get('HomeController', [new Dependency('HomeManager')])

Arguments passed to service manager will be used as constructor arguments. If no arguments are passed, the service can still be configured with constructor arguments via the service configuration.

'Route\Generator' => new Service(
    Route\Generator\Generator::class,
    [new Param('routes'), new Dependency('Route\Builder')]
),

In the example above the route generator is created with the routes configuration passed as a constructor argument.

Sometimes only calls to the setter methods are needed, in which case the hydrator configuration object can be used.

'Controller\Manager' => new Hydrator(
    Controller\Manager\Manager::class,
    [
        'aliases'       => new Param('alias'),
        'configuration' => new ConfigLink,
        'events'        => new Param('events'),
        'services'      => new Param('services')
    ]
),

A service configuration can have parent configurations which allows either the parent constructor arguments to be used, if none are provided, or the parent configuration may specify the calls to use. It is also possible for a service configuration to merge calls together.

'Manager' => new Hydrator(null, [
    'aliases'       => new Param('alias'),
    'configuration' => new ConfigLink,
    'events'        => new Param('events'),
    'services'      => new Param('services'),
]),

The above hydrator is used as a parent configuration for all Managers.

'Route\Manager' => new Manager(Route\Manager\Manager::class)

A dependency configuration object is used to retrieve a shared service.

Constructor Autowiring

When a service manager creates a class that either

  • Does not have a service configuration, or
  • No arguments are passed to the service manager, or
  • If the arguments passed are named

Then the service manager will attempt to determine the required dependencies of the class constructor and their type hint. Service configurations can return any positive value.

Home\Model::class => new Service(Home\Model::class,['home'])

When the name of a service configuration is a FQCN it must have a value other than its string name, otherwise a recursion will occur.

Home\Model::class => Home\Model::class //not allowed

Service configurations are only required when an explicit configuration is needed, and in some cases, can provide better runtime performance.

Named Arguments and Plugins

This contrived example demonstrates named arguments and plugins.

$web = new App(include __DIR__ . '/../config/config.php');

$response = $web->call(
    'Controller.valid.add.response',
    ['date_created' => time(), 'strict' => true]
);

var_dump($response instanceof Response);

The application is instantiated and a call is made to the valid method of the controller class with its parameters resolved from either the array of arguments explicitly passed to the call method or by the call method retrieving a plugin with the same name as the parameter. Methods can be chained together and each will have their parameters resolved similarly.

class Controller
{
    protected $blog;

    function valid(Request $request, $strict)
    {
        var_dump($strict);
        
        return $this;
    }
    
    function add(Response $response, $date_created)
    {
        var_dump($date_created);
        
        $this->blog = new Blog;
        
        return $this;
    }
    
    function response(ViewManager $vm, Response $response, $args)
    {
        var_dump($this->blog, $args);
        return $response;
    }
}

The output of the above is ...

boolean true

int 1414690433

object(Blog\Blog)[100]

array (size=2)
'date_created' => int 1414690433
'strict' => boolean true

boolean true

The parameter $args can be used as a named argument that provides an array of the named arguments available to that method.

To manage all of the parameters, an optional callback can be added to the call method, e.g

$response = $web->call(
    'Controller.valid.add.response',
    [],
    function($name) { return new $name; }
);

Plugins and Aliases

The parameter names of the additional arguments can be aliases or service names. An alias maps a string of varying characters excluding the call separator . to any positive value. If the value is a service configuration object, then it will be resolved and its value returned. Each plugin has a configuration specific to its own use and they are resolved each time they are used. This enables them to be used in various ways for different purposes, e.g to provide a value, or to trigger an event, or to call a particular service method.

return [
    'blog:create'  => new Service('Blog\Create'),
    'config'       => new Dependency('Config'),
    'layout'       => new Dependency('Layout'),
    'request'      => new Dependency('Request'),
    'response'     => new Dependency('Response'),
    'route:create' => new Dependency('Route\Create'),
    'sm'           => new Dependency('Service\Manager'),
    'url'          => new Dependency('Route\Plugin'),
    'web'          => new Service('Mvc'),
    'vm'           => new Dependency('View\Manager')
];

Note that the plugin method is used when calling an object.

$this->call('blog:create');

Which means invoking a web application is no different to calling any other method, e.g

$app = new Application($config);

$app->call('web'); //invoke web application (event)

And

$app = new Application($config);

$app->call('request.getHost'); //get string hostname from the request object.

And with named arguments

$app->call('Home\Factory', ['config' => $config, 'vm' => $vm]);

To get all of the available arguments that are not plugin arguments, add $args to the method signature

public function __invoke(Config $config, ViewManager $vm, array $args = [])
{
    var_dump($args);
}

Callable PHP Programming

When the application is opened in the web browser the main public/index.php script is called.

use Mvc5\Application\App;
use Mvc5\Application\Args;

include __DIR__ . '/../init.php';

(new App(include __DIR__ . '/../config/config.php'))->call(Args::WEB);

After loading the application with its configuration, its call method is invoked with the string parameter web as the name of the function to call. The parameter passed to the call method must resolve to a callable type, which means the parameter provided can also be an anonymous function.

```php (new App(include __DIR__ . '/../config/config.php'))->call(function($request, $response) { var_dump($request->getPathInfo()); }); ```

When the url in the web browser is changed from / to /blog the output of the application will be /blog. The parameters $request and $response are automatically resolved, because they are required by the anonymous function, as named arguments using the plugin alias configuration. The above anonymous function is the only function called by the application, unlike the web function in the main public/index.php script. The plugin alias configuration for the web function is below.

'web' => new Service('Mvc')

In the following order, if there is no alias, service or event configuration for the name web, then an error would occur as there isn't a callable PHP function with that name. However, one can be added to the main public/index.php script. To easily test this, the name web2 should be used instead.

function web2($request, $response) {
    var_dump($request->getPathInfo());
}

(new App(include __DIR__ . '/../config/config.php'))->call('web2');

Additionally, since the call method argument must resolve to a callable type, the web alias configuration can also be an anonymous function.

'web' => function($request, $response) {
    var_dump($request->getPathInfo());
}

However, since its default value is a Service configuration, an actual service configuration must exist.

'Mvc' => new Service(Mvc\Mvc::class, [new ServiceManagerLink]),

Which means the service configuration can also be an anonymous function that returns another one as the one to invoke.

'Mvc' => function() {
    return function($request, $response) {
        var_dump($request->getPathInfo());
    };
},

This is the limit in which a single anonymous function can be used by the call method. There is also a limit in how much functionality can be obtained from a single function. Functions can become large and splitting them into a list of functions is beneficial since the function becomes an extensible list of functions each with their own specific dependencies injected. Consequently, the outcome of the function does not have to depend on the list of functions. By using an event class it is possible to control the outcome of each function and consequently the function itself.

Callable Events

When there is no alias or service configuration and the string function name is not callable, the call method will check to see if an event configuration exists, if so, it will create and trigger an event for that configuration. Otherwise an exception is thrown since nothing can be found for the name of that function. Read more about events and named arguments.

$config = include __DIR__ . '/../config/config.php';

$config['events']['web2'] = [
    function($request, $one) {
        ob_start();

        echo $one.'. One, ';
    },
    function($two, $request, $response) {
        echo $two.'. Two, ';
    },
    function($response) {
        echo '3. Three';

        $response->setContent(ob_get_clean());
    },
    function($response) {
        $response->send();
    }
];

(new App($config))->call('web2', ['one' => 1, 'two' => 2]);

About

A PHP framework with dependency injection, events, and named arguments

Resources

License

Stars

Watchers

Forks

Packages

No packages published