By default framework supports html and json view resolving on both STDERR and STDOUT flows. Json will be handled by JsonResolver/JsonRenderer, while html will generally be handled by HtmlRenderer/HtmlResolver.
Each view resolver on STDOUT flow corresponds to a <format> tag in stdout.xml. Framework comes by default with:
<formats>
<format name="html" content_type="text/html" class="HtmlResolver" charset="UTF-8"/>
<format name="json" content_type="application/json" class="JsonResolver" charset="UTF-8"/>
</formats>
Each view resolver on STDERR flow corresponds to a <resolver> tag in stderr.xml. Framework comes by default with:
<resolvers>
<resolver format="html" content_type="text/html" class="HtmlRenderer" charset="UTF-8"/>
<resolver format="json" content_type="application/json" class="JsonRenderer" charset="UTF-8"/>
</resolvers>
For STDOUT flow only, it is possible to have multiple resolvers for same content type:
<formats>
<format name="html" content_type="text/html" class="HtmlResolver" charset="UTF-8"/>
<format name="custom" content_type="text/html" class="CustomResolver" charset="UTF-8"/>
<format name="json" content_type="application/json" class="JsonResolver" charset="UTF-8"/>
</formats>
In which case <route> that requires another resolver from default needs to set format attribute accordingly:
<routes>
...
<route ... format="custom"/>
</routes>
By default, framework supports only text/html and application/json display formats via its MVC APIs. To add support for another (eg: text/xml), developers need to setup XML tags for each MVC APIs then write referenced view resolver classes.
These view resolvers will be executed to handle responses to requests in a desired display format. To support a new display format, first you need a matching <format> tag @ stdout.xml. Example:
<format name="xml" content_type="text/xml" class="XMLRenderer"/>
This tells that XMLRenderer class will handle text/xml responses when ever a <route> has a format attribute whose value equals xml. Example:
<route url="sitemap.xml" controller="SitemapController" view="sitemap" format="xml"/>
Now you need to write the view resolver itself. It must have a name that equals value of class attribute above, extend Lucinda\STDOUT\ViewResolver and be found in application/resolvers folder. Example:
class XMLResolver extends \Lucinda\STDOUT\ViewResolver
{
public function run(): void
{
if (!$this->response->view()->getFile()) {
return;
}
// locates view
$view = $this->application->getViewsPath()."/".$this->response->view()->getFile().".php";
if (!file_exists($view)) {
throw new Lucinda\STDOUT\ServletException("View file not found!");
}
// compiles view into a string
ob_start();
$_VIEW = $this->response->view()->getData();
require_once($view);
$output = ob_get_contents();
ob_end_clean();
// sends string to output buffer
$this->response->setBody($output);
}
}
Then, in application/views, you need to write sitemap.php file, as referenced by view attribute above, to be rendered by resolver above. Example:
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<?php foreach($_VIEW["items"] as $url=>$priority) { ?>
<url>
<loc><?php echo $url; ?></loc>
<priority><?php echo $priority; ?></priority>
</url>
<?php ?>
</urlset>
This assumes SitemapController sent an items response attribute as an array whose key is url and value is priority:
class SitemapController extends Lucinda\STDOUT\Controller
{
public function run(): void
{
...
$this->response->view()["items"] = $model->getPages();
}
}
These view resolvers will be executed to handle responses to exceptions in a desired display format. To support a new display format, first you need a matching <resolver> tag @ stderr.xml. Example:
<renderer format="html" content_type="text/xml" class="XMLRenderer"/>
This tells that XMLRenderer class will handle text/xml responses if ErrorListener is active and originally called <route> had a format attribute whose value equals xml (see above).
Now you need to write the view resolver itself. It must have a name that equals value of class attribute above, implement Lucinda\STDERR\ViewResolver and be found in application/renderers folder. Example:
class XMLRenderer implements \Lucinda\STDERR\ViewResolver
{
public function run(): void
{
if (!$this->response->view()->getFile()) {
return;
}
// locates view
$view = $response->view()->getFile().".php";
if (!file_exists($view)) {
throw new Lucinda\STDERR\Exception("View file not found!");
}
// compiles view into a string
ob_start();
$_VIEW = $response->view()->getData();
require_once($view);
$output = ob_get_contents();
ob_end_clean();
// sends string to output buffer
$this->response->setBody($output);
}
}
Then, in application/views, you need same sitemap.php file as in previous section and an ErrorsController modified to send items response attribute as an array whose key is url and value is priority:
class ErrorsController extends Lucinda\STDERR\Controller
{
public function run(): void
{
...
$this->response->view()["items"] = $model->getPages();
}
}
Framework or developers collect response information into Response objects: a Lucinda\STDOUT\Response (encapsulating response to requests) and a Lucinda\STDERR\Response (encapsulating response to exceptions).
Both classes were done with symmetry in mind (for shared logic, methods have same signature). Most of their shared methods are useful to developers!
Since everything is managed in XML via view attribute of matching <route>, setting views is seldom needed. Following method is dedicated at setting view file:
$this->response->view()->setFile("application/views/foo/bar")
Response will render file application/views/foo/bar.html, if response format is htmlIn order to send data to views from controllers, following method is relevant:
view()[NAME] = VALUE: sends data by name and value. Overrides if NAME is already used! Example:
$this->response->view()["user_name"]="John Doe";
By default, view resolvers used by framework send only Content-Type header and Etag/Last-Modified (if HTTP caching was activated via Setting Response Caching section above). To fine tune that behavior, following method is relevant:
headers(NAME, VALUE): sets a response header by name and value. Overrides if NAME is already used! Example:
$this->response->headers("Content-Encoding", "gzip");
If you desire to set response headers by their ISO name instead (so you won't have to keep on remembering their names), you need to open bootstrap index.php file and add this event listener:
$object->addEventListener(Lucinda\STDOUT\EventType::REQUEST, "HttpHeadersListener");
then use Lucinda\Headers\Response in controllers or subsequent event listeners to easily set value of response headers. Example:
$this->attributes->getHeaders()->getResponse()->addTransferEncoding("gzip")
By default, HTTP response status for every request is 200 OK (containing full response body) or 304 Not Modified (if it hadn't modified from last call), if HTTP caching was activated (see Setting Response Caching section above).
When an uncaught exception is thrown and framework enters STDERR flow, HTTP response status defaults to 500 Internal Server Error, unless another one is specifically set by http_status of matching <exception>. Example:
<exception class="Lucinda\STDOUT\PathNotFoundException" http_status="404"/>
For those rare cases in which you need to programmatically control http status of response, following method is relevant:
$this->response->setStatus(404)
Response will render with 404 Not Found status code.To redirect caller to another location, following static method is available:
Response::redirect("/login", false, true); // Lucinda\STDOUT\Response or Lucinda\STDERR\Response
Sometimes it is necessary for controllers to write directly to output stream and thus do the task of view resolvers. To get access to output stream, use this method:
$this->response->setBody("Move along!");
If this is used, MVC APIs will not perform view resolution (since output stream was already filled) and output exactly what you filled there!
Framework promotes caching of unchanged response using your browser via HTTP headers (as if it were an image). To make it possible, you need to open bootstrap index.php file and add this event listener:
$object->addEventListener(Lucinda\STDOUT\EventType::RESPONSE, "HttpCachingListener");
which requires another event listener before:
$object->addEventListener(Lucinda\STDOUT\EventType::REQUEST, "HttpHeadersListener");
which requires a <headers> tag @ stdout.xml:
<headers cacheable="application/cacheables/[DRIVER]"/>
Where [DRIVER] is a Lucinda\Headers\Cacheable implementation, able to get a string / date representation of response about to be sent. Framework comes by default with:
After you have completed Setting Response Caching section described above, you are now able to automatically communicate with client browser cache depending on [DRIVER] solution used and deliver an efficient response according to following rules:
Request Header | Cacheable | Response Header | Matching | Response Status | Response Body |
- | EtagCacheable | ETag | no | 200 | yes |
If-None-Match | EtagCacheable | ETag | yes | 304 | no |
If-None-Match | EtagCacheable | ETag | no | 200 | yes |
- | DateCacheable | Last-Modified | no | 200 | yes |
If-Modified-Since | DateCacheable | Last-Modified | yes | 304 | no |
If-Modified-Since | DateCacheable | Last-Modified | no | 200 | yes |
Framework supports internationalization of views based on user's preffered locale. To make it possible, you need to open bootstrap index.php file and add this event listener:
$object->addEventListener(Lucinda\STDOUT\EventType::REQUEST, "LocalizationListener");
then create a <internationalization> tag to set up the process. Example:
<internationalization locale="en_US" method="header"/>
This sets default locale to en_US and performs internationalization by value of Accept-Language header.
After you have completed Setting Response Internationalization section described above, you are now able to make views translate automatically by client locale (language & country). Following locale detection methods are supported:
If detection fails or locale is not supported, a default set in XML is used (eg: en-US). Once locale was detected, clients must be able to see a translated version of called page! In order for that to happen, developers need to perform following steps:
{"greeting":"Hello!", "who_are_you":"I am %0 from %1."}
{"greeting":"Bon jour!", "who_are_you":"Je suis %0 de %1."}
<p><strong>Hello!<strong><br/>I am Lucian from Bucharest.</p>
You use:<p><strong>${translate('greeting')}<strong><br/>${translate('who_are_you', '', 'Lucian', 'Bucharest')}
By using a second argument for translate functions, developers can call another translation file, a very useful feature against using a single huge messages.json. Example:
${translate('who_are_you', 'presentation', 'Lucian', 'Bucharest')}
This tells to look for who_are_you translation in locale/en_US/presentation.json file (if detected locale was en_US). If second function argument is empty, value of "domain" attribute value is used instead.