, (*1)
duktig-core
This is the core package for the Duktig micro MVC web framework., (*2)
A full skeleton web project using the Duktig framework is featured in the duktig-skeleton-web-app
package, which uses this core package and implements all its dependencies., (*3)
Table of contents
, (*4)
About
Duktig is a light weight MVC micro web framework written for PHP 7.1. It was primarily created as an educational project, but it is also fully tested and feasible for production use. It implements the MVC pattern and features an IoC container, events system, and uses HTTP middleware., (*5)
, (*6)
duktig-skeleton-web-app
package
The duktig-skeleton-web-app
package is a full-featured project which is based on the duktig-core
package. It can be used as a starting point for developing your own Duktig framework application since it readily implements all the necessary dependencies based on popular open-source projects and packages., (*7)
, (*8)
Purpose
Duktig framework's goal is to deliver a flexible yet powerful framework for creating web applications
by using the most feasible and up-to-date features and practices., (*9)
By learning from some of the most popular PHP web frameworks today (Aura, Silex, Slim, Stack, Yii2, Lumen, Symfony, Laravel, Bullet, Proton), Duktig's core architecture relies on modern principles and standards., (*10)
, (*11)
Standards
-
PSR compliant
- PSR-2 coding, and PSR-4 autoloading standards
- interfaces: PSR-3 logger, PSR-7 HTTP message, PSR-11 container, PSR-15 HTTP middlewares, PSR-17 HTTP factories
- decoupled package design
- powered by popular open source projects and libraries
- TDD developed, unit, integration, and functionally tested
, (*12)
Features
- MVC pattern
- IoC and DI container
- HTTP middleware
- event system
- lazy loading
, (*13)
Package design
Most of Duktig's core services are decoupled from the core package and packaged into their own modules. This kind of approach provides high flexibility and a good package design. Using interface injection, the object graph is naturally composed during the execution., (*14)
, (*15)
Core services
The duktig-core
implements several of its own core services, while leaving out the implementation of most others to external projects:
- configuration service,
- exception handler,
- response sender,, (*16)
These core services are registered in core's 'Config/services.php'
file. If needed they can be overridden and replaced by your own configuration. This is achieved by using the 'skipCoreServices'
config parameter, in which case they must be specified by your own configuration., (*17)
duktig-core
uses the Auryn DI container out-of-the-box, or rather the adapter package duktig-auryn-adapter
which simply adapts its API to Duktig's specification.
The container can be changed to your own choice., (*18)
, (*19)
Requirements
The duktig-core
package defines and uses a number of interfaces which need to be implemented by resolvable services at runtime. The duktig-skeleton-web-app
demonstrates how this is done in a real case, and is a recommended starting point for writing your own Duktig framework application. Briefly put, to implement these requirements, an application (for example the duktig-skeleton-web-app
) first includes all the required packages as composer dependencies and then registers them as services., (*20)
Once implemented, the application has access to the implementations of the following interfaces:
- Interop\Http\Factory\ServerRequestFactoryInterface
and Interop\Http\Factory\ResponseFactoryInterface
- HTTP message factories
- Interop\Http\ServerMiddleware\DelegateInterface
- HTTP middleware dispatcher service
- Duktig\Core\Event\Dispatcher\EventDispatcherInterface
- event dispatcher
- Duktig\Core\View\RendererInterface
- template renderer
- Duktig\Core\Route\Router\RouterInterface
- router service
- Psr\Log\LoggerInterface
- logger service, (*21)
, (*22)
Dependency injection
, (*23)
Container
The DI container must implement the Duktig\Core\DI\ContainerInterface
. This interface is an extension of the Psr\Container\ContainerInterface
with a several methods of its own. The dependency resolution in Duktig is based on constructor arguments injection which is the central feature the container must implement., (*24)
By default, Duktig uses the Auryn container out-of-the-box, or rather the duktig-auryn-adapter
package which adapts to the defined interface. This is defined in the dutig-core
configuration. The container can however be changed to any PSR-11 container which additionally implements the Duktig\Core\DI\ContainerInterface
., (*25)
, (*26)
ContainerFactory
The container itself is instantiated and configured by the Duktig\Core\DI\ContainerFactory
. If the custom container class has any constructor parameters, the ContainerFactory
will try to resolve and inject them by using the ReflectionClass
., (*27)
The ContainerFactory
then configures the container, by running it through the service configurators. The services are configured in your app's services.php
config file., (*28)
, (*29)
Dependency resolution
As with other standard PHP DI containers, the constructor parameter type hinting is used to provide dependency injection. The following entities in the framework are resolved by the container:, (*30)
- services
- controllers
- closure-type route handlers
- middlewares
- event listeners
These entities will have their constructor parameters resolved and injected at runtime. Any dependency can be injected in this way, either if it was previously defined with the container as a service, or even if it is automatically provisioned, which of course depends on if the container of your choice supports the automatic provisioning feature (the Auryn DI container does)., (*31)
, (*32)
Lazy-loading
The framework itself takes advantage of the lazy-loading optimization and delays the object creation in several cases, therefore improving performance. Ie. the controller resolution happens only when the end of the middleware stack is reached, and not sooner. Lazy-loading is naturally related to the container's make()
method implementation. Therefore, if the container of your choice uses lazy-loading (which the Auryn DI container does), it will also be applied throughout the framework work flow., (*33)
, (*34)
Framework components
, (*35)
Routing
duktig-core
defines its routing in terms of several entities. It uses a router which has the job to match the current request to the appropriate route. It also uses a route provider service which provides a simple API for fetching and identifying available routes., (*36)
Router
Duktig's router is featured as a standalone service. The router must implement the Duktig\Core\Route\Router\RouterInterface
. This interface defines only one mandatory method match
which matches the Psr\Http\Message\ServerRequestInterface
object to a Duktig\Core\Route\Router\RouterMatch
object. The RouterMatch
is nothing more than a value object which represents the matched route; it holds the route which was matched and its parameters., (*37)
Route
Duktig\Core\Route\Route
is the Duktig's route model. Its form was heavily influenced by the Symfony Route's route model, as it is one of the most feature-rich and popular route representations in the open-source community., (*38)
RouteProvider
Duktig\Core\Route\RouteProvider
is a service which provides access to the routes. It accesses the routes' configuration from the configuration services, and converts it to the Route
objects, exposing them in a way of a several user-friendly API methods., (*39)
Route handlers
A route can have two different kinds of resolvers:, (*40)
- the first is a classical controller with an action method, where the controller extends the
BaseController
class exposing access to the request and some essential components,
- the second is a closure type handler, which is given directly in route configuration.
Both types of route handlers must return a ResponseInterface
type object. For a closure type handler, it is recommended to use the Interop\Http\Factory\ResponseFactoryInterface
to create a response instance, while a controller will already have the response prepared for use via the BaseController
parent class., (*41)
Both types of handlers are dynamically resolved by the container, and have their constructor arguments dependencies injected upon creation., (*42)
, (*43)
Controllers
Controllers are assigned to routes and are in charge of generating a response. Alternatively, instead of defining special controller classes, a simpler closure-type route handlers can be used., (*44)
BaseController
All controller classes should extend the base Duktig\Core\Controller\BaseController
in order to get access to the application context, including the properties:
- $request
- the PSR-7 request object
- $response
- - a "fresh" PSR-7 response object
- $queryParams
- parsed URI parameters
- $renderer
- template rendering service
- $config
- configuration service, (*45)
BaseController
also provides methods for quicker manipulation of the response object and its rendering., (*46)
Route parameters
Route parameters are passed to the controller as the action method's parameters. Ie. if a route uses one string parameter $param
, and assigns it to the exampleAction
method, the parameter will be available to the action method in this way:, (*47)
public function exampleAction(string $param) : ResponseInterface;
Return type
Every controller or route handler must return a PSR-7 response object. The $response
property is available for use within controller that extend the main BaseController
class, which is internally generated by a PSR-17 $responseFactory
service., (*48)
Dependency injection
Controllers will have their constructor parameters resolved and injected at runtime. Controllers, among other entities, are by default not given the access to the container, as this is widely considered as the service locator anti-pattern. However, no special restriction is imposed on this approach neither, and could easily be implemented. This practice is, however, strongly discouraged., (*49)
It may be quite needless to point this out specifically, but, naturally, when your controller defines it's own dependencies, it must also pay respect to its parent's dependencies as well, ie.:, (*50)
<?php
namespace MyProject\Controller;
use Duktig\Core\Controller\BaseController;
use Interop\Http\Factory\ResponseFactoryInterface;
use Duktig\Core\View\RendererInterface;
use Duktig\Core\Config\ConfigInterface;
use MyProject\Service\CustomService;
class IndexController extends BaseController
{
private $customService;
public function __construct(
CustomService $customService,
ResponseFactoryInterface $responseFactory,
RendererInterface $renderer,
ConfigInterface $config
)
{
parent::__construct($responseFactory, $renderer, $config);
$this->customService = $customService;
}
}
Lazy loading
The controller is resolved and instantiated only at the point when it is reached by the command chain. The special ControllerResponder
middleware is used to resolve and execute the controller, and return its response to the application., (*51)
, (*52)
Middleware
Duktig uses the "single-pass" HTTP middleware which corresponds to the PSR-15 specification. It implies the implementation of the Psr\Http\ServerMiddleware\MiddlewareInterface
and the method with the following signature:, (*53)
public function process(ServerRequestInterface $request, DelegateInterface $delegate);
Likewise the middleware dispatching system must implement the Psr\Http\ServerMiddleware\DelegateInterface
with the following method:, (*54)
public function process(ServerRequestInterface $request);
Duktig leaves out the implementation of the middleware dispatching system from its core functionality, and delegates it to an external package., (*55)
Application and route middleware
Two kinds of middlewares are used in Duktig:, (*56)
- application middleware - global for the whole application, it is run on each request,
- route middleware - variable, can be assigned to a specific route.
ControllerResponder
The ControllerResponder is a special middleware which lies at the end of the middleware stack. It resolves the route handler, calls it, and returns its response back to the middleware stack. Since it is used as a "responder" from the route handler's perspective, hence its name., (*57)
The middleware stack
The full middleware stack which the request traverses consists of:
- application middleware
- route middleware
- the ControllerResponder
middleware, (*58)
Dependency injection
All middlewares are instantiated by the container, therefore will have their constructor dependencies injected., (*59)
, (*60)
Templating
The template rendering service is defined by the Duktig\Core\View\RendererInterface
. It provides a simple API necessary to use the templating., (*61)
, (*62)
Events
Event dispatcher
The event dispatcher is defined by the Duktig\Core\Event\Dispatcher\EventDispatcherInterface
. It implies that a container is provided to the dispatcher, which is then used to resolve the listeners. Therefore the event listeners will have their dependencies injected and be lazy-loaded when their events are dispatched., (*63)
Event
Events in Duktig are simply value objects which contain the contextual information for the listeners to act upon. It is also correct to say that an event is just a value object with a unique name., (*64)
Two different event types can be used in Duktig., (*65)
Event as its separate class
An event can be created as its own class, which must extend the Duktig\Core\Event\EventAbstract
class., (*66)
In this case, its name can but does not have to be specifically provided (as the constructor parameter), and a default event's name will be its fully qualified class name without the prefix backslash. Ie. for an event class MyProject\Event\CustomEvent
, its default name will be 'MyProject\Event\CustomEvent'
., (*67)
Here is a simple example of firing an event which is defined in its own separate class. Let us assume the UserEvent
takes the parameter $user
as the constructor parameter. Dispatching this event is as simple as:, (*68)
$event = new \DemoApp\Event\UserEvent($user);
$eventDispatcher->dispatch($event);
Simple event
In the case of a simplest event which is represented only by its unique name, and does not need to hold any other information for the listener's handler to use, instead of writing a separate class for the event, the existing Duktig\Core\Event\EventSimple
class can be used to instantiate an event on-the-fly., (*69)
In this case, a unique event name must be given to the constructor. Since the EventSimple
can be used to instantiate different events, each of those events is held responsible for their own unique naming., (*70)
The simple event can be dispatched by instantiating the EventSimple
object on the fly, ie:, (*71)
$eventDispatcher->dispatch(new \Duktig\Core\Event\EventSimple('theEventName'));
Listeners
The event listener may either be provided as resolvable class/service name or as a simple closure., (*72)
In the first case, in which the listener is a separate class, it must implement the Duktig\Core\Event\ListenerInterface
. When the event is dispatched, the listener is be resolved by the container and have all its constructor dependencies injected., (*73)
In case the listener is given as a simple closure, it is not resolved by the container, so no dependencies will be injected. The closure-type listener expect only one optional argument, the event:, (*74)
function($event) { /* ... */ }
Core events
The framework dispatches its core events throughout the points of interest in the application flow. The full list of Duktig's core events is found in the duktig-core
's events.php
file. Some core events are only defined by their unique names (ie. 'duktig.core.app.afterConfiguring'
), while others are created as separate classes., (*75)
, (*76)
Error handling
Duktig uses its own error and exception handler which implements the Duktig\Core\Exception\Handler\HandlerInterface
. Its basic tasks are to register the error handling throughout the application, to convert a \Throwable
into a response, and to report the occurence of such an error., (*77)
It takes in cosideration the PHP 7 error and exception handling mechanisms. From the PHP 7 version, both the \Error
and the \Exception
classes implement the \Throwable
interface. Instead of halting script execution, some fatal errors and recoverable errors now throw exceptions. Also, an uncaught exception will continue to produce a fatal error, and in this same way an \Error
exception thrown from an uncaught fatal error still produces a fatal error., (*78)
In production environment, exceptions and errors are rendered through default or custom error templates. Handler prioritizes the templates by their locations and names. It searches for the most specific template it can find for the given throwable, while first trying to locate the template in the application custom template path, and if none are found it uses the default templates from the duktig-core
package. It searches and renders an error template in following steps:
- if an HttpException
is thrown, it searches for the template with the error code for its name,
- if no such template is found, as well as for all other exception types, it looks for a template
with name equal to the exception class name,
- finally, it searches for a generic error template., (*79)
The renderer service is itself given the location of the templates, both for the custom templates within the application dir, and the default templates whithin the framework core dir. In this way it first looks for custom, and then for default templates., (*80)
, (*81)
Configuration
The configuration specifics are described within the duktig-skeleton-web-app
project where it is seen in action. The skeleton application takes the duktig-core
and provides it with all its dependencies, employing it into the full Duktig environment., (*82)
, (*83)
Testing
The duktig-core
and all the the other packages implemented by the duktig-skeleton-web-app
are fully tested using PHPUnit and Mockery., (*84)
A special Duktig\Test\AppTesting
class is available for the testing environment. It extends access to the container and to the response object. It can be used to easily mock services, and to gain direct access to the response., (*85)