Developers/Html frontend/Implement new components

From Aimeos documentation

Developers
Other languages:
English 100%


2018.x+ version

In rare cases you might want to create a new shop component when none of the existing ones really fits into what you need. It's rather easy to create one but the adapters for the frameworks and the applications need to integrate your new component to utilize your new component. It's generally a good idea to discuss a new component in the Aimeos forum first and contribute your code to the official core. Otherwise, you would have to do everything yourself.

This article highlights the differences of a component compared to a subpart for an existing shop component. Please read the article about implementing a new subpart first for a decent understanding of the basics.

Class structure

Because a component must implement the same interface as all subparts and thus, also extends from the same parent class, the methods that have to be implemented are the same:

  1. namespace Aimeos\Client\Html\Catalog\Detail;
  2.  
  3. class Standard
  4. 	extends \Aimeos\Client\Html\Common\Client\Factory\Base
  5. 	implements Aimeos\Client\Html\Common\Client\Factory\Iface
  6. {
  7. 	private $subPartPath = 'client/html/catalog/detail/standard/subparts';
  8. 	private $subPartNames = array();
  9. 	private $tags = array();
  10. 	private $expire;
  11. 	private $view;
  12.  
  13.  
  14. 	public function getBody( $uid = '' )
  15. 	{
  16. 	}
  17.  
  18. 	public function getHeader( $uid = '' )
  19. 	{
  20. 	}
  21.  
  22. 	public function getSubClient( $type, $name = null )
  23. 	{
  24. 	}
  25.  
  26. 	public function process()
  27. 	{
  28. 	}
  29.  
  30. 	protected function getSubClientNames()
  31. 	{
  32. 	}
  33. }

Differences arise from the required code inside these methods as they have to care about caching (if you want to) and exception handling too. A component can implement the same optional methods as any subpart. For a detailed description of these methods, please refer to the article about creating new subparts.

The getSubClient() and _getSubClientNames() methods are also exactly the same as in any other subpart and won't be described in this article again.

process() method

This method is not affected by caching at all because its purpose is to execute code once during each request. The difference to process() methods of subclients is only that you need to catch thrown exceptions and assign error messages to the view if necessary. If you don't need to process any input in your new component, you can simply copy & paste the code below into your new class:

  1. public function process()
  2. {
  3. 	$context = $this->getContext();
  4. 	$view = $this->getView();
  5.  
  6. 	try
  7. 	{
  8. 		// your required code
  9. 		parent::process();
  10. 	}
  11. 	catch( \Aimeos\Client\Html\Exception $e )
  12. 	{
  13. 		$error = array( $context->getI18n()->dt( 'client/html', $e->getMessage() ) );
  14. 		$view->detailErrorList = $view->get( 'detailErrorList', array() ) + $error;
  15. 	}
  16. 	catch( \Aimeos\Controller\Frontend\Exception $e )
  17. 	{
  18. 		$error = array( $context->getI18n()->dt( 'controller/frontend', $e->getMessage() ) );
  19. 		$view->detailErrorList = $view->get( 'detailErrorList', array() ) + $error;
  20. 	}
  21. 	catch( \Aimeos\MShop\Exception $e )
  22. 	{
  23. 		$error = array( $context->getI18n()->dt( 'mshop', $e->getMessage() ) );
  24. 		$view->detailErrorList = $view->get( 'detailErrorList', array() ) + $error;
  25. 	}
  26. 	catch( Exception $e )
  27. 	{
  28. 		$context->getLogger()->log( $e->getMessage() . PHP_EOL . $e->getTraceAsString() );
  29.  
  30. 		$error = array( $context->getI18n()->dt( 'client/html', 'A non-recoverable error occured' ) );
  31. 		$view->detailErrorList = $view->get( 'detailErrorList', array() ) + $error;
  32. 	}
  33. }

The only thing you have to adapt is the name of the error list assigned to the view and it should be named after your class name to something like $view->...ErrorList. The cascade of catch() statements ensures that all exceptions are caught and all error messages translated that are passed to the view and shown to the customers. Unspecific execptions are logged and only a generic error message is shown in the front-end to avoid giving away sensitive information. You need to print these error messages in your component view which is described in the next section.

Display error messages

The execptions caught in the methods and assigned to the view should be shown to the customers so they know what's going on. For this, you need to add a snippet similar to this one at the top of your component body view:

  1. <?php if( isset( $this->detailErrorList ) ) : ?>
  2. 	<ul class="error-list">
  3. <?php foreach( (array) $this->detailErrorList as $errmsg ) : ?>
  4. 		<li class="error-item"><?php echo $this->encoder()->html( $errmsg ); ?></li>
  5. <?php endforeach; ?>
  6. 	</ul>
  7. <?php endif; ?>

Remember to adapt the view parameter name to the name you've used in the methods of your new component class. These few lines of code create an HTML block that will contain all error messages and which will be styled by the used theme, so you don't have to care about this.

No caching

If the output of your new component isn't suitable for caching, things are a bit easier for the getBody() and getHeader() method because you only need to catch the exceptions as for the process() method.

getBody()

Very similar to the process() method, you have to catch all execptions, translate them if possible and assign them to the view so the visitors are notified about the errors. The method example below only contains the parts that are different to those of the getBody() method from the subpart clients:

  1. public function getBody( $uid = '' )
  2. {
  3. 	$context = $this->getContext();
  4. 	$view = $this->getView();
  5.  
  6. 	try
  7. 	{
  8. 		if( !isset( $this->view ) ) {
  9. 			$view = $this->view = $this->getObject()->addData( $view, $this->tags, $this->expire );
  10. 		}
  11.  
  12. 		$html = '';
  13. 		foreach( $this->getSubClients() as $subclient ) {
  14. 			$html .= $subclient->setView( $view )->getBody( $uid );
  15. 		}
  16. 		$view->detailBody = $html;
  17. 	}
  18. 	catch( \Aimeos\Client\Html\Exception $e )
  19. 	{
  20. 		$error = array( $context->getI18n()->dt( 'client/html', $e->getMessage() ) );
  21. 		$view->detailErrorList = $view->get( 'detailErrorList', array() ) + $error;
  22. 	}
  23. 	catch( \Aimeos\Controller\Frontend\Exception $e )
  24. 	{
  25. 		$error = array( $context->getI18n()->dt( 'controller/frontend', $e->getMessage() ) );
  26. 		$view->detailErrorList = $view->get( 'detailErrorList', array() ) + $error;
  27. 	}
  28. 	catch( \Aimeos\MShop\Exception $e )
  29. 	{
  30. 		$error = array( $context->getI18n()->dt( 'mshop', $e->getMessage() ) );
  31. 		$view->detailErrorList = $view->get( 'detailErrorList', array() ) + $error;
  32. 	}
  33. 	catch( Exception $e )
  34. 	{
  35. 		$context->getLogger()->log( $e->getMessage() . PHP_EOL . $e->getTraceAsString() );
  36.  
  37. 		$error = array( $context->getI18n()->dt( 'client/html', 'A non-recoverable error occured' ) );
  38. 		$view->detailErrorList = $view->get( 'detailErrorList', array() ) + $error;
  39. 	}
  40.  
  41. 	$tplconf = 'client/html/catalog/detail/standard/template-body';
  42. 	$default = 'catalog/detail/body-default.php';
  43.  
  44. 	$html = $view->render( $view->config( $tplconf, $default ) );
  45.  
  46. 	return $html;
  47. }

Don't forget to adapt the "detailErrorList" view variable to your component name and it must be the same as used in your error displaying code of your view.

In doubt, have a look into a full example of a working getBody() component method without caching.

getHeader()

Compared to the getBody() method, getHeader() is really simple because there's no way to display any errors that have been occured. Instead, you have to log them all and return no output. The code example below only contains the lines that are different from the getHeader() method of a subpart client:

  1. public function getBody( $uid = '' )
  2. {
  3. 	$view = $this->getView();
  4.  
  5. 	try
  6. 	{
  7. 		if( !isset( $this->view ) ) {
  8. 			$view = $this->view = $this->getObject()->addData( $view, $this->tags, $this->expire );
  9. 		}
  10.  
  11. 		$html = '';
  12. 		foreach( $this->getSubClients() as $subclient ) {
  13. 			$html .= $subclient->setView( $view )->getHeader( $uid );
  14. 		}
  15. 		$view->detailHeader = $html;
  16. 	}
  17. 	catch( Exception $e )
  18. 	{
  19. 		$this->getContext()->getLogger()->log( $e->getMessage() . PHP_EOL . $e->getTraceAsString() );
  20. 		return;
  21. 	}
  22.  
  23. 	$tplconf = 'client/html/catalog/detail/standard/template-header';
  24. 	$default = 'catalog/detail/header-default.php';
  25.  
  26. 	$html = $view->render( $view->config( $tplconf, $default ) );
  27.  
  28. 	return $html;
  29. }

In doubt, have a look into a full example of a working getHeader() component method without caching.

With content caching

Components that can cache its output are extremely fast because once the content is generated and stored somewhere, it can be retrieved within milliseconds and directly pushed to the browser. The downside is that some additional code is needed.

getBody()

When caching comes into play, the first thing you have to think about is: What does your output depend on? Usually, two external sources can influence what content needs to be generated and those are request parameters and configuration settings. Both has to be in some way part of the key to the generated content and each value that is different will generate a new content tol be cached.

In an Aimeos component, it's not difficult to encode both sources into a key which is used determine the correct content. All parameters are prefixed depending on their source, e.g. parameters that are used for the catalog filter start with a "f", all catalog list parameters with a "l" and the parameters for the catalog detail page with a "d". You only need to specify the prefixes of the parameters your component listens to. The configuration settings are build hierarchically, so the prefix that is shared by all subclients of your component is required. The rest is simply calling the getCached() and setCached() method from the parent class.

  1. public function getBody( $uid = '' )
  2. {
  3. 	$prefixes = array( 'd' );
  4. 	$confkey = 'client/html/catalog/detail';
  5.  
  6. 	if( ( $html = $this->getCached( 'body', $uid, $prefixes, $confkey ) ) === null ) )
  7. 	{
  8. 		// code is the same as for the uncached variant
  9.  
  10. 		$html = $view->render( $view->config( $tplconf, $default ) );
  11.  
  12. 		$this->setCached( 'body', $uid, $prefixes, $confkey, $html, $this->tags, $this->expire );
  13. 	}
  14. 	else
  15. 	{
  16. 		$html = $this->modifyBody( $html, $uid );
  17. 	}
  18.  
  19. 	return $html;
  20. }

There's one thing to note when caching content: Sometimes, a subpart can't be cached because it depends on the sessions or cookies of the customers. In this case the whole content wouldn't be cachable at all but fortunately, there's a solution for this problem. The modifyBody() method allows any subclient to replace a section in the cached content. The details for this are described in the article about creating new subparts. The important thing here is to call the modifyBody() method provided by the parent class after sucessfully retrieving the cached content.

In doubt, have a look into a full example of a working getBody() component method which implements caching.

getHeader()

For getHeader(), implementing caching is very similar to the implementation of getBody(). You also have to specify which parameters are used in your component and what's the shared configuration prefix for all settings. The calls to getCached() and setCached() only differ by the value of the first parameter which in "header" in this case to ensure that the content is stored for the header, not the body.

  1. public function getHeader( $uid = '' )
  2. {
  3. 	$prefixes = array( 'd' );
  4. 	$confkey = 'client/html/catalog/detail';
  5.  
  6. 	if( ( $html = $this->getCached( 'header', $uid, $prefixes, $confkey ) ) === null )
  7. 	{
  8. 		// same code as for the uncached variant
  9.  
  10. 		$html = $view->render( $view->config( $tplconf, $default ) );
  11.  
  12. 		$this->setCached( 'header', $uid, $prefixes, $confkey, $html, $this->tags, $this->expire );
  13. 	}
  14. 	else
  15. 	{
  16. 		$html = $this->modifyHeader( $html, $uid );
  17. 	}
  18.  
  19. 	return $html;
  20. }

In getHeader() the exception handling is much simpler as you only need to log away any exception. Only keep in mind that you also need to call the modifyHeader() method after sucessfully retrieving the cached content to give subclients of your component the chance to replace their sections with their new content.

In doubt, have a look into a full example of a working getHeader() component method which implements caching.

Factory class

All components are instantiated by factories which care about creating the HTML client and decorating it with additional functionality added via configuration. The factory class is a rather simple piece of code that contains only a createClient() method:

  1. namespace Aimeos\Client\Html\Catalog\Lists;
  2.  
  3. class Factory
  4. 	extends \Aimeos\Client\Html\Common\Factory\Base
  5. 	implements\Aimeos\Client\Html\Common\Factory\Iface
  6. {
  7. 	public static function createClient( \Aimeos\MShop\Context\Item\Iface $context, array $paths, $name = null )
  8. 	{
  9. 		if( $name === null ) {
  10. 			$name = $context->getConfig()->get( 'client/html/catalog/detail/name', 'Standard' );
  11. 		}
  12.  
  13. 		if( ctype_alnum( $name ) === false )
  14. 		{
  15. 			$classname = is_string( $name ) ? '\\Aimeos\\Client\\Html\\Catalog\\Detail\\' . $name : '<not a string>';
  16. 			throw new \Aimeos\Client\Html\Exception( sprintf( 'Invalid characters in class name "%1$s"', $classname ) );
  17. 		}
  18.  
  19. 		$iface = '\\Aimeos\\Client\\Html\\Iface';
  20. 		$classname = '\\Aimeos\\Client\\Html\\Catalog\\Detail\\' . $name;
  21.  
  22. 		$client = self::createClientBase( $context, $classname, $iface, $templatePaths );
  23. 		$client = self::addClientDecorators( $context, $client, $templatePaths, 'catalog/detail' );
  24.  
  25. 		return $client->setObject( $client );
  26. 	}
  27. }

The code above is a factory for the catalog list client. You can copy the code and replace the "Catalog\Lists" and "catalog/lists" strings with the name of you own component. For example, if you want to create a new "catalog homepage" component, you should replace the strings like this:

Catalog\Lists -> Catalog\Homepage
catalog/lists -> catalog/homepage

Component factories for other purposes can created the same way, e.g. for a "basket upsell" component replace the strings in that way:

Catalog\Lists -> Basket\Upsell
catalog/lists -> basket/upsell

The factory and the default implementation of your component must be saved to the appropriate directory, i.e. to the "./client/html/src/client/html/catalog/homepage" or "./client/html/src/client/html/basket/upsell" directory of your Aimeos extension.