Developers/Html frontend/Create new subparts

From Aimeos documentation

Developers
Other languages:
English 100%


2018.x+ version

Each core component can be divided into one or more subparts which are implemented as HTML clients themselves and they can contain several subparts as well. This article focuses on creating a new HTML client subpart that can be added via configuration between any existing subparts.

Component, level and name

Please keep in mind that a subpart is written for a specific component and for a certain level inside the tree structure. I.e. you can't use a subpart written for one component in another one or move the subpart up and down in the hierarchy solely by configuration. The existing components are located in the client/html/src/Client/Html/ directory and grouped together with similar compenents, e.g. the "Catalog" directory contains all compentents used for the product presentation, among others also the catalog detail component. Let's see what this component consists of:

Core-catalog-detail-structure.png Core-catalog-detail-layout.png

As you can see, it contains some partials (blue) and one subpart (red). The locations of the subpart (and the partials too) depends on where you put them into your template, in this case the catalog/detail/body-default.php template. The naming for the subpart class must correspond to the namespace and directory structure:

Aimeos\Client\Html\Catalog\Detail\Yourpart\Standard

You can rename "Yourpart" to whatever you like but it must consist of characters from a-z and digits from 0-9 only. Furthermore, the first character must be capitalized. The last part ("Standard" in this case) is the name of the implementation because there can be several ones with different code and functionality but the first implementation must always be named "Standard"!

You can also add new subparts to existing subparts that currently don't have one, e.g. to the "Service" subpart. The generated output of your new subpart would be then be added inside resp. at the end of the subpart after you've configured your subpart accordingly.

Basic class structure

All HTML clients, either being a component or a subpart, need to extend from the common "Client\Html\Base" base class and implement the same methods. Especially the getBody() and getHeader() methods are important because they care about generating the HTML, XML, JS or whatever code which is sent to the browser. The skeleton below is from the catalog detail basic subpart and you can use it as reference for your own subpart implementation.

  1. namespace Client\Html\Catalog\Detail\Basic;
  2.  
  3. class Standard
  4. 	extends \Aimeos\Client\Html\Common\Client\Factory\Base
  5. 	implements \Aimeos\Client\Html\Common\Client\Factory\Iface
  6. {
  7. 	/** client/html/catalog/detail/basic/standard/subparts
  8. 	 * List of HTML sub-clients rendered within the catalog detail basic section
  9. 	 *
  10. 	 * The output of the frontend is composed of the code generated by the HTML
  11. 	 * clients. At first, always the HTML code generated by the parent is printed, then
  12. 	 * the HTML code of its sub-clients. The order of the HTML sub-clients
  13. 	 * determines the order of the output of these sub-clients inside the parent
  14. 	 * container. If the configured list of clients is
  15. 	 *
  16. 	 *  array( "subclient1", "subclient2" )
  17. 	 *
  18. 	 * you can easily change the order of the output by reordering the subparts:
  19. 	 *
  20. 	 *  client/html/catalog/detail/basic/standard/subparts = array( "subclient1", "subclient2" )
  21. 	 *
  22. 	 * @param array List of sub-client names
  23. 	 * @since 2016.01
  24. 	 * @category Developer
  25. 	 */
  26. 	private $subPartPath = 'client/html/catalog/detail/basic/standard/subparts';
  27. 	private $subPartNames = array();
  28. 	private $view;
  29.  
  30.  
  31. 	public function getBody( $uid = ' )
  32. 	{
  33. 	}
  34.  
  35. 	public function getHeader( $uid = ' )
  36. 	{
  37. 	}
  38.  
  39. 	public function getSubClient( $type, $name = null )
  40. 	{
  41. 	}
  42.  
  43. 	protected function getSubClientNames()
  44. 	{
  45. 	}
  46. }

The only thing you have to do is to rename the "Basic" and "basic" strings to whatever your subpart should be named. If you want to create a subpart one level below, e.g. for the "Additional", "Basket" or your own new level below an existing subpart, you have to add the names of these levels in the class name, the $subPartPath variable and its documentation. You would replace "Basic" to "Additional\Yourpart" in your class name and "/basic/" to "/additional/yourpart/" in the subPartPath variable for example.

There are two private class variables in this skeleton: $subPartPath and $subPartNames. Both will be used in the getSubClientNames() method explained later. The last class variable $cache is used if you implement the setViewParams() method - otherwise, you can leave it out.

Mandatory methods

getBody()

The getBody() method is usually the most important one because it generates most of the output sent to the browser. It contains only a few lines, everything else is outsourced to other methods. Three optional parameters can be passed to this method:

  • uid (unique identifier if the method is called more than once and the generated output is placed on the same page)
  1. /**
  2.  * Returns the HTML code for insertion into the body.
  3.  *
  4.  * @param string $uid Unique identifier for the output if the content is placed more than once on the same page
  5.  * @return string HTML code
  6.  */
  7. public function getBody( $uid = '' )
  8. {
  9. 	$view = $this->getView();
  10.  
  11. 	$html = '';
  12. 	foreach( $this->getSubClients() as $subclient ) {
  13. 		$html .= $subclient->setView( $view )->getBody( $uid, $tags, $expire );
  14. 	}
  15. 	$view->basicBody = $html;
  16.  
  17. 	/** client/html/catalog/detail/basic/standard/template-body
  18. 	 * Relative path to the HTML body template of the catalog detail basic client.
  19. 	 *
  20. 	 * The template file contains the HTML code and processing instructions
  21. 	 * to generate the result shown in the body of the frontend. The
  22. 	 * configuration string is the path to the template file relative
  23. 	 * to the templates directory (usually in client/html/templates).
  24. 	 *
  25. 	 * You can overwrite the template file configuration in extensions and
  26. 	 * provide alternative templates. These alternative templates should be
  27. 	 * named like the default one but with the string "default" replaced by
  28. 	 * an unique name. You may use the name of your project for this. If
  29. 	 * you've implemented an alternative client class as well, "standard"
  30. 	 * should be replaced by the name of the new class.
  31. 	 *
  32. 	 * @param string Relative path to the template creating code for the HTML page body
  33. 	 * @since 2016.01
  34. 	 * @category Developer
  35. 	 * @see client/html/catalog/detail/basic/standard/template-header
  36. 	 */
  37. 	$tplconf = 'client/html/catalog/detail/basic/standard/template-body';
  38. 	$default = 'catalog/detail/basic-body-default.html';
  39.  
  40. 	return $view->render( $view->config( $tplconf, $default ) );
  41. }

In the "foreach" block the subclients are initialized and their generated output is concatenated. The result is then assigned to the view as "...Body" variable. The prefix should be the name you've given to the subpart and it must not collide with an existing one, so be specific enough!

The next block contains a lot of documentation for the default template configuration and you should adapt this block to your subpart. Especially the first and the last lines are important because references are automatically created in the documentation according to these lines. You should also adapt the @since tag to give developers an hint since when this configuration option is available.

At last, the output is generated by the view according to the given template location which can be overwritten using the config setting in $tplconf. Both, the template configuration key and it's default value must be adapted to the name of your class. The default value consists of the component name (catalog/detail/) and the filename made up of the name of your class (including sub-names, e.g. "additional-text-body-default.html"), the "body" string and the name of your implementation. The complete strings must be in lower-case.

If your subpart client is located at a different level in the hierarchy, you must add all parts of the class name in lower case and separated by slashes (/) like "client/catalog/detail/additional/text/..." for configuration values and in the documentation.

getHeader()

Similar to getBody(), the getHeader() method generates any output that must be part of the header section of the HTML document sent to the browser. The same three optional parameters can be passed to this method:

  • uid (unique identifier if the method is called more than once and the generated output is placed on the same page)
  1. /**
  2.  * Returns the HTML string for insertion into the header.
  3.  *
  4.  * @param string $uid Unique identifier for the output if the content is placed more than once on the same page
  5.  * @return string String including HTML tags for the header
  6.  */
  7. public function getHeader( $uid = '' )
  8. {
  9. 	$view = $this->getView();
  10.  
  11. 	$html = '';
  12. 	foreach( $this->getSubClients() as $subclient ) {
  13. 		$html .= $subclient->setView( $view )->getHeader( $uid, $tags, $expire );
  14. 	}
  15. 	$view->basicHeader = $html;
  16.  
  17. 	/** client/html/catalog/detail/basic/standard/template-header
  18. 	 * Relative path to the HTML header template of the catalog detail basic client.
  19. 	 *
  20. 	 * The template file contains the HTML code and processing instructions
  21. 	 * to generate the HTML code that is inserted into the HTML page header
  22. 	 * of the rendered page in the frontend. The configuration string is the
  23. 	 * path to the template file relative to the templates directory (usually
  24. 	 * in client/html/templates).
  25. 	 *
  26. 	 * You can overwrite the template file configuration in extensions and
  27. 	 * provide alternative templates. These alternative templates should be
  28. 	 * named like the default one but with the string "default" replaced by
  29. 	 * an unique name. You may use the name of your project for this. If
  30. 	 * you've implemented an alternative client class as well, "standard"
  31. 	 * should be replaced by the name of the new class.
  32. 	 *
  33. 	 * @param string Relative path to the template creating code for the HTML page head
  34. 	 * @since 2015.01
  35. 	 * @category Developer
  36. 	 * @see client/html/catalog/detail/basic/standard/template-body
  37. 	 */
  38. 	$tplconf = 'client/html/catalog/detail/basic/standard/template-header';
  39. 	$default = 'catalog/detail/basic-header-default.html';
  40.  
  41. 	return $view->render( $this->config( $tplconf, $default ) );
  42. }

In the getHeader() method, the "foreach" block also retrieves the output from the subclients. It assigns the result to the view as "...Header" variable and its name must not collide with others!

Please include the documentation for the default template configuration here as well and adapt it to your subpart. Especially the first and the last lines as well as the @since tag are important.

The last lines for generating the output by the view are also very similar besides the fact that the string "header" is used instead of "body". You must adapt the template configuration key and it's default value to the name of your class too.

Like for the body, you must add all parts of the class name in lower case and separated by slashes (e.g. "client/catalog/detail/additional/text/...") for configuration values and in the documentation if your subpart client is located at a different level in the hierarchy.

getSubClient()

Each client can have a list of subclients itself and your class is responsible for creating these clients on request. Fortunatly, doing this is very easy because the parent class contains almost all necessary code and you only need to call its method and passing the right values. The getSubClient() method accepts two parameters itself:

  • type (name of the subclient, e.g. for the "catalog/detail/additional" client this would be "attribute", "download", "text" and so on)
  • name (name of the implementation and "Standard" is the standard value if none is given)
  1. /**
  2.  * Returns the sub-client given by its name.
  3.  *
  4.  * @param string $type Name of the client type
  5.  * @param string|null $name Name of the sub-client ("Standard" if null)
  6.  * @return \Aimeos\Client\Html\Iface Sub-client object
  7.  */
  8. public function getSubClient( $type, $name = null )
  9. {
  10. 	return $this->createSubClient( 'catalog/detail/basic/' . $type, $name );
  11. }

This method contains only one line and you have to adapt the first parameter of the createSubClient() call according to your class name. Remember that for clients at a deeper level in the hierarchy, the same as for the other methods applies: You have to extend the prefix string with all class name parts in lower-case separated by slashes (/) and only leave out the "Aimeos\Client\Html" and "Standard" part.

getSubClientNames()

The implementation for the getSubClientNames() helper method is a one-liner too and you can copy and paste this method without any changes. Its intelligence is in the two class variables you've seen in the basic class skeleton which are only referenced here:

  1. /**
  2.  * Returns the list of sub-client names configured for the client.
  3.  *
  4.  * @return array List of HTML client names
  5.  */
  6. protected function getSubClientNames()
  7. {
  8. 	return $this->getContext()->getConfig()->get( $this->subPartPath, $this->subPartNames );
  9. }

The configuration path is assigned to the $subPartPath variable which can be used to overwrite the names of those subparts whose content should be rendered as well. The default names including their order is stored in the array assigned to the $subPartNames class variable. Thus, you are able to provide strong defaults for the subparts and their order, so no configuration is necessary while you make it possible to overwrite this configuration to add new subparts or reorder the existing subparts.

Optional methods

addData()

This method is called in both, the getBody() and getHeader() method and assigns the required data to the given view. The method signature contains three parameters:

  • view (the view object to which the necessary data should be assigned)
  • tags (will contain the list of tags for cache control and cache invalidation afterwards)
  • expire (expiration date in YYYY-MM-DD HH:mm:ss format for the assigned data)
  1. /**
  2.  * Sets the necessary parameter values in the view.
  3.  *
  4.  * @param Aimeos\MW\View\Iface $view The view object which generates the HTML output
  5.  * @param array &$tags Result array for the list of tags that are associated to the output
  6.  * @param string|null &$expire Result variable for the expiration date of the output (null for no expiry)
  7.  * @return Aimeos\MW\View\Iface Modified view object
  8.  */
  9. public function addData( Aimeos\MW\View\Iface $view, array &$tags = array(), &$expire = null )
  10. {
  11. 	// use controller or manager to retrieve required data
  12.  
  13. 	$this->addMetaItem( $itemOrItemList, $expire, $tags );
  14.  
  15. 	$view->yourpartVarname = ...
  16.  
  17. 	return $view;
  18. }

Usually, some data is fetched from the database via the frontend controller or directly by one of the existing managers. Don't retrieve anything directly from the database without depending at a manager because this prevents implementations using alternative data sources!

For cache control there's a nice helper functions that make calculation of the expiration date and attaching the necessary tags very easy. The addMetaItem() method accepts an item object or a list of item objects that either provide a getStartDate() and getEndDate() method themselves or contains referenced items that implements those methods. It adds the expiration time and the tags in the second and third variable passed to this method.

Necessary data can be assigned directly to the view because the magic methods __get() and __set() care about their handling in the view class.

modifyBody()

When caching the complete output of a component, sooner or later there will be one subpart that depends on data that is different for every user, like something in their session. It's also conceivable that a parameter can have so many different values that hundreds or thousands of different cache entries for almost the same content need to be generated. In both cases it's more efficient to generate only the subpart that depends on these data for every request and replacing this section in the cached component output.

To replace a section of the cached body content, the modifyBody() method is called each time after an entry is retrieved from the cache, so the subclients are able to cut out the old content and paste in the new one. The method needs two parameters in return:

  • content (the cached content including the begin and end marker of the volatile section in the content)
  • uid (unique identifier if the content is placed more than once on the same page using different configuration values)
  1. /**
  2.  * Modifies the cached body content to replace content based on sessions or cookies.
  3.  *
  4.  * @param string $content Cached content
  5.  * @param string $uid Unique identifier for the output if the content is placed more than once on the same page
  6.  * @return string Modified body content
  7.  */
  8. public function modifyBody( $content, $uid )
  9. {
  10. 	return $this->replaceSection( $content, $this->getBody( $uid ), 'catalog.detail.navigator' );
  11. }

This line of code inside the method body is an advanced version of the PHP str_replace() function. It replaces the old part identified by the last parameter with the newly generated output of the getBody() method in the given content from the cache. The important part is the name of the marker passed as last parameter: It's a unique string for the implemented client and should resemble the parts identifiying your class. In the template file the marker is prefixed by "<!-- " and postfixed by " -->" to create an HTML/XML comment for a valid file. The template for the catalog stage navigator contains a working example for such a marker.

The marker in the template must be always part of the cached output, so always add it unconditionally to your template! Otherwise, new content can't be added to the cache entry and you won't see the expected part in the resulting HTML page.

modifyHeader()

Like for the body, the same is also possible for the header: Replace dynamic parts of a cached entry without loosing the ability of caching at all. The modifyHeader() method is also called after the cached header entry is retrieved and needs the same parameters:

  • content (the cached content including the begin and end marker of the volatile section in the header)
  • uid (unique identifier if the content is placed more than once in the header of the same page using different configuration values)
  1. /**
  2.  * Modifies the cached header content to replace content based on sessions or cookies.
  3.  *
  4.  * @param string $content Cached content
  5.  * @param string $uid Unique identifier for the output if the content is placed more than once on the same page
  6.  * @return string Modified body content
  7.  */
  8. public function modifyHeader( $content, $uid )
  9. {
  10. 	return $this->replaceSection( $content, $this->getHeader( $uid ), 'catalog.stage.navigator' );
  11. }

In the given content file the marker must be prefixed by "<!-- " and postfixed by " -->" to create a valid HTML comment and the same as for modifyBody() applies.

Keep in mind that here the marker must be also always added unconditionally to your template to be able to replace sections with no output before!

process()

In subparts that are not only retrieving already existing data but actively modifying something, it's required to do this only once per request and before the getBody() or getHeader() methods are called. For this case, a process() method can be implemented in your subpart class that e.g. adds something to the basket or recalculates an existing value. It doesn't need any parameter but the view is available via $this->getView() as well as the complete context by $this->getContext().

  1. /**
  2.  * Processes the input, e.g. store given values.
  3.  * A view must be available and this method doesn't generate any output
  4.  * besides setting view variables.
  5.  */
  6. public function process()
  7. {
  8. 	// do something only once
  9. 	parent::process();
  10. }

Examples for classes that uses the process() method to execute code only once are the standard basket and the class for the last seen products.

Always call parent::process() at the end of the method to execute the process() methods of the subclients as well! Otherwise, you would stop the processing at that point for all subclients below yours.