Working with view models

1. Introduction

The view-generating part of the APF is heavily based on the HMVC pattern. In contrast to MVC it is more flexible and allows to build complex applications without the need to strongly connect all parts of the application or to bring workarounds for decoupling in place (e.g. view helpers). Due to the nature of HMVC the application is split into any number of separate parts representing a particular functionality of the application. Separation of concerns is thus easy to achieve and brings several benefits for re-usability and testing aspects.

To bind all parts of the application together the APF used the inversion of control principle expressed by the APF DOM model and implemented by the Page controller. Besides, the URL or the session are also used to allow information exchange and/or storage between parts of the application.

In HMVC-based applications there is not just one model but each MVC unit could have it's own data representation matching the functional requirements. It is not necessarily required to define one model per unit and several units could even share a model in case they are responsible for handling certain parts of the application that deal with the same data structure.

In personalized HMVC-based application there is often a strong need to share essential information such as the current user. For this reason, you may define an application model that will be used by various parts and contains vital information of the application's state - such as the user.

In order to still encapsulate data structures within dedicated HMVC entities it is recommended to use view models. View models represent data structures (a.k.a. DTO) and/or behaviour (a.k.a. domain object) of single MVC units. Besides their data and/or behavioural encapsulation they can be used as a container to generate the view representation of a dedicated MVC element.

Using HMVC in combination with view models not only allows to encapsulate data but also to easily test controllers based on the view model's content rather than based on the HTML output that is generated. This chapter describes the usage of the view model concept within HMVC in depth to enable you writing clean and reusable applications.

In order to fully capture the components that are used within this chapter it is recommended to read the following chapter before:

2. Using view models

The following chapters will describe usage of the view model concept by two different scenarios:

  • Filling a template with data from an application entity.
  • Displaying repetitive data via a template and with the iterator.

As mentioned above view models are any PHP class that encapsulates data from your application in any kind - e.g. a user's address or a web page teaser. Models solely containing data are often referred to as DTOs. Models that contain business logic are considered domain objects.

In general, view models are objects that are directly used within an HMVC unit to generate HTML output. It may be handy to directly use objects returned by an O/R mapper as view models since they already represent the data that is processed by a particular part of an application.

Besides the strong data tying using view models has a positive impact on the size of the controller implementation as view logic can be encapsulated nicely.

Please note that models represented by arrays or array access implementations are prone to errors during access and break IDE and static code analysis support. For this reason, the APF does explicitly not recommend to use arrays or array access implementations for any kind of models.

2.1. Displaying entities with templates

Given the use case that an application should display the user's address the appropriate data could be encapsulated in the following entity:

PHP code
class Address { public function getStreet() { } public function getStreetNumber() { } public function getZip() { } public function getCity() { } public function getCountry() { } }

An instance of this class may be returned by an O/R mapper or may be created within the controller manually. To keep things simple the following code assumes that method getAddress() returns an instance.

To display the address object you could use the following template:

APF template
<html:template name="user-address"> <p> ${street} ${street-number} </p> <p> ${zip} ${city} </p> <p> ${country} </p> </html:template>

To display the entity you could use the following controller code:

PHP code
class AddressController extends BaseDocumentController { public function transformContent() { $tmpl = $this->getTemplate('user-address'); $address = $this->getAddress(); $tmpl->setPlaceHolder('street', $address->getStreet()); $tmpl->setPlaceHolder('street-number', $address->getStreetNumber()); $tmpl->setPlaceHolder('zip', $address->getZip()); $tmpl->setPlaceHolder('city', $address->getCity()); $tmpl->setPlaceHolder('country', $address->getCountry()); $tmpl->transformOnPlace(); } }

This has several disadvantages: the controller code gets fat and contains a lot of view-centric logic (e.g. displaying data) and separation between real view logic and just displaying data according to the MVC pattern gets lost.

For this reason, it is recommended to implement the controller in a more view model-based style as we already have a model Address that represents the data to display. For this reason, we could simplify the controller as follows:

PHP code
class AddressController extends BaseDocumentController { public function transformContent() { $address = $this->getAddress(); $this->getTemplate('user-address') ->setData('address', $address) ->transformOnPlace(); } }

To bring the optimization in place the view template needs some adaption using APF's extended templating functionality:

APF template
<html:template name="user-address"> <p> ${address->getStreet()} ${address->getStreetNumber()} </p> <p> ${address->getZip()} ${address->getCity()} </p> <p> ${address->getCountry()} </p> </html:template>
In case no model is available you may nevertheless use or change to the view model concept as it also applies to arrays. Admittedly, arrays are no real data encapsulation and prone to errors during access but could be an appropriate tool to enhance your application. Using arrays you could still keep your controller simple:
PHP code
class AddressController extends BaseDocumentController { public function transformContent() { $address = [ 'street' => '...', 'street-number' => '...', 'zip' => '...', 'city' => '...', 'country' => '...' ]; $this->getTemplate('user-address') ->setData('address', $address) ->transformOnPlace(); } }
The template needs to be adapted as follows according to Extended template functionality:
APF template
<html:template name="user-address"> <p> ${address['street']} ${address['street-number']} </p> <p> ${address['zip'] ${address['city']} </p> <p> ${address['country']} </p> </html:template>

The same approach can be used to display close-to-static content such as web page teasers with partly dynamic data (e.g. a formatted date). Assuming that your data is coming from any kind of CMS implementation and method getContent() returns an associative array with identifiers referring to editorial form fields and their respective CMS content. In such kind of situations you can still apply the view model concept by wrapping the data into a view model with getters for each field.

The following code box contains a view model implementation that both covers CMS data as well as the dynamic parts (formatted date):

PHP code
namespace VENDOR\..\model; class Teaser { private $data; public function __construct(array $cmsData = []) { $this->data = $cmsData; } public function getHeadline() { return $this->data['headline']; } public function getText() { return $this->data['text']; } public function getDate() { return (new \DateTime())->format('d.m.Y'); } }

Using the new model you can set up your controller as follows:

PHP code
namespace VENDOR\..\controller; use APF\core\pagecontroller\BaseDocumentController; use VENDOR\..\model\Teaser; class TeaserController extends BaseDocumentController { public function transformContent() { $model = new Teaser($this->getContent()); $this->getTemplate('teaser')->setData('content', $model)->transformOnPlace(); } }

The template necessary for displaying the teaser could be the following:

APF template
<html:template name="teaser"> <h2> ${content->getHeadline()} (${content->getDate()}) </h2> <p> ${content->getText()} </p> </html:template>

2.2. Displaying lists

Displaying lists is quite similar to displaying simple entities. The view model concept can be applied the same way and reduces the lines of code of your controller.

The following chapters show how to display a list of users along with some key information. To keep things easy, loading the user list is delegated to the UmgtManager shipped with the APF. As a view model class UmgtUser is used that is also delivered with the APF.

As mentioned in chapter 2 you can basically use any PHP class as your view model and even arrays could be a less favorable solution. In case we are using an existing business component such as the UmgtManager that delivers it's own set of domain objects we are already half way done.

For details on the UmgtManager please refer to chapter .

2.2.1. Using templates

Within this chapter, a simple <html:template />-Tag should be used to display the user list. This certainly implies more logic to be put into the controller, however the view logic can be encapsulated very well to still be able to guarantee excellent testability.

Based on the UmgtManager-API displaying a list of users can be implemented as follows:

PHP code
namespace VENDOR\..\controller; use APF\core\pagecontroller\BaseDocumentController; use APF\modules\usermanagement\biz\model\UmgtUser; use APF\modules\usermanagement\biz\UmgtManager; class UserListController extends BaseDocumentController { public function transformContent() { $umgt = $this->getUmgtManager(); $users = $umgt->getPagedUserList(); $this->displayUsers($users); } protected function displayUsers(array $users = []) { foreach($users as $user) { $this->setPlaceHolder('users', $this->displayUser($user), true); } } protected function displayUser(UmgtUser $user) { return $this->getTemplate('user') ->setData('user', $user) ->transformTemplate(); } /** * @return UmgtManager */ protected function getUmgtManager() { ... } }

The following code box contains the corresponding view template:

APF template
<ul> ${users} </ul> <html:template name="user"> <li> <p> ${user->getDisplayName()} </p> <p> ${user->getStreetName()}, ${user->getStreetNumber()} </p> <p> ${user->getZIPCode()}, ${user->getCity()} </p> </li> </html:template>

Without a doubt, this approach introduces some more view logic into the controller compared to the implementation in chapter 2.1. However there is an elegant solution to remove the logic again by using a view component that takes care of iterating over the list and displaying the entries: the iterator tag. For details, please have a look at the next chapter.

In case an existing model does not contain all required functionality to display the desired information there are two options: generate content within the controller or extend the model. To separate view and controller logic it is recommended to extend the model.

Assuming that UmgtUser does not provide an option to display an obfuscated e-mail address you could easily extend the class as follows:

PHP code
namespace VENDOR\..\model; use APF\modules\usermanagement\biz\model\UmgtUser; class UserViewModel { /** * @var $user UmgtUser */ private $user; public function __construct(UmgtUser $user) { $this->user = $user; } public function getObfuscatedEmail() { return preg_replace('/(.+){4}(.+)@(.+)\.(.+)/', '****$2@$3.**', $this->user->getEMail()); } public function __call($name, array $arguments) { if (method_exists($this->user, $name)) { return $this->user->{$name}(); } throw new InvalidArgumentException('No such method "' . $name . '()" in class "' . get_class($this->user) . '"!'); } }
Please note that using __call() is just used to keep this example clear. The APF team does explicitly not recommend to use magic methods as this will prevent any sort of auto complete, highlighting, and documentation within your IDE!

The controller then needs to be slightly adapted to display the obfuscated e-mail address:

PHP code
namespace VENDOR\..\controller; use APF\core\pagecontroller\BaseDocumentController; use APF\modules\usermanagement\biz\model\UmgtUser; use APF\modules\usermanagement\biz\UmgtManager; class UserListController extends BaseDocumentController { ... protected function displayUser(UmgtUser $user) { return $this->getTemplate('user') ->setData('user', new UserViewModel($user)) ->transformTemplate(); } ... }

With your template you can now easily use the getObfuscatedEmail() method just as any other method:

APF template
<ul> ${users} </ul> <html:template name="user"> <li> <p> ${user->getDisplayName()} </p> <p> ${user->getStreetName()}, ${user->getStreetNumber()} </p> <p> ${user->getZIPCode()}, ${user->getCity()} </p> <p> ${user->getObfuscatedEmail()} </p> </li> </html:template>

2.2.2. Using the iterator

The iterator tag is a view component based on the APF DOM tree concept represented by a tag that provides an API to inject the list to display. It takes responsibility for templating and displaying content including static surrounding content (e.g. start and end of a list). This component is ideal to be used with the view model concept as it allows you to keep your controller clean and small.

In order to display the list of users from chapter 2.2.1 the templates needs some adaption:

APF template
<html:iterator name="users"> <ul> <iterator:item> <li> <p> ${user->getDisplayName()} </p> <p> ${user->getStreetName()}, ${user->getStreetNumber()} </p> <p> ${user->getZIPCode()}, ${user->getCity()} </p> </li> </iterator:item> </ul> </html:iterator>

The controller implementation is now really reduced to controller logic according to the MVC pattern:

PHP code
namespace VENDOR\..\controller; use APF\core\pagecontroller\BaseDocumentController; use APF\modules\usermanagement\biz\model\UmgtUser; use APF\modules\usermanagement\biz\UmgtManager; class UserListController extends BaseDocumentController { public function transformContent() { $umgt = $this->getUmgtManager(); $users = $umgt->getPagedUserList(); $this->getIterator('users') ->fillDataContainer($users) ->transformOnPlace(); } /** * @return UmgtManager */ protected function getUmgtManager() { ... } }

2.2.3. Using a custom tag

In case the HtmlIteratorTag is not sufficient to display a list of data within your application you can easily create a custom tag that displays your data based on the <html:template /> tag. Details on tag implementation can be taken from chapter Implementation of tags.

Assuming that the user's address is optional during registration it might make sense to not display the address block in case no data is available. To easily create the desired output the template could be structured as follows:

APF template
<user:list name="users"> <ul> <html:template name="user"> <li> <p> ${user->getDisplayName()} </p> ${user-address} <html:template name="address"> <p> ${user->getStreetName()}, ${user->getStreetNumber()} </p> <p> ${user->getZIPCode()}, ${user->getCity()} </p> </html:template> </li> </html:template> </ul> </user:list>

The <user:list /> tag consists of an inner HTML part that takes static HTML and the outer <html:template /> tag representing one user entry. Ths inner <html:template /> tag represents the user's address. In case the user has address data available the content of the inner <html:template /> is injected in tho the ${user-address} place holder.

The following code box containing the implementation of the <user:list /> tag. Please note that the software design of the tag can be adapted in any kind matching your requirements.

PHP code
namespace VENDOR\..\taglib; use APF\core\pagecontroller\Document; use APF\core\pagecontroller\TemplateTag; use APF\modules\usermanagement\biz\model\UmgtUser; class UserListTag extends Document { /** * @var $users UmgtUser[] */ private $users; public function setList(array $users = []) { $this->users = $users; return $this; } public function onParseTime() { $this->extractTagLibTags(); } public function transform() { /* @var $template TemplateTag */ $template = $this->getChildNode('name', 'user', 'APF\core\pagecontroller\Template'); $html = ''; foreach ($this->users as $user) { // inject user to allow access values via the extended templating syntax $template->setData('user', $user); // check on available address $street = $user->getStreetName(); $number = $user->getStreetNumber(); $zip = $user->getZIPCode(); $city = $user->getCity(); if (!empty($street) && !empty($number) && !empty($zip) && !empty($city)) { /* @var $address TemplateTag */ $address = $template->getChildNode('name', 'user', 'APF\core\pagecontroller\Template'); // inject user to allow access values via the extended templating syntax $address->setData('user', $user); // fill place holder with address representation $template->setPlaceHolder('user-address', $address->transformTemplate()); } $html .= $template->transform(); } return $html; } }

The controller code is just as simple as with using the iterator:

PHP code
namespace VENDOR\..\controller; use APF\core\pagecontroller\BaseDocumentController; use APF\modules\usermanagement\biz\model\UmgtUser; use APF\modules\usermanagement\biz\UmgtManager; class AlternativeUserListController extends BaseDocumentController { public function transformContent() { $umgt = $this->getUmgtManager(); $users = $umgt->getPagedUserList(); $this->getChildNode('name', 'users', 'VENDOR\..\taglib\UserListTag')->setList($users); } /** * @return UmgtManager */ protected function getUmgtManager() { ... } }

As the <user:list /> tag implements the transform() method directly returning the desired list there is no need for an additional transformOnPlace() call as with the <html:template /> tag.

Comments

Do you want to add a comment to the article above, or do you want to post additional hints? So please click here. Comments already posted can be found below.
« 1   »
Entries/Page: | 5 | 10 | 15 | 20 |
1
cialis 09.12.2016, 11:27:11
You should not use generic cialis without a perscription together with any other treatments for erectile dysfunction.
2
viagra 12.10.2016, 10:15:39
This is a general rule for all Australian buy viagra online pills.
3
herbal_viagra 09.09.2016, 11:09:48

Estonia remained its cantonment for actor duties, herbal viagra usa online.