PPI Framework¶
PPI is a framework delivery engine. Using the concept of microservices, it lets you choose which parts of frameworks you wish to use on a per-feature basis. As such each feature makes its own independent decisions, allowing you to pick the best tools from the best PHP frameworks
PPI bootstraps framework components for you from the top frameworks such as ZendFramework2, Symfony2, Laravel4, Doctrine2
In 7 short chapters (or less!) learn how to use PPI2 for your web projects.
Installation¶
Download from composer¶
Download the latest version of 2.1, in the current directory
composer create-project -sdev --no-interaction ppi/skeleton-app /var/www/skeleton "^2.1"
Downloading from the website¶
Automatic Vagrant Installation¶
The recommended install procedure is to use the pre-built vagrant image that ships with the skeleton app in the ansible
directory.
Installing vagrant and ansible¶
Before you can run vagrant you’ll need to install a few system dependencies.
Install vagrant https://docs.vagrantup.com/v2/installation/
Install ansible: http://docs.ansible.com/ansible/intro_installation.html#latest-releases-via-pip
Accessing the application¶
If you wish to use the skeletonapp as a hostname, run this command and browse to http://skeletonapp.ppi
sudo sh -c 'echo "192.168.33.99 skeletonapp.ppi" >> /etc/hosts'
Otherwise you can browse straight to the ip address of: http://192.168.33.99
Manual Web Server Configuration¶
Security is crucial to consider. As a result all your app code and configuration is kept hidden away outside of /public/
and is inaccessible via the browser. Therefore we need to create a virtual host in order to route all web requests
to the /public/
folder and from there your public assets (css/js/images) are loaded normally. The .htaccess
or web server’s rewrite rules kick in which route all non-asset files to /public/index.php
.
Apache Configuration¶
We are now creating an Apache virtual host for the application to make http://skeletonapp.ppi serve
index.php
from the skeletonapp/public
directory.
<VirtualHost *:80>
ServerName skeletonapp.ppi
DocumentRoot "/var/www/skeleton/public"
SetEnv PPI_ENV dev
SetEnv PPI_DEBUG true
<Directory "/var/www/skeleton/public">
AllowOverride All
Allow from all
DirectoryIndex index.php
Options Indexes FollowSymLinks
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [L]
</Directory>
</VirtualHost>
Nginx Virtual Host¶
server {
listen 80;
server_name skeletonapp.ppi;
root /var/www/skeleton/public;
index index.php;
location / {
try_files $uri /index.php$is_args$args;
}
location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param HTTPS off;
}
}
Restart your web server. The skeletonapp website can now be accessed using http://skeletonapp.ppi
Requirements¶
To easily check if your system passes all requirements, PPI provides two ways and we recommend you do both.
Why do we have both scripts? Because your CLI environment can have a separate php.ini file from your web environment so this will ensure you’re good to go from both sides.
Requirements checking on the command-line¶
$ ./app/check
_
_____ _____ |_|
/ __ | / __ | / |
| |__| || |__| || |
| ___/ | ___/ | |
| | | | |_|
|/ |/
Framework Version 2
-- Requirements Check --
* Configuration file used by PHP: /etc/php/cli-php5.4/php.ini
* Mandatory requirements **
OK PHP version must be at least 5.3.3 (5.4.13--pl0-gentoo installed)
OK PHP version must not be 5.3.16 as PPI won't work properly with it
OK Vendor libraries must be installed
OK app/cache/ directory must be writable
OK app/logs/ directory must be writable
OK date.timezone setting must be set
OK Configured default timezone "Europe/Lisbon" must be supported by your installation of PHP
OK json_encode() must be available
OK session_start() must be available
OK ctype_alpha() must be available
OK token_get_all() must be available
OK simplexml_import_dom() must be available
OK detect_unicode must be disabled in php.ini
OK xdebug.show_exception_trace must be disabled in php.ini
OK xdebug.scream must be disabled in php.ini
OK PCRE extension must be available
Watch out for the green OK
markers. If they all light up, congratulations, you’re good to go!
Below is the list of required and optional requirements.
Requirements checking in the browser¶
The check.php script is accessible in your browser at: http://skeletonapp.ppi/check.php
Must have requirements¶
- PHP needs to be a minimum version of PHP 5.3.3
- JSON needs to be enabled
- ctype needs to be enabled
- Your PHP.ini needs to have the date.timezone setting
Optional requirements¶
- You need to have the PHP-XML module installed
- You need to have at least version 2.6.21 of libxml
- PHP tokenizer needs to be enabled
- mbstring functions need to be enabled
- iconv needs to be enabled
- POSIX needs to be enabled (only on *nix)
- Intl needs to be installed with ICU 4+
- APC 3.0.17+ (or another opcode cache needs to be installed)
- PHP.ini recommended settings
short_open_tag = On
magic_quotes_gpc = Off
register_globals = Off
session.autostart = Off
Skeleton Application¶
The skeleton application is an app
for you to get up and running as quickly as possible. Inside you’ll find the PHP libraries (vendor
dir), a selection of useful modules, our recommended directory structure and some default configuration.
First, lets review the file structure of the PPI skeleton application:
www/ # your web root directory
|
└── skeleton/ # the unpacked archive
|
├── app/
│ ├── console # CLI script to help debug the application
│ ├── init.php
│ ├── config/ # application configuration files
│ │ ├── base/ # base configuration to be extended by other environments
│ │ │ ├── app.yml
│ │ ├── dev/ # configuration for the development environment (``dev``)
│ │ │ └── app.yml
│ │ ├── prod/ # configuration for the production environment (``prod``)
│ │ │ └── app.yml
│ ├── cache/ # application cache (must be writable by the web server)
│ ├── logs/ # application logs (must be writable by the web server)
│ └── views/ # global template (view) files
│ └── base.html.php
│
├── modules/ # application modules
│ ├── Application/
│ ├── Framework/
│ └── UserModule/
│
├── public/
│ ├── index.php # front controller
│ ├── css/
│ ├── images/
│ └── js/
│
└── vendor/ # libraries installed by Composer
Lets break it down into parts:
The public folder¶
The structure above shows you the /public/
folder. Anything outside of /public/
i.e: all your business code will be secure from direct URL access. In your development environment you don’t need a virtualhost file, you can directly access your application like so: http://localhost/skeleton/public/. In your production environment this will be http://www.mysite.com/. All your publicly available asset files should be here, CSS, JS, Images.
The public index.php file¶
The /public/index.php
is also known as your bootstrap file, or front controller and is presented below:
<?php
// All relative paths start from the main directory, not from /public/
chdir(dirname(__DIR__));
// Setup autoloading and include PPI
require_once 'app/init.php';
// Set the environment
$env = getenv('PPI_ENV') ?: 'dev';
$debug = getenv('PPI_DEBUG') !== '0' && $env !== 'prod';
// Create our PPI App instance
$app = new PPI\App(array(
'environment' => $env,
'debug' => $debug
));
// Configure the application
$app->loadConfig($app->getEnvironment().'/app.php');
// Load the application, match the URL and send an HTTP response
$app->boot()->dispatch()->send();
Environments¶
PPI supports the notion of “environments” to make the application behave differently from when you are coding and testing the application in your laptop to when you deploy it to a production server. While in production debug messages won’t be logged, your application won’t stop due to non-fatal PHP errors and we’ll use caching wherever possible. In development you’ll get everything!
Auto-set the environment using web server variables¶
Editing index.php
whenever you want to test the application in another environment can be tedious.
An alternative is to set environment variables in your web server on a per vhost basis.
If you’re using Apache, environment variables can be set using the SetEnv directive.
Production VirtualHost configuration:
<VirtualHost *:80>
ServerName prod.skeletonapp.ppi.localhost
DocumentRoot "/var/www/skeleton/public"
SetEnv PPI_ENV prod
SetEnv PPI_DEBUG false
...
And a development VirtualHost configuration:
<VirtualHost *:80>
ServerName dev.skeletonapp.ppi.localhost
DocumentRoot "/var/www/skeleton/public"
SetEnv PPI_ENV dev
SetEnv PPI_DEBUG true
...
The front controller (index.php
) needs to be slightly edited to load these environment variables:
// file: public/index.php
// Set the environment
$env = getenv('PPI_ENV') ?: 'dev';
$debug = getenv('PPI_DEBUG') !== '0' && $env !== 'prod';
// Create our PPI App instance
$app = new PPI\App(array(
'environment' => $env,
'debug' => $debug
));
After this change http://prod.skeletonapp.ppi.localhost/
will use production settings while
http://dev.skeletonapp.ppi.localhost/
is configured to work in development mode.
Creating a new environment¶
You don’t need to be restricted to the dev
and prod
environments. To create a new environment with a special
configuration, let’s call it staging
, just copy the folder contents of an existing environment to the new one
and edit the app.yml
file inside the staging
dir.
$ cd /path/to/skeletonapp/app/config
$ cp -r prod staging
$ vim staging/app.yml
Now make sure public/index.php
is picking up your new environment:
<?php
// ...
// Staging
$app = new PPI\App(array(
'environment' => 'staging',
'debug' => true
));
$app->loadConfig($app->getEnvironment().'/app.yml');
// ...
Note
PPI creates cache and log files associated with each environment. For this new staging
environment cache files
will be available under app/cache/staging/
and the log file is available at app/logs/staging.log
.
The app folder¶
This is where all your apps global items go such as app config, datasource config and modules config and global templates (views). You wont need to touch these out-of-the-box but it allows for greater flexibility in the future if you need it.
The app/config folder¶
Starting with version 2.1 all the application configuration lives inside app/config/<env>/
folders. Each <env>
folder holds configuration for a specific environment: dev
, prod
.
Supported configuration formats¶
PPI supports both PHP and YAML formats. PHP is more powerful whereas YAML is more clean and readable. It is up to you to pick the format of your liking.
Note
In 2.1 we changed the default configuration file format from PHP to YAML because (we think) it is less verbose and faster to type but don’t worry because PHP configuration files are and will always be supported.
YAML imports/include¶
The YAML language doesn’t natively provide the capability to include other YAML files like a PHP include
or require
statement.
To overcome this limitation PPI supports two special syntaxes: imports
and @include
.
Note
One of the goals of the PPI Framework is to provide an environment familiar to users coming from or going to the Symfony and Zend frameworks (among others). We support these two variants so these users do not need to worry about learning new syntaxes.
imports:
Available in the Symfony framework. Works like a PHP include statement providing base configuration to be tweaked in the current file. It is usually added at the top of the file.
imports: - { resource: ../base/app.yml }
@include:
Available in the Zend framework. Similar to the imports
syntax but can be used also in a subelement of a value.
framework: @include: ../base/datasource.yml
The app.yml file¶
Looking at the example config file below, you can control things here such as the enabled templating engines, the datasource connection and the logger (monolog
).
- YAML
imports: - { resource: datasource.yml } - { resource: modules.yml } framework: templating: engines: ["php", "smarty", "twig"] skeleton_module: path: "./utils/skeleton_module" monolog: handlers: main: type: stream path: %app.logs_dir%/%app.environment%.log level: debug
- PHP
<?php $config = array(); $config['framework'] = array( 'templating' => array( 'engines' => array('php', 'smarty', 'twig'), ), 'skeleton_module' => array( 'path' => './utils/skeleton_module' ) ); $config['datasource'] => array( 'connections' = require __DIR__ . '/datasource.php' ); $config['modules'] = require __DIR__ . 'modules.php'; return $config;
Tip
The configuration shown above is not exhaustive. For a complete listing of the available configuration options please check the sections in the Configuration Reference chapter.
The datasource.yml file¶
The datasource.yml
is where you setup your database connection information.
Warning
Because this file may hold sensitive information consider not adding it to your source control system.
- YAML
datasource: connections: main: type: 'pdo_mysql' host: 'localhost' dbname: 'ppi2_skeleton' user: 'root' pass: 'secret'
- PHP
<?php return array( 'main' => array( 'type' => 'pdo_mysql', // This can be any pdo driver. i.e: pdo_sqlite 'host' => 'localhost', 'dbname' => 'ppi2_skeleton', 'user' => 'root', 'pass' => 'secret' ) );
The modules.yml file¶
The example below shows that you can control which modules are active and a list of directories module_paths that PPI will scan for your modules. Pay close attention to the order in which your modules are loaded. If one of your modules relies on resources loaded by another module. Make sure the module loading the resources is loaded before the others that depend upon it.
- YAML
modules: active_modules: - Framework - Application - UserModule module_listener_options: module_paths: ['./modules', './vendor']
- PHP
<?php return array( 'active_modules' => array( 'Framework', 'Application', 'UserModule', ), 'module_listener_options' => array( 'module_paths' => array('./modules', './vendor') ), );
The app/views folder¶
This folder is your applications global views folder. A global view is one that a multitude of other module views extend from. A good example of this is your website’s template file. The following is an example of /app/views/base.html.php
:
<html>
<body>
<h1>My website</h1>
<div class="content">
<?php $view['slots']->output('_content'); ?>
</div>
</body>
</html>
You’ll notice later on in the Templating section to reference and extend a global template file, you will use the following syntax in your modules template.
<?php $view->extend('::base.html.php'); ?>
Now everything from your module template will be applied into your base.html.php files _content section demonstrated above.
The modules folder¶
This is where we get stuck into the real details, we’re going into the /modules/
folder. Click the next section to proceed.
Modules¶
By default, one module is provided with the SkeletonApp, named Application. It provides a simple route pointing to the homepage. A simple controller to handle the “home” page of the application. This demonstrates using routes, controllers and views within your module.
Module Structure¶
Your module starts with Module.php. You can have configuration on your module. Your can have routes which result in controllers getting dispatched. Your controllers can render view templates.
├── Module.php
├── resources
│ ├── config
│ │ └── config.yml
│ ├── routes
│ │ ├── laravel.php
│ │ └── symfony.yml
│ └── views
│ └── index
│ └── index.html.php
├── src
│ ├── Controller
│ │ ├── Index.php
│ │ └── Shared.php
The Module.php class¶
Every PPI module looks for a Module.php
class file, this is the starting point for your module.
<?php
namespace Application;
use PPI\Framework\Module\AbstractModule;
class Module extends AbstractModule
{
}
Autoloading¶
Registering your namespace can be done using the Zend Framework approach below. You can also skip this and register your module’s namespace to your composer.json
file
<?php
namespace Application;
use PPI\Framework\Module\AbstractModule;
class Module extends AbstractModule
{
public function getAutoloaderConfig()
{
return array(
'Zend\Loader\StandardAutoloader' => array(
'namespaces' => array(
__NAMESPACE__ => __DIR__ . '/src/',
),
),
);
}
}
Init¶
The above code shows you the Module class, and the all important init()
method. Why is it important? If you remember from The Skeleton Application section previously, we have defined in our modules.config.php
config file an activeModules option, when PPI is booting up the modules defined activeModules it looks for each module’s init() method and calls it.
The init()
method is run for every page request, and should not perform anything heavy. It is considered bad practice to utilize these methods for setting up or configuring instances of application resources such as a database connection, application logger, or mailer.
<?php
namespace Application;
use PPI\Framework\Module\AbstractModule;
class Module extends AbstractModule
{
public function init()
{
}
}
Configuration¶
Expanding on from the previous code example, we’re now adding a getConfig()
method. This must return a raw PHP array. You may require/include
a PHP file directly or use the loadConfig()
helper that works for both PHP and YAML files. When using loadConfig()
you don’t need to tell the full path, just the filename.
All the modules with getConfig() defined on them will be merged together to create ‘modules config’ and this is merged with your global app’s configuration file at /app/app.config.php
. Now from any controller you can get access to this config by doing $this->getConfig()
. More examples on this later in the Controllers section.
<?php
class Module extends AbstractModule
{
/**
* Returns configuration to merge with application configuration.
*
* @return array
*/
public function getConfig()
{
return $this->loadConfig(__DIR__ . '/resources/config/config.yml');
}
}
Tip
To help you troubleshoot the configuration loaded by the framework you may use the app/console config:dump
command
Conclusion¶
Lets move onto Services and Routing for our modules on the next pages.
Services¶
Each of your features (modules) wants to be self-contained, isolated and in control of its own destiny. To keep such separation is a good thing (Separation of Responsibility principal). Once you’ve got that nailed then you want to begin exposing information out of your module. A popular architectural pattern is Service Oriented Architecture (SOA).
Services in PPI have names that you define. This can be something simple like user.search
or cache.driver
or it’s even somewhat popular to use the Fully Qualified Class Name (FQCN) as the name of the service like this: MyService::class
. With that in mind it’s just a string and it’s up to you what convention you use just make sure it’s consistent.
Defining the Service in our Module¶
This is of course optional for your module but if you want to begin doing services then the method is getServiceConfig
.
This will be called on all modules upon boot()
of PPI. Boot should be almost instantaneous and non-blocking, so be sure not to do anything expensive here such as make network connections, as that’ll slow down your boot process.
<?php
namespace MyModule;
use PPI\Framework\Module\AbstractModule;
use MyModule\Factory\UserSearchFactory;
use MyModule\Factory\UserCreateFactory;
use MyModule\Factory\UserImportService;
class Module extends AbstractModule
{
public function getServiceConfig()
{
return ['factories' => [
'user.search' => UserSearchFactory::class,
'user.create' => UserCreateFactory::class,
'user.import' => function ($sm) {
return new UserImportService($sm->get('Doctrine\ORM\EntityManager'));
}
]];
}
}
Above you’ll see two types of ways to create a service. One is a Factory class and one is an inline factory closure. It’s recommended to use a Factory class but each to their own.
Creating a Service Factory¶
<?php
namespace MyModule\Factory;
use Zend\ServiceManager\ServiceLocatorInterface;
use Zend\ServiceManager\FactoryInterface;
use MyModule\Service\UserSearchService;
class UserSearchFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $sm)
{
$config = $sm->get('config');
if(!isset($config['usersearch']['search_key']) {
throw new \RuntimeException('Missing user search configuration');
}
return new UserSearchService(
$config['usersearch']['search_key'],
$sm->get('Doctrine\ORM\EntityManager')
);
}
}
Using services in our Controllers¶
To use the services in our Controllers, we just need to call $this->getService('service.name')
<?php
public function searchUsersAction(Request $request, $lat, $long)
{
$userSearchService = $this->getService('user.search');
$users = $userSearchService->getUsersFromLatLong($lat, $long);
return $this->render('MyModule:search:searchUsers.twig', compact('users'));
}
Routing¶
Routes are the rules that tell the framework what URLs map to what actions of your application.
When PPI is booting up it will take call `getRoutes()`
on each module and register its entry within the main `ChainRouter`
, which is a router stack.
PPI will call `match`
on each router in the order that your modules have been defined in your application config.
PPI provides bindings for popular PHP routers which you will see examples of below. Review the documentation for each router to learn more about using them.
Using Symfony Router¶
<?php
// Module.php
class Module extends AbstractModule
{
// ....
public function getRoutes()
{
return $this->loadSymfonyRoutes(__DIR__ . '/routes/symfonyroutes.yml');
}
}
# resources/config/symfonyroutes.yml
BlogModule_Index:
pattern: /blog
defaults: { _controller: "BlogModule:Blog:index"}
Using Aura Router¶
<?php
// Module.php
class Module extends AbstractModule
{
// ....
public function getRoutes()
{
return $this->loadAuraRoutes(__DIR__ . '/resources/config/auraroutes.php');
}
}
<?php
// resources/config/auraroutes.php
$router
->add('BlogModule_Index', '/blog')
->addValues(array(
'controller' => 'BlogModule\Controller\Index',
'action' => 'indexAction'
));
// add a named route using symfony controller name syntax
$router->add('BlogModule_View', '/blog/view/{id}')
->addTokens(array(
'id' => '\d+'
))
->addValues(array(
'controller' => 'BlogModule:Index:view'
));
return $router;
Using FastRoute Router¶
<?php
// Module.php
class Module extends AbstractModule
{
public function getRoutes()
{
return $this->loadFastRouteRoutes(__DIR__ . '/resources/routes/fastroutes.php');
}
}
<?php
// resources/config/fastroutes.php
/**
* @var \FastRoute\RouteCollector $r
*/
$r->addRoute('GET', '/blog', 'BlogModule\Controller\Index');
Controllers¶
So what is a controller? A controller is just a PHP class, like any other that you’ve created before, but the intention of it, is to have a bunch of methods on it called actions. The idea is: each route in your system will execute an action method. Examples of action methods would be your homepage or blog post page. The job of a controller is to perform a bunch of code and respond with some HTTP content to be sent back to the browser. The response could be a HTML page, a JSON array, XML document or to redirect somewhere. Controllers in PPI are ideal for making anything from web services, to web applications, to just simple html-driven websites.
Lets quote something we said in the last chapter’s introduction section
Defaults¶
This is the important part, The syntax is Module:Controller:action
. So if you specify Application:Blog:show then this will execute the following class path: /modules/Application/Controller/Blog->showAction()
. Notice how the method has a suffix of Action, this is so you can have lots of methods on your controller but only the ones ending in Action()
will be executable from a route.
Example controller¶
Review the following route that we’ll be matching.
Blog_Show:
pattern: /blog/{blogId}
defaults: { _controller: "Application:Blog:show"}
So lets presume the route is /blog/show/{blogId}
, and look at what your controller would look like. Here is an example blog controller, based on some of the routes provided above.
<?php
namespace Application\Controller;
use Application\Controller\Shared as BaseController;
class Blog extends BaseController
{
public function showAction(Request $request, $blogId)
{
$bs = $this->getBlogStorage();
if(!$bs->existsByID($blogId)) {
$this->setFlash('error', 'Invalid Blog ID');
return $this->redirectToRoute('Blog_Index');
}
// Get the blog post for this ID
$blogPost = $bs->getByID($blogId);
// Render our main blog page, passing in our $blogPost article to be rendered
$this->render('Application:blog:show.html.php', compact('blogPost'));
}
}
Generating urls using routes¶
Here we are still executing the same route, but making up some urls using route names
<?php
namespace Application\Controller;
use Application\Controller\Shared as BaseController;
class Blog extends BaseController
{
public function showAction(Request $request, $blogId)
{
// pattern: /about
$aboutUrl = $this->generateUrl('About_Page');
// pattern: /blog/show/{blogId}
$blogPostUrl = $this->generateUrl('Blog_Post', array('id' => $blogId);
}
}
Redirecting to routes¶
An extremely handy way to send your users around your application is redirect them to a specific route.
<?php
namespace Application\Controller;
use Application\Controller\Shared as BaseController;
class Blog extends BaseController
{
public function showAction(Request $request, $blogId)
{
// Send user to /login, if they are not logged in
if(!$this->isLoggedIn()) {
return $this->redirectToRoute('User_Login');
}
// go to /user/profile/{username}
return $this->redirectToRoute('User_Profile', array('username' => 'ppi_user'));
}
}
Working with POST
values¶
<?php
namespace Application\Controller;
use Application\Controller\Shared as BaseController;
class Blog extends BaseController
{
public function postAction()
{
$this->getPost()->set('myKey', 'myValue');
var_dump($this->getPost()->get('myKey')); // string('myValue')
var_dump($this->getPost()->has('myKey')); // bool(true)
var_dump($this->getPost()->remove('myKey'));
var_dump($this->getPost()->has('myKey')); // bool(false)
// To get all the post values
$postValues = $this->post();
}
}
Working with QueryString parameters¶
<?php
namespace Application\Controller;
use Application\Controller\Shared as BaseController;
class Blog extends BaseController
{
// The URL is /blog/?action=show&id=453221
public function queryStringAction()
{
var_dump($this->getQueryString()->get('action')); // string('show')
var_dump($this->getQueryString()->has('id')); // bool(true)
// Get all the query string values
$allValues = $this->queryString();
}
}
Working with server variables¶
<?php
namespace Application\Controller;
use Application\Controller\Shared as BaseController;
class Blog extends BaseController
{
public function serverAction()
{
$this->getServer()->set('myKey', 'myValue');
var_dump($this->getServer()->get('myKey')); // string('myValue')
var_dump($this->getServer()->has('myKey')); // bool(true)
var_dump($this->getServer()->remove('myKey'));
var_dump($this->getServer()->has('myKey')); // bool(false)
// Get all server values
$allServerValues = $this->server();
}
}
Working with cookies¶
<?php
namespace Application\Controller;
use Application\Controller\Shared as BaseController;
class Blog extends BaseController
{
public function cookieAction()
{
$this->getCookie()->set('myKey', 'myValue');
var_dump($this->getCookie()->get('myKey')); // string('myValue')
var_dump($this->getCookie()->has('myKey')); // bool(true)
var_dump($this->getCookie()->remove('myKey'));
var_dump($this->getCookie()->has('myKey')); // bool(false)
// Get all the cookies
$cookies = $this->cookies();
}
}
Working with session values¶
<?php
namespace Application\Controller;
use Application\Controller\Shared as BaseController;
class Blog extends BaseController
{
public function sessionAction()
{
$this->getSession()->set('myKey', 'myValue');
var_dump($this->getSession()->get('myKey')); // string('myValue')
var_dump($this->getSession()->has('myKey')); // bool(true)
var_dump($this->getSession()->remove('myKey'));
var_dump($this->getSession()->has('myKey')); // bool(false)
// Get all the session values
$allSessionValues = $this->session();
}
}
Working with the config¶
Using the getConfig()
method we can obtain the config array. This config array is the result of ALL the configs returned from all the modules, merged with your application’s global config.
<?php
namespace Application\Controller;
use Application\Controller\Shared as BaseController;
class Blog extends BaseController
{
public function configAction()
{
$config = $this->getConfig();
switch($config['mailer']) {
case 'swift':
break;
case 'sendgrid':
break;
case 'mailchimp':
break;
}
}
}
Working with the is() method¶
The is()
method is a very expressive way of coding and has a variety of options you can send to it. The method always returns a boolean as you are saying “is this the case?”
<?php
namespace Application\Controller;
use Application\Controller\Shared as BaseController;
class Blog extends BaseController
{
public function isAction()
{
if($this->is('ajax')) {}
if($this->is('post') {}
if($this->is('patch') {}
if($this->is('put') {}
if($this->is('delete') {}
// ssl, https, secure: are all the same thing
if($this->is('ssl') {}
if($this->is('https') {}
if($this->is('secure') {}
}
}
Getting the users IP or UserAgent¶
Getting the user’s IP address or user agent is very trivial.
<?php
namespace Application\Controller;
use Application\Controller\Shared as BaseController;
class Blog extends BaseController
{
public function userAction()
{
$userIP = $this->getIP();
$userAgent = $this->getUserAgent();
}
}
Working with flash messages¶
A flash message is a notification that the user will see on the next page that is rendered. It’s basically a setting stored in the session so when the user hits the next designated page it will display the message, and then disappear from the session. Flash messages in PPI have different types. These types can be 'error'
, 'warning'
, 'success'
, this will determine the color or styling applied to it. For a success message you’ll see a positive green message and for an error you’ll see a negative red message.
Review the following action, it is used to delete a blog item and you’ll see a different flash message depending on the scenario.
<?php
namespace Application\Controller;
use Application\Controller\Shared as BaseController;
class Blog extends BaseController
{
public function deleteAction()
{
$blogID = $this->getPost()->get('blogID');
if(empty($blogID)) {
$this->setFlash('error', 'Invalid BlogID Specified');
return $this->redirectToRoute('Blog_Index');
}
$bs = $this->getBlogStorage();
if(!$bs->existsByID($blogID)) {
$this->setFlash('error', 'This blog ID does not exist');
return $this->redirectToRoute('Blog_Index');
}
$bs->deleteByID($blogID);
$this->setFlash('success', 'Your blog post has been deleted');
return $this->redirectToRoute('Blog_Index');
}
}
Getting the current environment¶
You may want to perform different scenarios based on the site’s environment. This is a configuration value defined in your global application config. The getEnv()
method is how it’s obtained.
<?php
namespace Application\Controller;
use Application\Controller\Shared as BaseController;
class Blog extends BaseController
{
public function envAction()
{
$env = $this->getEnv();
switch($env) {
case 'development':
break;
case 'staging':
break;
case 'production':
default:
break;
}
}
}
Templating¶
As discovered in the previous chapter, a controller’s job is to process each HTTP request that hits your web application. Once your controller has finished its processing it usually wants to generate some output content. To achieve this it hands over responsibility to the templating engine. The templating engine will load up the template file you tell it to, and then generate the output you want, his can be in the form of a redirect, HTML webpage output, XML, CSV, JSON; you get the picture!
In this chapter you’ll learn:
- How to create a base template
- How to load templates from your controller
- How to pass data into templates
- How to extend a parent template
- How to use template helpers
Base Templates¶
What are base templates?
Why do we need base templates? well you don’t want to have to repeat HTML over and over again and perform repetitive steps for every different type of page you have. There’s usually some commonalities between the templates and this commonality is your base template. The part that’s usually different is the content page of your webpage, such as a users profile or a blog post.
So lets see an example of what we call a base template, or somethings referred to as a master template. This is all the HTML structure of your webpage including headers and footers, and the part that’ll change will be everything inside the page-content section.
Where are they stored?
Base templates are stored in the ./app/views/
directory. You can have as many base templates as you like in there.
This file is ./app/views/base.html.php
Example base template:
<!DOCTYPE html>
<html>
<head>
<title>Welcome to Symfony!</title>
</head>
<body>
<div id="header">...</div>
<div id="page-content">
<?php $view['slots']->output('_content'); ?>
</div>
<div id="footer">...</div>
</body>
</html>
Lets recap a little, you see that slots helper outputting something called _content? Well this is actually injecting the resulting output of the CHILD template belonging to this base template. Yes that means we have child templates that extend parent/base templates. This is where things get interesting! Keep on reading.
Extending Base Templates¶
On our first line we extend the base template we want. You can extend literally any template you like by specifying its Module:folder:file.format.engine
naming syntax. If you miss out the Module and folder sections, such as ::base.html.php
then it’s going to take the global route of ./app/views/
.
<?php $view->extend('::base.html.php'); ?>
<div class="user-registration-page">
<h1>Register for our site</h1>
<form>...</form>
</div>
The resulting output¶
If you remember that the extend call is really just populating a slots section named _content then the injected content into the parent template looks like this.
<!DOCTYPE html>
<html>
<head>
<title>Welcome to Symfony!</title>
</head>
<body>
<div id="header">...</div>
<div id="page-content">
<div class="user-registration-page">
<h1>Register for our site</h1>
<form>...</form>
</div>
</div>
<div id="footer">...</div>
</body>
</html>
Example scenario¶
Consider the following scenario. We have the route Blog_Show
which executes the action Application:Blog:show
. We then load up a template named Application:blog:show.html.php
which is designed to show the user their blog post.
The route¶
Blog_Show:
pattern: /blog/{id}
defaults: { _controller: "Application:Blog:show"}
The controller¶
<?php
namespace Application\Controller;
use Application\Controller\Shared as BaseController;
class Blog extends BaseController {
public function showAction() {
$blogID = $this->getRouteParam('id');
$bs = $this->getBlogStorage();
if(!$bs->existsByID($blogID)) {
$this->setFlash('error', 'Invalid Blog ID');
return $this->redirectToRoute('Blog_Index');
}
// Get the blog post for this ID
$blogPost = $bs->getByID($blogID);
// Render our blog post page, passing in our $blogPost article to be rendered
$this->render('Application:blog:show.html.php', compact('blogPost'));
}
}
The template¶
So the name of the template loaded is Application:blog:show.html.php then this is going to translate to ./modules/Application/blog/show.html.php
. We also passed in a $blogPost
variable which can be used locally within the template that you’ll see below.
<?php $view->extend('::base.html.php'); ?>
<div class="blog-post-page">
<h1><?=$blogPost->getTitle();?></h1>
<p class="created-by"><?=$blogPost->getCreatedBy();?></p>
<p class="content"><?=$blogPost->getContent();?></p>
</div>
Using the slots helper¶
We have a bunch of template helpers available to you, the helpers are stored in the $view variable, such as $view['slots']
or $view['assets']
. So what is the purpose of using slots? Well they’re really for segmenting the templates up into named sections and this allows the child templates to specify content that the parent is going to inject for them.
Review this example it shows a few examples of using the slots helper for various different reasons.
The base template¶
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title><?php $view['slots']->output('title', 'PPI Skeleton Application') ?></title>
</head>
<body>
<div id="page-content">
<?php $view['slots']->output('_content') ?>
</div>
</body>
</html>
The child template¶
<?php $view->extend('::base.html.php'); ?>
<div class="blog-post-page">
<h1><?=$blogPost->getTitle();?></h1>
<p class="created-by"><?=$blogPost->getCreatedBy();?></p>
<p class="content"><?=$blogPost->getContent();?></p>
</div>
<?php $view['slots']->start('title'); ?>
Welcome to the blog page
<?php $view['slots']->stop(); ?>
What’s going on?
The slots key we specified first was title and we gave the output method a second parameter, this means when the child template does not specify a slot section named title then it will default to “PPI Skeleton Application”.
Using the assets helper¶
So why do we need an assets helper? Well one main purpose for it is to include asset files from your project’s ./public/
folder such as images, css files, javascript files. This is useful because we’re never hard-coding any baseurl’s anywhere so it will work on any environment you host it on.
Review this example it shows a few examples of using the slots helper for various different reasons such as including CSS and JS files.
<?php $view->extend('::base.html.php'); ?>
<div class="blog-post-page">
<h1><?=$blogPost->getTitle();?></h1>
<img src="<?=$view['assets']->getUrl('images/blog.png');?>" alt="The Blog Image">
<p class="created-by"><?=$blogPost->getCreatedBy();?></p>
<p class="content"><?=$blogPost->getContent();?></p>
<?php $view['slots']->start('include_js'); ?>
<script type="text/javascript" src="<?=$view['assets']->getUrl('js/blog.js');?>"></script>
<?php $view['slots']->stop(); ?>
<?php $view['slots']->start('include_css'); ?>
<link href="<?=$view['assets']->getUrl('css/blog.css');?>" rel="stylesheet">
<?php $view['slots']->stop(); ?>
</div>
What’s going on?
By asking for images/blog.png
we’re basically asking for www.mysite.com/images/blog.png
, pretty straight forward right? Our include_css
and include_js
slots blocks are custom HTML that’s loading up CSS/JS files just for this particular page load. This is great because you can split your application up onto smaller CSS/JS files and only load the required assets for your particular page, rather than having to bundle all your CSS into the one file.
Using the router helper¶
What is a router helper? The router help is a nice PHP class with routing related methods on it that you can use while you’re building PHP templates for your application.
What’s it useful for? The most common use for this is to perform a technique commonly known as reverse routing. Basically this is the process of taking a route key and turning that into a URL, rather than the standard process of having a URL and that translate into a route to become dispatched.
Why is reverse routing needed? Lets take the Blog_Show route we made earlier in the routing section. The syntax of that URI would be like: /blog/show/{title}
, so rather than having numerous HTML links all manually referring to /blog/show/my-title
we always refer to its route key instead, that way if we ever want to change the URI to something like /blog/post/{title}
the templating layer of your application won’t care because that change has been centrally maintained in your module’s routes file.
Here are some examples of reverse routing using the routes helper
<a href="<?=$view['router']->generate('About_Page');?>">About Page</a>
<p>User List</p>
<ul>
<?php foreach($users as $user): ?>
<li><a href="<?=$view['router']->generate('User_Profile', array('id' => $user->getID())); ?>"><?=$view->escape($user->getName());?></a></li>
<?php endforeach; ?>
</ul>
The output would be something like this
<a href="/about">About Page</a>
<p>User List</p>
<ul>
<li><a href="/user/profile?id=23">PPI User</a></li>
<li><a href="/user/profile?id=87675">Another PPI User</a></li>
</ul>
Databases¶
When you’re developing an app, it is 100% sure you’ll need to persist and read information to and from a database. Fortunately, PPI makes it simple to work with databases with our powerful DataSources component, which makes use of Doctrine DBAL layer, a library whose sole goal is to give you robust tools to make this easy. In this chapter, you’ll learn the basic philosophy behind doctrine and see how easy is to use the DataSource component to work with databases.
Note
We suggest to use our DataSource component which is a wrapper around the Doctrine DBAL component. This provides you with a simple yet very powerful database layer to talk to any PDO supported database engine. If you prefer to work with another database component then you can simply create that as a service and inject that into your storage classes instead of the ‘datasource’ component.
A Simple Example: A User¶
The easiest way to understand how the DataSource component works is to see it in action. In this section, you’ll configure your database, create a Product storage class, persist it to the database and fetch it back out.
Configuring the Database¶
Before you begin, you’ll need to configure your database connection information. By convention, this information is usually configured in the app/datasource.config.php file:
<?php
$connections = array();
$connections['main'] = array(
'type' => 'pdo_mysql', // This can be any pdo driver. i.e: pdo_sqlite
'host' => 'localhost',
'dbname' => 'database',
'user' => 'database_user',
'pass' => 'database_password'
);
return $connections; // Very important you must return the connections variable from this script
Note
You can have multiple connections within your app. That means you may need to have multiple db engines, like MySQL, PGSQL, MSSQL, or any other PDO driver.
Creating the Storage Class¶
After configuring the database connection information, we need to have a storage class, which is the one that’s going to be talking to the DataSource component when there’s a need to persist information.
<?php
namespace UserModule\Storage;
use UserModule\Storage\Base as BaseStorage;
use UserModule\Entity\User as UserEntity;
// Note here, we extend from
// a BaseStorage class
class User extends BaseStorage
{
protected $_meta = array(
'conn' => 'main', // the connection.
'table' => 'user',
'primary' => 'id',
'fetchMode' => \PDO::FETCH_ASSOC
);
/**
* Create a user record
*
* @param array $userData
* @return mixed
*/
public function create(array $userData)
{
return $this->insert($userData);
}
/**
* Get a user entity by its ID
*
* @param $userID
* @return mixed
* @throws \Exception
*/
public function getByID($userID)
{
$row = $this->find($userID);
if ($row === false) {
throw new \Exception('Unable to obtain user row for id: ' . $userID);
}
return new UserEntity($row);
}
/**
* Delete a user by their ID
*
* @param integer $userID
* @return mixed
*/
public function deleteByID($userID)
{
return $this->delete(array($this->getPrimaryKey() => $userID));
}
/**
* Count all the records
*
* @return mixed
*/
public function countAll()
{
$row = $this->_conn->createQueryBuilder()
->select('count(id) as total')
->from($this->getTableName(), 'u')
->execute()
->fetch($this->getFetchMode());
return $row['total'];
}
/**
* Get entity objects from all users rows
*
* @return array
*/
public function getAll()
{
$entities = array();
$rows = $this->fetchAll();
foreach ($rows as $row) {
$entities[] = new UserEntity($row);
}
return $entities;
}
}
First of all, we can see the class extends a BaseController class, which is a Shared Storage class, where we can place reusable code for all of our storage classes.
<?php
namespace UserModule\Storage;
use PPI\DataSource\ActiveQuery;
class Base extends ActiveQuery
{
public function sharedFunction()
{
// code here...
}
}
As you can see, the storage class is pretty explanatory by itself, you have a set of functions that perform specific tasks on the database; please note the use of the Doctrine DBAL Query Builder. Let’s see how it works:
public function getByUsername($username)
{
$row = $this->createQueryBuilder()
->select('u.*')
->from($this->getTableName(), 'u')
->andWhere('u.username = :username')
->setParameter(':username', $username)
->execute()
->fetch($this->getFetchMode());
if ($row === false) {
throw new \Exception('Unable to find user record by username: ' . $username);
}
return new UserEntity($row);
}
Note
Doctrine 2.1 ships with a powerful query builder for the SQL language. This QueryBuilder object has methods to add parts to an SQL statement. If you built the complete state you can execute it using the connection it was generated from. The API is roughly the same as that of the DQL Query Builder. For more information please refer to http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/query-builder.html
Entities¶
The previous function returns an object called UserEntity, you may be wondering, what is that, right? well, an Entity is just an object representing a record in a table. Now, let’s see how does an Entity class looks like:
<?php
namespace UserModule\Entity;
class User
{
protected $_id = null;
protected $_username = null;
protected $_firstname = null;
protected $_lastname = null;
protected $_email = null;
public function __construct(array $data)
{
foreach ($data as $key => $value) {
if (property_exists($this, '_' . $key)) {
$this->{'_' . $key} = $value;
}
}
}
public function getID()
{
return $this->_id;
}
public function getFirstName()
{
return $this->_firstname;
}
public function getLastName()
{
return $this->_lastname;
}
public function getFullName()
{
return $this->getFirstName() . ' ' . $this->getLastName();
}
public function getEmail()
{
return $this->_email;
}
public function setUsername($username)
{
$this->_username = $username;
}
public function getUsername()
{
return $this->_username;
}
}
Fetching Data¶
We have covered so far the Storage and Entities classes, now let’s see how it actually works, for that, let’s put a sample code:
<?php namespace UserModule\Controller; use UserModule\Controller\Shared as SharedController; class Profile extends SharedController { public function viewAction() { // Get the username from the route params $username = $this->getRouteParam('username'); // Instantiate the storage service $storage = $this->getService('user.storage'); // Fetch the user by username // This returns a UserEntity Object $user = $storage->getByUsername($username); // Using the UserEntity Object is that simple: echo $user->getFullName(); // Returns the user's full name. } }
Inserting Data¶
In the previous section we saw how to fetch information from the database, now, let’s see how to insert it.
<?php
namespace UserModule\Controller;
use UserModule\Controller\Shared as SharedController;
class Profile extends SharedController
{
public function createAction()
{
// Assuming we're getting the info
// from a submitted form through POST
$post = $this->post();
// Instantiate the storage service
$storage = $this->getService('user.storage');
// @todo You've got to add some codes here
// To check for missing fields, or fields being empty.
// Prepare user array for insertion
$user = array(
'email' => $post['userEmail'],
'firstname' => $post['userFirstName'],
'lastname' => $post['userLastName'],
'username' => $post['userName']
);
// Create the user
$newUserID = $storage->create($user);
// Successful registration. \o/
$this->setFlash('success', 'User created');
return $this->redirectToRoute('User_Thankyou_Page');
}
}
Configuration Reference¶
Configuration Reference¶
Framework Configuration (“framework”)¶
This reference document is a work in progress. It should be accurate, but all options are not yet fully covered.
The core of the PPI Framework can be configured under the framework
key in your application configuration.
This includes settings related to sessions, translation, routing and more.
Configuration¶
session¶
type: integer
default: 0
This determines the lifetime of the session - in seconds. By default it will use
0
, which means the cookie is valid for the length of the browser session.
type: string
default: /
This determines the path to set in the session cookie. By default it will use /
.
type: string
default: ''
This determines the domain to set in the session cookie. By default it’s blank, meaning the host name of the server which generated the cookie according to the cookie specification.
type: Boolean
default: false
This determines whether cookies should only be sent over secure connections.
type: Boolean
default: false
This determines whether cookies should only accessible through the HTTP protocol. This means that the cookie won’t be accessible by scripting languages, such as JavaScript. This setting can effectively help to reduce identity theft through XSS attacks.
New in version 2.1: The gc_probability
option is new in version 2.1
type: integer
default: 1
This defines the probability that the garbage collector (GC) process is started
on every session initialization. The probability is calculated by using
gc_probability
/ gc_divisor
, e.g. 1/100 means there is a 1% chance
that the GC process will start on each request.
New in version 2.1: The gc_divisor
option is new in version 2.1
type: integer
default: 100
See gc_probability.
New in version 2.1: The gc_maxlifetime
option is new in version 2.1
type: integer
default: 14400
This determines the number of seconds after which data will be seen as “garbage” and potentially cleaned up. Garbage collection may occur during session start and depends on gc_divisor and gc_probability.
type: string
default: %app.cache.dir%/sessions
This determines the argument to be passed to the save handler. If you choose
the default file handler, this is the path where the files are created. You can
also set this value to the save_path
of your php.ini
by setting the
value to null
:
- YAML
# app/config/app.yml framework: session: save_path: null
- PHP
// app/config/app.php return array( 'framework' => array( 'session' => array( 'save_path' => null, ), ));
templating¶
default: { http: [], ssl: [] }
This option allows you to define base URLs to be used for assets referenced
from http
and ssl
(https
) pages. A string value may be provided in
lieu of a single-element array. If multiple base URLs are provided, PPI2
will select one from the collection each time it generates an asset’s path.
For your convenience, assets_base_urls
can be set directly with a string or
array of strings, which will be automatically organized into collections of base
URLs for http
and https
requests. If a URL starts with https://
or
is protocol-relative (i.e. starts with //) it will be added to both
collections. URLs starting with http://
will only be added to the
http
collection.
New in version 2.1: Unlike most configuration blocks, successive values for assets_base_urls
will overwrite each other instead of being merged. This behavior was chosen
because developers will typically define base URL’s for each environment.
Given that most projects tend to inherit configurations
(e.g. config_test.yml
imports config_dev.yml
) and/or share a common
base configuration (i.e. app.yml
), merging could yield a set of base
URL’s for multiple environments.
type: string
This option is used to bust the cache on assets by globally adding a query
parameter to all rendered asset paths (e.g. /images/logo.png?v2
). This
applies only to assets rendered via the Twig asset
function (or PHP equivalent)
as well as assets rendered with Assetic.
For example, suppose you have the following:
- Twig
<img src="{{ asset('images/logo.png') }}" alt="PPI!" />
- PHP
<img src="<?php echo $view['assets']->getUrl('images/logo.png') ?>" alt="PPI!" />
By default, this will render a path to your image such as /images/logo.png
.
Now, activate the assets_version
option:
- YAML
# app/config/app.yml framework: # ... templating: { engines: ['twig'], assets_version: v2 }
- PHP
// app/config/app.php return array( 'framework' => array( ..., 'templating' => array( 'engines' => array('twig'), 'assets_version' => 'v2', ), ));
Now, the same asset will be rendered as /images/logo.png?v2
If you use
this feature, you must manually increment the assets_version
value
before each deployment so that the query parameters change.
You can also control how the query string works via the assets_version_format option.
type: string
default: %%s?%%s
This specifies a sprintf pattern that will be used with the assets_version
option to construct an asset’s path. By default, the pattern adds the asset’s
version as a query string. For example, if assets_version_format
is set to
%%s?version=%%s
and assets_version
is set to 5
, the asset’s path
would be /images/logo.png?version=5
.
Note
All percentage signs (%
) in the format string must be doubled to escape
the character. Without escaping, values might inadvertently be interpreted
as a service parameter.
Tip
Some CDN’s do not support cache-busting via query strings, so injecting the
version into the actual file path is necessary. Thankfully, assets_version_format
is not limited to producing versioned query strings.
The pattern receives the asset’s original path and version as its first and
second parameters, respectively. Since the asset’s path is one parameter, you
cannot modify it in-place (e.g. /images/logo-v5.png
); however, you can
prefix the asset’s path using a pattern of version-%%2$s/%%1$s
, which
would result in the path version-5/images/logo.png
.
URL rewrite rules could then be used to disregard the version prefix before serving the asset. Alternatively, you could copy assets to the appropriate version path as part of your deployment process and forgo any URL rewriting. The latter option is useful if you would like older asset versions to remain accessible at their original URL.
Full Default Configuration¶
- YAML
framework: # router configuration router: resource: ~ # Required type: ~ http_port: 80 https_port: 443 # set to true to throw an exception when a parameter does not match the requirements # set to false to disable exceptions when a parameter does not match the requirements (and return null instead) # set to null to disable parameter checks against requirements # 'true' is the preferred configuration in development mode, while 'false' or 'null' might be preferred in production strict_requirements: true # session configuration session: storage_id: session.storage.native handler_id: session.handler.native_file name: ~ cookie_lifetime: ~ cookie_path: ~ cookie_domain: ~ cookie_secure: ~ cookie_httponly: ~ gc_divisor: ~ gc_probability: ~ gc_maxlifetime: ~ save_path: %app.cache_dir%/sessions # templating configuration templating: assets_version: ~ assets_version_format: %%s?%%s assets_base_urls: http: [] ssl: [] cache: ~ engines: # Required # Example: - twig loaders: [] packages: # Prototype name: version: ~ version_format: %%s?%%s base_urls: http: [] ssl: [] # translator configuration translator: enabled: false fallback: en
Monolog Configuration Reference¶
Monolog is a logging library for PHP 5.3 used by PPI. It is inspired by the Python LogBook library.
- YAML
monolog: handlers: # Examples: syslog: type: stream path: /var/log/symfony.log level: ERROR bubble: false formatter: my_formatter processors: - some_callable main: type: fingers_crossed action_level: WARNING buffer_size: 30 handler: custom custom: type: service id: my_handler # Default options and values for some "my_custom_handler" my_custom_handler: type: ~ # Required id: ~ priority: 0 level: DEBUG bubble: true path: "%app.logs_dir%/%app.environment%.log" ident: false facility: user max_files: 0 action_level: WARNING activation_strategy: ~ stop_buffering: true buffer_size: 0 handler: ~ members: [] channels: type: ~ elements: ~ from_email: ~ to_email: ~ subject: ~ email_prototype: id: ~ # Required (when the email_prototype is used) factory-method: ~ channels: type: ~ elements: [] formatter: ~