, (*1)
duktig-skeleton-web-app
This is a skeleton web application made with the Duktig micro MVC web framework written for PHP 7.1, based on iuravic/duktig-core
library., (*2)
Table of contents
, (*3)
About
The duktig-skeleton-web-app
is a starting point for developing your own applications with the Duktig framework. It is founded on the duktig-core
package, and it provides all its necessary dependencies. The skeleton application also contains functional examples of the Duktig framework's major features., (*4)
, (*5)
duktig-core
package
It is advisable to also inspect the duktig-core
documentation which describes the purpose and features of the Duktig framework, as well as explains its base elements and functionalities., (*6)
, (*7)
Package design
Most of the Duktig's core services are fully decoupled from the duktig-core
package. The duktig-core
simply describes them by defining its interfaces, and each of them is implemented as a separate package in the form of an adapter towards a readily available open-source project. This kind of approach provides high flexibility, reusability, and generally stands as a good package design., (*8)
, (*9)
Dependencies
The duktig-skeleton-web-app
composes the full Duktig web application framework by using popular and tested open-source packages. Adapter packages are used simply to adapt the external packages' APIs to the core's interfaces, therefore making them usable by the duktig-core
., (*10)
The following projects and packages are used here to provide the full functionalities for Duktig framework:, (*11)
, (*12)
Core services
The services.php
file shows how these packages are implemented and configured to provide for the core functionality., (*13)
, (*14)
Install
The following command creates a new project via Composer:, (*15)
$ composer create-project -s dev iuravic/duktig-skeleton-web-app {$PROJECT_PATH}
The Duktig packages' repositories are currently not tagged to corresponding versions, for which I apologize at this moment, but this command will never the less correctly resolve and fetch all the dependencies and create your new project., (*16)
, (*17)
Usage and application flow
Let's take a look at a full request-to-response life-cycle, and some of its key elements, in the order in which they come up in the chain of command., (*18)
, (*19)
index.php
A typical index.php file would look like this:, (*20)
<?php
require __DIR__.'/../vendor/autoload.php';
$app = (new \Duktig\Core\AppFactory)->make(
__DIR__.'/../src/Config/config.php',
\Duktig\Core\App::class
);
$app->run();
We see that the app is created by the Duktig\Core\AppFactory
by providing the custom configuration file and the application class., (*21)
, (*22)
AppFactory
The Duktig\Core\AppFactory
creates an instance of the application by:, (*23)
- taking the user configuration and merging it with core configuration,
- instantiating and configuring the DI container through the use of the
Duktig\Core\DI\ContainerFactory
,
- resolving the app with its dependencies.
, (*24)
Request processing and the App
class
The Duktig\Core\App
is the framework's main class and its method run()
is the entry point for the request. The framework "runs" the request through the full application stack. It employs HTTP middleware at its core and composes a middleware stack which consists of:, (*25)
- application middleware,
- route middleware,
- controller responder middleware.
Two kinds of middleware exist in Duktig: the application middleware -- which is used on every request, and the route specific middleware -- which can be assigned to a specific route., (*26)
At the end of the middleware stack lies the ControllerResponder middleware. It is in charged of resolving the controller/route handler, and returning the response object from it to the stack., (*27)
After finishing processing the response, the framework sends it to the browser and terminates the application business., (*28)
, (*29)
Example project functionalities
Within this package several functionalities are implemented as simple show-case examples. These can be looked up as the "how-tos", they can be modified, or simply removed from your project. Basically they should serve to show a quick way around building apps with the Duktig framework., (*30)
Following is a quick list of those functionalities., (*31)
Core services
The 'Config/services.php'
defines and registers all of the core services by using external packages., (*32)
Routes
See route configuration Config/routes.php
., (*33)
Path |
Route |
Features |
/ |
'landing-page' |
Landing page |
/example-controller-action/{myParam} |
'example-route-w-controller' |
URI path params; Route specific middleware |
/example-ip-check |
'example-ip-check' |
/example-callable-handler |
'example-route-with-callable-handler' |
closure-type route handler; |
Controllers
See controller directory Controller/
., (*34)
The DuktigSkeleton\Controller\IndexController
features:
- extension of the BaseController
class
- constructor parameter resolution
- use of external services
- response definition
- template rendering
- redirection
- URI path parameters
- query parameters, (*35)
Middlewares
The following middlewares are implemented with their corresponding features:
- Application-wide middleware ExampleAppMiddleware
:
- registers in the configuration file 'Config/middlewares.php'
- modifies the response body
- Route specific middleware ExampleGetIPRouteMiddleware
:
- assigned to the route 'example-route-w-controller'
- uses an external service by dependency injection
- modifies the application request by adding an attribute to it, (*36)
Events
The event EventIPInRange
is a simple event object which shows how to:
- extend the EventAbstract
class
- use external services
- represent contextual data for its listeners, (*37)
The EventIPInRange
has one listener attached and registered via the Config/events.php
config file, the ListenerReportIP
, which demponstrates how to:
- implement the ListenerInterface
- use external services
- handle the event object and perform a task, (*38)
Services
The ExampleIPService
is a simple service which implements a few basic features:
- the ExampleIPService
itself is being resolved by the use of the container's automatic provisioning feature (notice that it was not specifically registered in the Config/services.php
, since the Auryn implements this feature)
- dependency injection
- access to the configuration service and config parameters, (*39)
Configuration
The following configuration settings are implemented in the skeleton project:
- the base configuration file 'Config/config.php'
with its settings
- setting custom configuration parameters in the 'Config/params.php'
- registering application middleware in the 'Config/middlewares.php'
- registering events with listeners in the 'Config/events.php'
, (*40)
, (*41)
Configuration
Before taking a final step and looking into the configuration, please take a moment to also look the duktig-core
's documentation., (*42)
, (*43)
Configuration files
Duktig's configuration is contained within the simple '.php'
config files inside the Config
directory. Your application's Config
folder should mirror the contents of the duktig-core
's config. The core's and your application's configurations get fully merged at runtime, and all the config values defined in your application overwrite those from the core's. The only exception to this is the services.php
file whose content is not overwritten, but merged with your application's services.php
. In order to skip the core's services configuration, the config parameter 'skipCoreServices'
can be used., (*44)
, (*45)
The configuration service
The configuration service is used to access the configuration parameters and values, that is the contents of the config.php
file. It implements a simple API given by the Duktig\Core\Config\ConfigInterface
., (*46)
It can be accessed via dependency injection, where it is type-hinted as the ConfigInterface
, which will then have the Duktig\Core\Config\Config
service resolved in its place., (*47)
The configuration service is a shared service, meaning that its instantiation will not return a blank instance, but an already configured value object., (*48)
, (*49)
Implementing duktig-core's requirements
The duktig-core
package's has requirements which must be implemented and provided in order to create a full-functioning application environment. Those requirements need to be implemented, and registered with the container. The duktig-skeleton-web-app
package already implements all those requirements; they are packaged separately (see chapter dependencies) and already resolved as the project's requirements., (*50)
, (*51)
Registering services
Services are registered in the Config/services.php
file in your app folder. This file must return a closure which gets the container as it's argument, and returns it after configuring it:, (*52)
<?php
return function($container) {
// ...
return $container;
};
, (*53)
Middleware
Duktig uses the "single-pass" PSR-15 compatible middleware and its dispatching system. Two different middleware types exist within the framework., (*54)
Application middleware
The application middleware is one which is run with every request. It is the app-wide middleware, and is defined in the Config/middleware.php
configuration file in your app's folder. The next example shows how two application middlewares can be assigned:, (*55)
<?php
return [
\MyProject\Middleware\ExampleAppMiddleware1::class,
\MyProject\Middleware\ExampleAppMiddleware2::class,
];
Route middleware
The route middleware is assigned to a specific route and is only run when that route is resolved. The route middleware is defined in your app's Config/routes.php
file by using the 'middlewares'
route config parameter. This brief excerpt shows how one route specific middleware can be assigned to an 'example-route'
route:, (*56)
<?php
return [
'example-route' => [
// ...
'middlewares' => [
\MyProject\Middleware\ExampleRouteMiddleware::class,
],
],
];
, (*57)
Events
Events and listeners can be registered either by using the configuration or programmatically. Even though both mechanisms are available, it is a general recommendation to use configuration to define events and their listeners rather than the programmatic method since the separation of configuration and code generally results in better factoring of the code. However this programming practice should be evaluated on a case-to-case basis., (*58)
Registration via the config file
To register events and their listeners, the Config/events.php
file in your app's directory is used. The following example shows how two events can be registered with a listener each:, (*59)
<?php
return [
\MyProject\Event\EventIPInRange::class => [
\MyProject\Event\ListenerReportIP::class,
],
'custom-event-name' => [
function($event) {
// ...
$event->getName();
},
],
];
The first event exists as a standalone class and uses its fully qualified class name as the event's name. Its listener ListenerReportIP
is defined as a standalone class as well. All such events with a class of their own must extend the Duktig\Core\Event\EventAbstract
class. And all such listeners must implement the Duktig\Core\Event\ListenerInterface
. Both the event and the listener are resolved by the container, and have their constructor dependencies injected., (*60)
The second event is defined by a custom name, and has one closure-type listener assigned to it. The closure-type listener can be assigned to any event, be it a standalone class or an event with a custom name. This listener can take only one argument, the event, and does not get resolved by the container., (*61)
In case the event is determined only by its unique name, and holds no specific contextual information for its listeners, it can be dispatched and instantiatiated as the special Duktig\Core\Event\EventSimple
class. The EventSimple
class creates an event on-the-fly requiring only its unique name., (*62)
Programatic configuration
To attach listeners programatically, we use the event dispatcher's API defined by the Duktig\Core\Event\Dispatcher\EventDispatcherInterface
. The following example demonstrates registering the same two events and listeners as in the previous example where the configuration file was used:, (*63)
$eventDispatcher->addListener(
\MyProject\Event\EventIPInRange::class,
\MyProject\Event\ListenerReportIP::class
);
$eventDispatcher->addListener(
'custom-event-name',
function($event) {
// ...
}
);
Custom listeners can be added to the core events in the same way, therefore tapping access to the framework's internal check-points., (*64)
, (*65)
Routes
Routes are defined in the Config/route.php
file. Since Duktig's route model is heavily influenced by the Symfony's route model, it's elements match it quite closely., (*66)
Here is an example of a route which takes an URI path parameter called myParam
. The parameter gets passed as an argument to the IndexController::exampleAction
. In the following example the route gets an ExampleRouteMiddleware
assigned to it., (*67)
<?php
return [
'example-route-w-controller' => [
'path' => '/example/{myParam}',
'params_requirements' => ['myParam' => '.*'],
'handler' => \MyProject\Controller\IndexController::class,
'handler_method' => 'exampleAction',
'methods' => ['GET'],
'middlewares' => [\MyProject\Middleware\ExampleRouteMiddleware::class],
],
];
Let's take a look at another example with a route that gets an optional trailing slash at the end, and which uses a closure-type handler. This kind of route handler also gets resolved by the container and it's arguments will be resolved and injected:, (*68)
<?php
return [
'example-route-with-callable-handler' => [
'path' => '/example-callable-handler{trailingSlash}',
'params_requirements' => ['trailingSlash' => '/?'],
'handler' => function (\Interop\Http\Factory\ResponseFactoryInterface $responseFactory) {
$response = $responseFactory->createResponse();
$response->getBody()->write('Response set by a callable route handler');
return $response;
},
'handler_method' => null,
'methods' => ['GET'],
]
];
The full list of route configuration parameters can be found in the Ā“duktig-coreĀ“'s Config/routes.php
file., (*69)
, (*70)
Tests
This package achieves a high code coverage percentage using the PHPUnit and Mockery. To run its tests at the command line, install the package as a new project with full dev requirements, and within the project directory run the command:, (*71)
$ vendor/bin/phpunit -c phpunit.xml.dist
This will also generate a coverage report in the coverage
directory within the project directory., (*72)
These tests cover the project's functional elements, while all the othe packages that are used within it are fully unit tested separately., (*73)