Presentation
Presentation refers to the part of the application that is visible to the user - the user interface - and the handling of user interaction.
Services
The presentation layer usually depends on underlying services like persistency, event handling, caching - just to name a few. These services typically exist as one instance that is created on application startup and is used throughout the whole application lifetime.
The preferred way to get access to a service instance in client code is to set the dependency explicitly or let it be injected, if the instance is not created explicitly (see Dependency injection). But there are also situations where this is not possible (e.g. a global function that is used by third party code). Since most of these services are registered with ObjectFactory
, it's getInstance
method is used as a service locator.
The following example shows how to get the persistence service at any code location:
Application
Web applications typically implement a request-response pattern, where a client sends a (HTTP-)request to the application, which returns a response after processing it. wCMF encapsulates the described procedure inside the Application
class. The following code demonstrates the usage of this class in the main entry script of the default application.
The example shows the three important methods of the Application
class:
- The
initialize
method is used to setup theApplication
. It returns aRequest
instance, that could be modified before execution. - The
run
method is called to execute the given request. The method returns aResponse
instance, that is not used in this example. - The
handleException
method is called, if an exception occurs. The method rolls back the database transaction and calls the failure action.
The details of request execution are the topic of the next section.
Request processing
The Request
instance created on initialization of the application provides all information about the incoming HTTP request, that is necessary for execution. Upon execution, the following actions are performed:
- The
Request
instance is passed toActionMapper
for further processing. - The Action Key is determined from the request parameters.
PermissionManager
is asked to authorize the action key for the current user (see Checking permissions).- If authorization is successful, the request data is transformed into the internal application format (see Formats).
- The
Controller
instance matching the request is determined (see Routing) and executed. - The
Response
instance is obtained after execution. - The response data is transformed into the requested response format (see Formats).
- Execution returns to step 3, if a valid action key is contained in the response data. It terminates, if either no action key is found or the next action key would be the same as the previous (to prevent recursion).
Formats
wCMF is designed to be able to consume various request formats and produce several response formats. While some clients communicate using JSON format, others might prefer to encode data in XML. Formatter
is used to determine the required format and delegate the actual formatting to the correct Format
implementation. wCMF currently provides the following implementations:
HtmlFormat
expects all data to be sent as key-value-pairs. Object data are transferred in parameters namedvalue-
name-oid (e.g.value-title-Book:3
). Responses in this format are rendered as HTML views (see Views).JsonFormat
handles JSON encoded request data and encodes response data into the same format.SoapFormat
is used together with the NuSOAP library to implement a SOAP interface.GenericFormat
is used to output arbitrary responses.DownloadFormat
is used to create a downloadable reponse document. It is automatically chosen, if theResponse::setDocument
method is called with aResponseDocument
.NullFormat
is used internally, if no formatting is required, e.g. if one controller calls another controller.
If not explicitely set, the request and response format is automatically determined from the HTTP headers sent with the request:
- The
Content-Type
header defines the request format - The
Accept
header defines the response format
To find the correct format, the Media Type value set in those headers is matched against the mime type of all registered formats (see Format::getMimeType
).
Formats are defined in the Formats
configuration section as shown in the following example:
Routing
Routing is the process of selecting the correct Controller
for a given request. wCMF distinguishes between internal and external routing.
Internal routing
Internal routing takes place after the Request
instance is created and initialized. wCMF inspects the action key formed from it's sender, context and action parameters (see Action Key) to determine the controller to be executed for the request. The mapping of action keys to controllers is defined in the ActionMapping
configuration section.
If the executed controller together with the context and action parameters of the response match another action key, the corresponding controller will be executed afterwards. This allows to chain actions together. If no matching action key is found, the response is returned to the client.
The following code is taken from the default application configuration and shows the configuration of the indexing process (see Long running requests):
- The first line states that the action indexAll called from any controller and in any context will invoke
SearchIndexController
, which will set a state dependent action name on the response (seeBatchController
). - The second line tells
ActionMapper
to re-invokeSearchIndexController
, if it was the last controller and the action is continue. So this action key only matches, if the last controller wasSearchIndexController
.
Controller methods
The previous example maps the action keys to a controller class without specifying a method to be called. In these cases, the framework calls the default method doExecute
, which must then be defined in the controller class (see Controllers).
Alternatively a specific controller method to be called could be defined in the action mapping, like illustrated in the following example:
In this case SearchIndexController
would have to define the methods doBegin
and doContinue
which are called for the appropriate action keys.
External routing
The mapping of the current request uri to an action key is called external routing. The default mapping logic is implemented in the DefaultRequest::initialize
method. The method matches the path part of the request uri against the entries of the Routes
configuration section to find an appropriate action key.
Variables declared in path segments will be automatically passed as request parameters, but would be overwritten by explicit request parameters if provided. The character * is used as wildcard. Regular expression patterns can be added to variables to narrow the set of possibilities.
The following example configuration taken from the default application illustrates the concept:
- The first two entries define the login and logout routes using the appropriate HTTP methods.
- The third entry defines the language and className variables (surrounded by curly braces) and would be matched by the request uris /rest/de/Author or /rest/en/Book. The executed action would be restAction.
- The id variable in the next entry must be an integer because of the regular expression constraint
[0-9]+
. - The last entry is matched by any GET request to a path that was not matched before. It is mapped to the cms action - corresponding to the action key ??cms.
- Note
- Any requests on routes that are not defined in the
Routes
configuration section will cause a404
error. This is why this section is also considered the public interface of the application.
Precedence rules
Due to the use of variables in route definitions several routes might match a given request. This leads to the question which route to take, since the application only processes one per request. To find the best matching route DefaultRequest
orders all matching routes and takes the first one. The sorting is done under the assumption that a more specific route is a better match.
The following rules are implemented to find the more specific route:
- the route with less variables is more specific and if two routes have the same number of variables
- the route with more patterns in the variable definitions is more specific.
In cases where these simple rules are not sufficient, you could configure a custom Request
implementation (see Dependency injection) which overrides the DefaultRequest::isMatch
method.
HTTP methods
To restrict a path to one or more HTTP methods, they are added in front of the route definition. In the following example the cms action is only available for the GET method, while the other actions accept GET, POST, PUT and DELETE requests:
If no method is added to a route, all methods are accepted.
Controllers
Controllers take the user input from the request and modify the model according to it. As a result a response is created which is presented to the user in a view or any other format. Which controller is executed on a specific request is determined in the routing process (see Routing).
wCMF provides Controller
as abstract base class for controller implementations. There are three important methods defined in this class, which are called by ActionMapper
in the following order:
Controller::initialize
is called directly after instantiation of the controller. The currentRequest
andResponse
instances are passed as parameters and subclasses can override this method to implement task specific initializations.Controller::validate
is called afterwards to check the validity of the request parameters. The default implementation returns true and subclasses are assumed to override this method if necessary to do task specific validations.Controller::execute
is called finally. This method accepts a parameter, that defines the actual method to be called on the subclass. If the parameter is omitted it defaults todoExecute
which must then be defined in the subclass (see \pres_controller_methods).
Controllers can redirect to other controllers by using the Controller::redirect
method.
If variables need to be shared between several requests to the same controller they can be stored in local session variables to avoid side effects (see Controller::setLocalSessionValue
).
Error handling
Errors are typically divided into fatal errors and non-fatal errors.
By definition the application is not able to recover from a fatal error, meaning that it's not functioning correctly. An example would be a programming error or a missing vital resource. These errors normally need to be fixed by the application maintainer. In case of non-fatal errors a notice to the user is sufficient in most cases. A typical example would be invalid user input, that can be fixed by the user itself.
In wCMF the following two strategies are recommended for handling these kind of situations:
- In case of a fatal error, an exception should be thrown. If it is not caught inside the application code, it will bubble up to the main script (usually index.php). In case the application is set up like in the Application section, the method
Application::handleException
will be called. This method rolls back the current transaction and calls the failure action, which is executed byFailureController
by default. - If a non-fatal error occurs, an instance of
ApplicationError
should be created and added to the response using theResponse::addError
method. The error class provides theApplicationError::get
method to retrieve predefined errors. The following example shows how to signal an invalid type parameter while request validation:
Action composition
The Internal routing section describes how chaining controller actions works. This is usefull, if there is a sequence of actions to be executed. If a complex action can be composed from simpler (already existing) actions, it is also possible to execute another controller inside a controller action and return to that action afterwards. This is done using the Controller::executeSubAction
method. An example is RESTController
which serves as a facade to the CRUD controllers.
Automatic transactions
To run a controller method inside a persistence transaction the convenience method Controller::requireTransaction
can be called at the beginning of the method. This call will start a transaction or join the already opened one. The transaction will be automatically committed after method execution or rolled back in case of an exception. Calls to Controller::executeSubAction
will use the same transaction.
Long running requests
There are situations where you want to split a long running process into parts, because it's exceeding memory or time limits or simply to give the user feedback about the progress. By subclassing BatchController
the implementation of this behavior is as simple as defining the steps of the process in the BatchController::getWorkPackage
method.
SearchIndexController
is an example for a controller implementing a long running process. It is used to create a Lucene search index over all searchable entity objects. The process is split into collecting all object ids and then indexing them. In the final step the index is optimized.
Views
Views are used to present application information to the user. In a web application they are typically HTML pages displayed in the browser.
In wCMF the response will be turned into an HTML page, if the Accept
HTTP header is set to text/html (see Formats). The appropriate Format
implementation is HtmlFormat
. It renders the response into a template file using the configured View
implementation. The format of the template files depends on that implementation. Since different actions could require different views to be displayed, a mapping of action keys to view templates is defined in the Views
configuration section.
The following example shows the configuration of the SmartyView
class and the mapping of action keys to views in the default application:
Since the default application only uses an HTML page to bootstrap the actual Dojo application, there is only one view mapping for RootController
.
Device dependent views
HtmlFormat
allows to provide different versions of a view template. This is especially useful, if you want to deliver device dependent content for the same action key.
To select a specific template version, the property html_tpl_format
has to be set on the response instance (see ControllerMessage::setProperty
). E.g. if the template file would be home.tpl, setting the value to mobile would select the template file home-mobile.tpl. If the requested version does not exist, it is ignored and the default template is used (home.tpl in this example).
Webservice APIs
Besides the user interface driven default application wCMF provides APIs for using the application as a web service. These APIs provide create/read/update/delete (CRUD) operations on all entity types.
RESTful API
The RESTful interface is implemented in RESTController
, which basically acts as a facade in front of the application. That means the controller checks the request data only and delegates the actual processing to the action specific controller.
The following example shows the configuration of the RESTful interface in the default application:
The Routes
configuration section defines the urls for the interface. They all call the action restAction internally, which is handled by RESTController
as defined in the ActionMapping
section. For example the english version of the Author
instance with id 1 is retrieved by making a GET request to the url /rest/en/Author/1.
SOAP API
wCMF uses the NuSOAP library for implementing the SOAP interface. It consists of the SoapServer
and SOAPController
classes. The controller class handles all requests and delegates the processing to the server class. The service description in the WSDL format is generated by the code generator into a file called soap-interface.php (see PHP Code).
The following example shows the configuration of the SOAP interface in the default application:
The Routes
configuration section defines that the url /soap redirects to the action soapAction internally. The ActionMapping
section defines that this action is handled by SOAPController
. The interface description is available at the url /soap?wsdl.
Caching
Caching is an effective method to improve application performance. Caches in wCMF are supposed to be divided into sections, which hold key-value pairs (see Cache
interface). Cache instances are defined in the application configuration and retrieved by using ObjectFactory
, which makes it easy to exchange the underlying caching implementation.
The following example shows the configuration of a FileCache
instance in the default application:
The usage of this cache is illustrated in the code example:
Supposed that complexCalculationA
in this example takes long to finish, it should only run once at the first time the result is needed. So we check if resultA is already cached and calculate it, if not. Since it is put it into the cache after calculation, it can be retrieved resultA directly from there the next time it is needed.
Response caching
Besides setting caching headers by using the Response::setHeader
method, wCMF provides a convenient way to set the Etag
and Last Modified
headers on the response.
All one needs to do is derive a cache identifier from the request data, that allows to distinguish different requests and set it on the response using the Response::setCacheId
method. If the used Format
is capable of caching (e.g. HtmlFormat
) it will provide the last modified date from the cached data. DefaultFormatter
will then use this information to create the Etag
and Last Modified
headers:
Another advantage of using a cache identifier is the possibility to avoid expensive calculations in Controller
implementations by using the Response::isCached
method. The following example shows it's usage:
Events
Events are a way to decouple parts of a software by introducing an indirect communication. They allow clients to react to certain application events without the event source knowing them. wCMF uses the publish-subscribe messaging pattern, in which EventManager
acts as the message broker. Subscribers use this class to register for certain Event
types and publishers to send the events.
The following events are defined in wCMF:
PersistenceEvent
is sent whenever aPersistentObject
instance is created, updated or deleted.PropertyChangeEvent
,ValueChangeEvent
andStateChangeEvent
signal changes inPersistentObject
instances.TransactionEvent
is fired at different phases of a transaction.ApplicationEvent
allows to listen to the different steps of the request handling process.
The event system can be extended by custom events by inheriting from the Event
class.
The class below is a basic example for a listener that subscribes to persistence events:
- Note
- To prevent memory leaks the
EventManager::removeListener
method must be called, if the event listener is destroyed.
To send a persistence event, the following code is used:
Implicit listener installation
In some cases you might want to install event listeners without explicitely instantiating them, because there is no appropriate place for that. For these cases the Application
class reads the listeners
value of the Application
configuration section and initializes all instances listed there.
The default application defines two listeners as shown in the following example:
Each entry in the listeners
array is supposed to refer to an instance configuration (see Dependency injection).
Logging
wCMF integrates the logging frameworks log4php and Monolog. To abstract from these libraries wCMF defines the Logger
interface and implementations for each framework. The decision, which framework to use is made by instantiating the appropriate Logger
instance and passing it to the LogManager::configure
method as shown for Monolog in the following example:
Afterwards Logger
instances can be retrieved using the following code:
The parameter used in the LogManager::getLogger
method is the logger name. It's a good practice to use the __CLASS__
constant as logger name, since this allows to enable/disable loggers by class names in the configuration (see Logging configuration).
The following example shows how to log an error message with a stack trace appended (see ErrorHandler::getStackTrace
):