The main components of the App package are:
At the core of a Charcoal application is a highy customizable configuration store: Charcoal\App\AppConfig
, provided by charcoal/config.
Typically, the application's configuration should load a file located in config/config.php
. This file might load other, specialized, config files (PHP, JSON, or INI).
In the front controller, ensure the configuration is loaded:
$config = new \Charcoal\App\AppConfig();
$config->addFile(__DIR__.'/../config/config.php');
It is recommended to keep a separate config file for all of your different app modules. Compartmentalized config sections are easier to maintain and understand.
The official boilerplate provides a good example of a configuration setup.
Key | Type | Default | Description |
---|---|---|---|
base_path | array |
[] |
|
base_url | array |
[] |
|
ROOT | array |
[] |
An alias of base_path . |
timezone | string |
"UTC" |
The current timezone. |
\Charcoal\App\AppConfig
API:
Key | Type | Default | Description |
---|---|---|---|
modules | array |
[] |
|
routables | array |
[] |
|
routes | array |
[] |
|
service_providers | array |
[] |
The main app can be seen, in a way, as the "default module".
Key | Type | Default | Description |
---|---|---|---|
cache | array |
null |
|
databases | array |
[] |
An array of DatabaseConfig |
default_database | string |
"" |
|
array |
[] |
The email (default from and SMTP options) configuration. See EmailConfig |
|
filesystem | array |
null |
|
logger | array |
null |
The logger service configuration |
translator | array |
null |
|
view | array |
null |
The default view configuration (default engine and path settings). See ViewConfig . |
Example of a module configuration:
{
"routes": {
"templates": {
"foo/bar": {},
"foo/baz/{:id}": {
"controller": "foo/baz",
"methods": [ "GET", "POST" ]
}
},
"default_template": "foo_bar",
"actions": {
"foo/bar": {}
}
},
"routables": {
"charcoal/cms/news": {}
},
"service_providers": [
"foo/bar/service-provider/test"
],
"middlewares": {}
}
The App component is based on Slim.
It actually extends the \Slim\App
class.
What is Slim?
At its core, Slim is a dispatcher that receives an HTTP request, invokes an appropriate callback routine, and returns an HTTP response.
The App is responsible for loading the modules, setting up the routes and the default handlers and adding Service Providers to provide external services to the DI Container.
Initialize the app in the Front Controller:
// Create container and configure it (with charcoal/config)
$container = new \Charcoal\App\AppContainer([
// Slim Configuration
'settings' => [
'displayErrorDetails' => true
],
// Charcoal Configuration; see "Config Component", above.
'config' => $config
]);
// Charcoal / Slim is the main app
$app = \Charcoal\App\App::instance($container);
$app->run();
The boilerplate provides a good example of a front controller.
—N/A—
All routes are actually handled by the Slim app. Charcoal Routes are just definition of a route:
- An identifier, which typically matches the controller.
- A RouteConfig structure, which contains:
- The
type
ofRequestController
. This can be:Action
Script
(Scripts can only be ran from the CLI.)Template
- The
route_controller
ident, which will identify the proper controller to create.- Controllers are created from a resolver factory. Their identifier may look like
foo/bar/controller-name
.
- Controllers are created from a resolver factory. Their identifier may look like
- The
Routes can also be (and most likely are in standard web scenario) defined by objects. For example: sections, news, events, etc. See [charcoal/object] for the definition of routable objects, and charcoal/cms for examples of routable objects.
The default action route handler is charcoal/app/route/action
(\Charcoal\App\Route\ActionRoute
).
Actions are set on POST
requests by default, but this can be overridden by setting the methods
route option.
By default, what this route handler does is instanciate an Action object (the type of object is set with the controller
, or ident
option) and invoke it. The Action must implement \Charcoal\App\Action\ActionInterface
.
Actions are basic Charcoal Entities (they extend the \Charcoal\Config\AbstractEntity
class). Actions are meant to be subclassed in custom projects. But it provides the following default options:
Key | Type | Default | Description |
---|---|---|---|
mode | string |
``json'` | The mode can be "json" or "redirect". json returns json data; redirect sends a 30X redirect. |
success | boolean |
false |
Wether the action was successful or not. Typically changed in the run method. |
success_url | string |
null |
|
failure_url | string |
null |
When writing an action, there are only 2 abstract methods that must be added to the Action class:
run(RequestInterface $request, ResponseInterface $response);
results();
The run method is ran automatically when invoking an action.
There are 2 steps to creating a custom action:
- Set up the action route
- Write the Action controller
In the config file (typically, config/routes.json
loaded from config/config.php
:
{
"routes": {
"actions": {
"test": {
"controller": "foo/bar/action/test"
}
}
}
}
The controller FQN should match its identifier. In this example, the Action Factory will attempt to load the foo/bar/action/test
controller, which will match the \Foo\Bar\Action\TestAction
class. Using PSR-4, this class should be located in the source at src/Foo/Bar/Action/TestAction.php
namespace Foo\Bar\Action;
use \Psr\Http\Message\RequestInterface;
use \Psr\Http\Message\ResponseInterface;
use \Charcoal\App\Action\AbstractAction;
class TestAction extends AbstractAction
{
private $greetings;
/**
* @param RequestInterface $request A PSR-7 compatible Request instance.
* @param ResponseInterface $response A PSR-7 compatible Response instance.
* @return ResponseInterface
*/
public function run(RequestInterface $request, ResponseInterface $response)
{
$this->greetings = 'Hello world!';
$this->setSuccess(true);
return $response;
}
/**
* @return array
*/
public function results()
{
return [
'success' => $this->success(),
'greetings' => $this->greetings
];
}
}
When requesting http://$URL/test
(with POST), the following should be returned:
{
"success": 1,
"greetings": "Hello World!"
}
The default script route handler is charcoal/app/route/script
(\Charcoal\App\Route\ScriptRoute
).
Scripts mock a Slim HTTP environment for the CLI. Allowing to be route like regular web routes but for a script environment. This package comes with the charcoal
binary which is meant to run those kind of scripts.
Thanks to composer, the charcoal binary is installed automatically in your project and callable with
php vendor/bin/charcoal
.
By default, what this route handler does is instanciate a Script object (the type of object is set with controller
or ident
option) and invoke it. The Script must implement \Charcoal\App\Script\ScriptInterface
.
The CLI helper (arguments parser, input and output handlers) is provided by CLImate.
Key | Type | Default | Description |
---|---|---|---|
arguments | array |
help, quiet, and verbose | The script arguments. |
Creating custom scripts is exactly like creating custom actions:
- Set up the script route.
- Write the Script controller.
In the config file (typically, config/routes.json
loaded from config/config.php
:
{
"routes": {
"scripts": {
"test": {
"controller": "foo/bar/script/test"
}
}
}
}
The controller FQN should match its identifier. In this example, the Script Factory will attempt to load the foo/bar/sript/test
controller, which will match the \Foo\Bar\Script\TestScript
class. Using PSR-4, this class should be located in the source at src/Foo/Bar/Script/TestScript.php
namespace Foo\Bar\Script;
use \Psr\Http\Message\RequestInterface;
use \Psr\Http\Message\ResponseInterface;
use \Charcoal\App\Script\AbstractScript;
class TestScript extends AbstractScript
{
/**
* @param RequestInterface $request A PSR-7 compatible Request instance.
* @param ResponseInterface $response A PSR-7 compatible Response instance.
* @return ResponseInterface
*/
public function run(RequestInterface $request, ResponseInterface $response)
{
$this->climate()->out('Hello World!');
return $response;
}
}
Calling the script with ./vendor/bin/charcoal test
should output:
★ Hello World!
The default template route handler is charcoal/app/route/template
(\Charcoal\App\Route\TemplateRoute
).
Templates are set on GET
requests by default, but this can be overridden by setting the methods
route option.
In a typical Charcoal project, most "Web pages" are served as a Template.
By default, what this route handler does is instanciate a Template object (the type of object is set with the controller
, or ident
option) and "render" it. The Action must implement \Charcoal\App\Action\ActionInterface
.
To render the template, it is important that a view
has been set properly on the DI container. This can be done easily with the View Service Provider
Creating custom templates is probably the most common thing to do for a Charcoal project. There are 3 steps involved:
- Set up the template route
- Write the template controller
- Write the template view
Although it is possible to use different rendering engines, the following example assume the default
mustache
engine.
In the config file (typically, config/routes.json
loaded from config/config.php
:
{
"routes": {
"templates": {
"test": {
"controller": "foo/bar/template/test",
"template": "foo/bar/template/test"
}
}
}
}
The controller FQN should match its identifier. In this example, the Template Factory will attempt to load the foo/bar/template/test
controller, which will match the \Foo\Bar\Template\TestTemplate
class. Using PSR-4, this class should be located in the source at src/Foo/Bar/Template/TestTemplate.php
namespace Foo\Bar\Template;
use \Psr\Http\Message\RequestInterface;
use \Psr\Http\Message\ResponseInterface;
use \Charcoal\App\Action\AbstractTemplate;
class TestTemplate extends AbstractTemplate
{
/**
* @return string
*/
public function greetings()
{
return 'Hello World!';
}
}
Finally, the template view must also be created. The route config above specified the template as foo/bar/template/test
. Because the default engine (mustache) is used, the loaded file should be located at templates/foo/bar/template/test.mustache
:
{{> foo/bar/template/inc.header }}
<section class="main">
{{ greetings }}
</section>
{{> foo/bar/template/inc.footer }}
👉 Slim's routing is actually provided by FastRoute
Common route configuration
Key | Type | Default | Description |
---|---|---|---|
ident | string |
null |
Route identifier. |
route | string |
null |
Route pattern. |
methods | string[] |
[ 'GET' ] |
The HTTP methods to wthich this route resolve to. Ex: ['GET', 'POST', 'PUT', 'DELETE'] |
controller | string |
null |
Controller identifier. Will be guessed from the ident when null . |
lang | string |
null |
The current language. |
groups | string[] |
null |
The route group, if any. |
Additionnaly, a route_controller option can be set, to load a custom route handler.
Action specific configuration
Key | Type | Default | Description |
---|---|---|---|
action_data | array |
[] |
Extra / custom action data. |
Script specific configuration
Key | Type | Default | Description |
---|---|---|---|
script_data | array |
[] |
Extra / custom script data. |
Template specific configuration
Key | Type | Default | Description |
---|---|---|---|
template | string |
null |
The template ident to display. |
engine | string |
'mustache' |
The template engine type. Default Charcoal view engines are mustache , php and php-mustache . |
template_data | array |
[] |
Extra / custom template data. |
cache | boolean |
false |
Set to true to enable template-level cache on this object. This is not recommended for any page that must serve dynamic content. |
cache_ttl | integer |
0 |
The time-to-live, in seconds, of the cache object, if applicable. |
Here is an example of route definitions. Some things to note:
- To set the "default" template (GET) route, simply map a route to "/".
- Most configuration options are optional.
- The "full" routes in the example below tries to display all posible config options.
- Custom route controller
- A lot of those are unnecessary, as they are set by default.
- The "redirect" option is not set, as it conflicts most other options or renders them unncessary.
- The same definition could be pure PHP.
{
"routes": {
"templates": {
"/": {
"redirect": "home"
},
"home": {
"controller": "acme/template/home",
"template": "acme/template/home"
},
"full": {
"route": "/full",
"route_controller": "acme/route/template",
"ident": "full-example",
"template": "acme/route/full",
"controller": "acme/route/full",
"engine": "mustache",
"methods": ["GET"],
"cache": false,
"cache_ttl": 0,
"template_data": {
"custom_options": 42
}
}
},
"actions": {
"publish": {
"controller": "acme/action/blog/publish",
}
},
"scripts": {
"foo": {
"controller": "acme/script/foo"
}
}
}
}
Routes are great to match URL path to template controller or action controller, but needs to all be defined in the (main) AppConfig
configuration.
Routables, on the other hand, are dynamic objects (typically, Charcoal Model objects that implements the Charcoal\App\Routable\RoutableInterface
) whose route path is typically defined from a dynamic property (and stored in a database).
The RoutableInterface
/ RoutableTrait
classes have one abstract method: handleRoute($path, $request, $response)
which must be implemented in the routable class.
This method should:
- Check the path to know if it should respond
- Typically, this means checking the path parameter against the database to load a matching object.
- But really, it could be anything...
- Return a
callable
object that will handle the route if it matches - Return
null
if no match
The returned callable signature should be:
function(RequestInterface $request, ResponseInterface $response)
and returns a ResponseInterface
Routables are called last (only if no explicit routes match fisrt). If no routables return a callable, then a 404 will be sent. (Slim's NotFoundHandler
).
The charcoal/cms module contains many good examples of routable objects.
Just like routes (or everything else "Charcoal", really...), middlewares are set up through the app's config.
To be enabled, middlewares must be "active" and they must be accessible from the app's container
.
For example
There are 2 middlewares provided by default in the app
module:
\Charcoal\App\Middleware\CacheMiddleware
\Charcoal\App\Middleware\Cache\IpMiddleware
Other Charcoal modules may provide more middlewares (for example, language detection in charcoal/translator).
As previously mentionned, Script
routes are only available to run from the CLI. A script loader is provided in bin/charcoal
. It will be installed, with composer, in vendor/bin/charcoal
.
To view available commands:
★ ./vendor/bin/charcoal
Also provided in this package is PSR-7 integration tests helpers, for phpunit
testing.
The \Charcoal\Test\App\ServerTestTrait
can be used by any TestCase to quickly start the built-in PHP server, performs request and run tests on the result.
use PHPUnit\Framework\TestCase;
use Charcoal\Test\App\ServerTestTrait;
class ExampleTest extends TestCase
{
use ServerTestTrait;
public static function setUpBeforeClass()
{
static::$serverRoot = dirname(__DIR__).DIRECTORY_SEPARATOR.'www';
}
public function testHomeURLis200()
{
$response = $this->callRequest([
'method' => 'GET',
'route' => '/en/home',
'options' => null
]);
$this->assertResponseHasStatusCode(200, $response);
}
}
Available methods are:
callRequest(array $request)
to get a ResponseInterface object.assertResponseMatchesExpected(array $expected, ResponseInterface $response)
assertResponseHasStatusCode($expectedStatusCode, ResponseInterface $response)
assertResponseBodyMatchesJson($json, ResponseInterface $response)
assertResponseBodyRegExp($pattern, ResponseInterface $response)