Difference between revisions of "Developers/Library/Extend managers items"

From Aimeos documentation

< Developers
(Easy way)
(Easy way)
 
(One intermediate revision by the same user not shown)
Line 20: Line 20:
 
namespace Aimeos\MShop\Customer\Manager\Decorator;
 
namespace Aimeos\MShop\Customer\Manager\Decorator;
 
   
 
   
class Myproject extends \Aimeos\MShop\Common\Manager\Decorator
+
class Myproject extends \Aimeos\MShop\Common\Manager\Decorator\Base
 
{
 
{
 
     public function saveItem( \Aimeos\MShop\Common\Item\Iface $item, $fetch = true )
 
     public function saveItem( \Aimeos\MShop\Common\Item\Iface $item, $fetch = true )
Line 45: Line 45:
 
namespace Aimeos\MShop\Customer\Manager\Decorator;
 
namespace Aimeos\MShop\Customer\Manager\Decorator;
 
   
 
   
class Myproject extends \Aimeos\MShop\Common\Manager\Decorator
+
class Myproject extends \Aimeos\MShop\Common\Manager\Decorator\Base
 
{
 
{
 
     private $attr = [
 
     private $attr = [

Latest revision as of 17:25, 5 November 2019

<languages/>


Usually, it's only rarely necessary to extend existing managers and items because most of the data can be associated to items via the list tables. The data can then be stored as attributes, texts or properties (in case of product items) instead.

To extend or overwrite existing classes, you have to

  • Extend the database table (refer to schema updates)
  • Create a new class in your project specific Aimeos extension
  • Store the class file in the ./lib/custom/src/ directory
  • Use the same directory structure as for the original class below the ./src directory
  • Give it a different name as the original class, e.g. Myproject
  • Extend from the original class

Easy way

Implementing decorators is a great way to dynamically extend managers without inheriting from the existing manager class. Instead, you can wrap multiple decorators around a manager object like the layers of an onion. In each decorator, you can implement additional code that changes the parameters or the result of the manager method or performs additional actions, e.g.

  1. namespace Aimeos\MShop\Customer\Manager\Decorator;
  2.  
  3. class Myproject extends \Aimeos\MShop\Common\Manager\Decorator\Base
  4. {
  5.     public function saveItem( \Aimeos\MShop\Common\Item\Iface $item, $fetch = true )
  6.     {
  7.         // do something before
  8.         $result = $this->getManager()->saveItem( $item, $fetch );
  9.         // do somthing afterwards
  10.         return $result;
  11.     }
  12. }

Since 2019.10 it's possible to add new columns by extending the database table and a manager decorator only. The example will use the customer manager but you can extend all managers in the same way.

With decorators, you can also add functionality to your managers without the need to add new columns

You need to create a decorator for the customer manager that will care about the new column(s). A decorator has the advantage that you can add multiple decorators on top of the manager and 3rd party extensions can do that too.

The new column must be nullable, that means it must allow NULL values!

Your new customer manager decorator class should be located in ./<yourext>/lib/custom/src/MShop/Customer/Manager/Decorator/Myproject.php and should contain:

  1. namespace Aimeos\MShop\Customer\Manager\Decorator;
  2.  
  3. class Myproject extends \Aimeos\MShop\Common\Manager\Decorator\Base
  4. {
  5.     private $attr = [
  6.         'mycolumn' => [
  7.             'code' => 'mycolumn',
  8.             'internalcode' => 'mcus."mycolumn"',
  9.             'label' => 'My new column',
  10.             'type' => 'string',
  11.             'internaltype' => \Aimeos\MW\DB\Statement\Base::PARAM_STR,
  12.         ],
  13.     ];
  14.  
  15.     public function getSaveAttributes()
  16.     {
  17.         return parent::getSaveAttributes() + $this->createAttributes( $this->attr );
  18.     }
  19.  
  20.     public function getSearchAttributes( $sub = true )
  21.     {
  22.         return parent::getSearchAttributes( $sub ) + $this->createAttributes( $this->attr );
  23.     }
  24. }

The $attr array contains a definition of the column(s) and the requirements are:

  1. The key must be exactly the name of your column in the database (here: 'mycolumn')
  2. The value for code must be exactly the name of your column in the database (here: 'mycolumn')
  3. The value for internalcode must be SQL alias of the table, a dot and the column name enclosed in quotation marks (here: 'mcus."mycolumn"')

For the SQL alias you have to use, please have a at into the SELECT statement of the domain in the configuration.

The other values in the $attr array are optional:

  • label is an arbitrary string that is shown in admin interface in the search bar
  • type is the type of the values that the column contains (default: 'string')
  • internaltype is the database type constant (default: PARAM_STR) and can be:
    • \Aimeos\MW\DB\Statement\Base::PARAM_STR
    • \Aimeos\MW\DB\Statement\Base::PARAM_INT
    • \Aimeos\MW\DB\Statement\Base::PARAM_FLOAT
    • \Aimeos\MW\DB\Statement\Base::PARAM_BOOL

As last step, you have to add your decorator name to the list of local decorators for that manager in the ./<yourext>/config/mshop.php file. For the customer manager it's:

  1. return [
  2.     'customer' => [
  3.         'manager' => [
  4.             'decorators' => [
  5.                 'local' => ['Myproject']
  6.             ]
  7.         ]
  8.     ]
  9. ];

Custom way

To have full control over the implementation including those of the item, you need do a bit more.

Items

The skeleton for your new product item class would be located in ./<yourext>/lib/custom/src/MShop/Product/Item/Myproject.php and should contain:

  1. namespace Aimeos\MShop\Product\Item;
  2.  
  3. class Myproject extends Standard
  4. {
  5.     private $myvalues;
  6.  
  7.     public function __construct( array $values, ... )
  8.     {
  9.         parent::__construct( $values, ... )
  10.         $this->myvalues = $values;
  11.     }
  12.  
  13.     public function getMyId()
  14.     {
  15.         if( isset( $this->myvalues['myid'] ) ) {
  16.             return (string) $this->myvalues['myid'];
  17.         }
  18.         return '';
  19.     }
  20.  
  21.     public function setMyId( $val )
  22.     {
  23.         if( (string) $val !== $this->getMyId() )
  24.         {
  25.             $this->values['myid'] = (string) $myid;
  26.             $this->setModified();
  27.         }
  28.         return $this;
  29.     }
  30.  
  31.     public function fromArray( array $list )
  32.     {
  33.         $unknown = [];
  34.         $list = parent::fromArray( $list );
  35.  
  36.         foreach( $list as $key => $value )
  37.         {
  38.             switch( $key )
  39.             {
  40.                 case 'myid': $this->setMyId( $value ); break;
  41.                 default: $unknown[$key] = $value;
  42.             }
  43.         }
  44.  
  45.         return $unknown;
  46.     }
  47.  
  48.     public function toArray( $private = false )
  49.     {
  50.         $list = parent::toArray( $private );
  51.  
  52.         if( $private === true ) {
  53.             $list['myid'] = $this->getMyId();
  54.         }
  55.  
  56.         return $list;
  57.     }
  58. }

The fromArray() and toArray() methods are important if you need to manage your new properties via the admin interface. By using calls the to parent class, you can utilize the existing code and only add new code for your own property.

Managers

Your new product manager class should be stored in ./<yourext>/lib/custom/src/MShop/Product/Manager/Myproject.php and usually contains:

  1. namespace Aimeos\MShop\Product\Manager;
  2.  
  3. class Myproject extends Standard
  4. {
  5.     private $searchConfig = array(
  6.         'product.myvalue'=> array(
  7.             'code'=>'product.myvalue',
  8.             'internalcode'=>'mpro."myval"',
  9.             'label'=>'Product MyValue',
  10.             'type'=> 'string', // integer, float, etc.
  11.             'internaltype'=> \Aimeos\MW\DB\Statement\Base::PARAM_STR, // _INT, _FLOAT, etc.
  12.         ),
  13.     );
  14.  
  15.     public function saveItem( \Aimeos\MShop\Common\Item\Iface $item, $fetch = true )
  16.     {
  17.         // a modified copy of the code from the parent class
  18.         // extended by a bind() call and updated bind positions (first parameter)
  19.     }
  20.  
  21.     public function getSearchAttributes( $withsub = true )
  22.     {
  23.         $list = parent::getSearchAttributes( $withsub );
  24.         foreach( $this->searchConfig as $key => $fields ) {
  25.             $list[$key] = new \Aimeos\MW\Criteria\Attribute\Standard( $fields );
  26.         }
  27.         return $list;
  28.     }
  29.  
  30.     protected function createItemBase( array $values = [] /* , ... */ )
  31.     {
  32.         return new \Aimeos\MShop\Product\Item\Myproject( $values /* , ... */ );
  33.     }
  34. }

The $searchConfig and getSearchAttributes() method will allow you to use the defined code (product.myvalue) in search criteria expressions passed to searchItems().

You also need to add a new SQL SELECT statement to the configuration, so the values are fetched from the database.

Configuration

Your new manager won't be used until you tell the corresponding factory that it should use the Myproject manager instead of the Standard one. Thus, create a new file ./<yourext>/config/mshop.php (in 2017.x and before it's ./<yourext>/lib/custom/config/mshop.php) and add this configuration:

  1. return [
  2.     'product' => [
  3.         'manager' => [
  4.             'name' => 'Myproject',
  5.             'standard' => [
  6.                 'insert' => [
  7.                     'ansi' => 'INSERT ... (with new column)',
  8.                 ],
  9.                 'update' => [
  10.                     'ansi' => 'UPDATE ... (with new column)',
  11.                 ],
  12.                 'search' => [
  13.                     'ansi' => 'SELECT ... (with new column)',
  14.                 ],
  15.             ],
  16.         ],
  17.     ],
  18. ];

The configuration of the new manager class does also work for sub-managers like the product lists type and all other sub-managers by using mshop/<domain>/manager/<submanager>/<submanager>/name instead, e.g. mshop/product/manager/lists/type/name

By adding a new SQL SELECT statement for mshop/product/manager/standard/search/ansi, the existing manager will care about retrieving the new column values and push them into your new item class you create in createItemBase() of your manager class.

Testing

All test class must be in the ./<yourext>/lib/custom/tests/ directory using the same directory structure as in ./src/, e.g. ./<yourext>/lib/custom/tests/MShop/Product/Item/MyprojectTest.php the ...Test.php extension is important so PHPUnit will recognize these files as test classes. For items, they usually consists of

  1. namespace Aimeos\MShop\Product\Item;
  2.  
  3. class MyprojectTest extends \PHPUnit\Framework\TestCase
  4. {
  5.     private $object;
  6.  
  7.     protected function setUp()
  8.     {
  9.         $values = ['myvalue' => 'test'];
  10.         $this->object = new \Aimeos\MShop\Product\Item\Myproject( $values );
  11.     }
  12.  
  13.     public function testGetMyValue()
  14.     {
  15.         $this->assertEquals( 'test', $this->object->getMyValue() );
  16.     }
  17.  
  18.     public function testSetMyValue()
  19.     {
  20.         $this->object->setMyValue( 'test2' );
  21.         $this->assertEquals( 'test2', $this->object->getMyValue() );
  22.         $this->assertTrue( $this->object->isModified() );
  23.     }
  24.  
  25.     public function testFromArray()
  26.     {
  27.         $this->object->fromArray( ['product.myvalue' => '123'] );
  28.         $this->assertEquals( '123', $this->object->getMyValue() );
  29.     }
  30.  
  31.     public function testToArray()
  32.     {
  33.         $list = $this->object->toArray();
  34.         $this->assertEquals( 'test', $list['product.myvalue'] );
  35.     }
  36. }

For your new manager, you should also test the methods you've implemented:

  1. namespace Aimeos\MShop\Product\Manager;
  2.  
  3. class MyprojectTest extends \PHPUnit\Framework\TestCase
  4. {
  5.     private $object;
  6.  
  7.     protected function setUp()
  8.     {
  9.         $context = \TestHelperMShop::getContext();
  10.         $this->object = new \Aimeos\MShop\Product\Manager\Standard( $context );
  11.     }
  12.  
  13.     public function testSaveItem()
  14.     {
  15.         // modified test method of the "StandardTest" class with your new property
  16.     }
  17.  
  18.     public function testGetSearchAttributes()
  19.     {
  20.         $list = $this->object->getSearchAttributes();
  21.         $this->assertArrayHasKey( 'product.myvalue', $list );
  22.     }
  23. }

To test your extension, you have to

  • checkout the Aimeos core in a separate directory
  • run composer update to install the required dependencies
  • configure your database in ./config/resources.php
  • store your new extension in the ./ext/ subdirectory e.g. as ./ext/me-myproject/'
  • execute ./vendor/bin/phing setup to create the tables and add the unittest data
  • execute ./vendor/bin/phing -Ddir=ext/me-myproject testext to run your tests