Source Code

Latest Release

Or install using Composer.

composer.phar require mvc5/mvc5

Demo Application

Install the demo mvc5-application.

composer.phar create-project mvc5/mvc5-application mvc5playground

The document root is the public directory and in a development environment, error reporting should be enabled in the php.ini file.

php -S localhost:8000 -t mvc5playground/public

Docker Project

Alternatively, install the PHP Docker Project and run docker-up -a. The demo mvc5-application project contains a Compose file that configures the version of PHP to use and the development tools to install. Traefik is used as a reverse proxy so that multiple projects can run at the same time. Shared services, such as Adminer, MailHog, MariaDB and PostgreSQL, can also be started at the same time as the project container. Other commands are available to provide a convenient way to work with a PHP project and Docker, e.g. docker-app, docker-phpunit and docker-xdebug.

Directory Structure

Mvc5

The src directory contains the PSR-4 composer autoloaded class files.

The config directory contains the php array configuration files that can be included and overridden by the application.

The view directory contains error and exception templates that can be configured by the application.

Application

The config directory contains the php array configuration files that includes and overrides the Mvc5 configuration. These configuration files can also be environment aware.

The public directory is the web site document root and contains the main index.php script.

The src directory contains the application class files and uses the PSR-4 autoloading standard.

The vendor directory is used for external libraries including the mvc5 directory.

The view directory contains the view model templates.

Create A Web Page

Copy and paste the example code into each new file.

1. Create a new view model file in the src/Home directory named ViewModel.php.

<?php

namespace Home;

class ViewModel
    extends \Mvc5\ViewModel
{
    /**
     *
     */
    const TEMPLATE = 'home/index';
}

The constant TEMPLATE_NAME can be used as the name or file path of the view model's associated template and is assigned within the constructor when no constructor arguments are given. Read more about view models.

2. Create a new controller file in the src/Home directory named Controller.php.

<?php
                                 
namespace Home;

use Mvc5\Http\Request;
use Mvc5\View;

class Controller
{
    /**
     *
     */
    use View\Model;
    
    /**
     *
     */
    const VIEW_MODEL = ViewModel::class;
    
    /**
     * @param Request $request
     * @return ViewModel
     */
    function __invoke(Request $request) : ViewModel
    {
        return $this->model(['msg' => 'Hello World!']);
    }
}

When the above controller is invoked, it returns a copy of the view model with the msg variable assigned to it. The view model will become the response model and will be rendered prior to sending the response.

3. Create a new template file in the view/home directory named index.phtml

<?php
                                 
  echo '<h1>' . $msg . '</h1>';

The variables assigned to a view model are available within the template as PHP variables, e.g $msg or via the current view model object, e.g. $this->msg. Read more about rendering view models.

Functional Programming

When the application is opened in the web browser, the main public/index.php script is called. However, for this example a simpler version is provided below without any exception handling.

<?php

use Mvc5\App;
use Mvc5\Arg;

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

(new App(include __DIR__ . '/../config/config.php', null, true))->call(Arg::WEB);

After loading the application with a configuration, the 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.

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

When the url in the web browser is changed from / to /dashboard the output of the application will be /dashboard. The parameters $request and $response are required and instead of an error occurring, they are resolved as named arguments using the service plugin configuration. The anonymous function is the only function called by the application, unlike the original web function in the main public/index.php script. The service plugin configuration for the default web function is below.

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

If no service or event configuration exists for the name web, then an exception will occur because a plugin for that name does not exist. Instead, a function with that name can be added to the main public/index.php script.

function web($request, $response) {
    var_dump($request->path());
}

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

By default, the call method assumes that a string is the name of an object, or an event to invoke. However, since it is a function, its name must be prefixed with the @ sign to indicate that it is a function (or a static class method) and should be invoked directly instead of creating an object.

Since the argument to the call method must resolve to a callable type, the web configuration can also be an anonymous class.

'web' => new class() {
    function __invoke($request, $response) {
        var_dump($request->path());
    }
}

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

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

However, a function can become large and separating it into a list of functions enables it to be extended with each function having their own individual dependencies. Consequently, the outcome of the function does not have to depend on the list of functions and by using an event class, the outcome of each function and the function itself can be controlled.

Events

When there is no service configuration and the function name is not callable, the call method will create an event object and invoke it. If an event configuration does not exist, then an exception will be thrown. Read more about events and named arguments.

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

$config['events']['web'] = [
    fn($model, $msg) => $model . ' ' . $msg,
    fn($response, $model) => $response->with('body', $model),
    function($response) {
        echo $response->body();
    }
];

(new Mvc5\App($config))->call('web', ['model' => 'Hello', 'msg' => 'World!']);

Middleware

An alternative to using an event is to call a list of functions using a middleware handler. A delegate function is passed to the current function which can optionally call it to invoke the next function on the stack. If the last function calls the next delegate function again, the last argument passed to the delegate function is returned. Read more about Middleware.

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

$config['middleware']['web'] = [
    fn($request, $response, $next) => $next($request->with('controller', fn() => 'Hello!'), $response),
    fn($request, $response, $next) => $next($request, $response->with('body', $request['controller']())),
    function($request, $response, $next) {
        echo $response->body();
    }
];

(new Mvc5\App($config))->call('web\middleware');