Porting a complex extension to Magento 2

Porting a complex extension to Magento 2

Cbc8378de58e66705678686057cffac9?s=128

Fabian Schmengler

October 29, 2016
Tweet

Transcript

  1. Porting a complex extension to Magento 2 Fabian Schmengler @fschmengler

    Meet Magento Romania 29.10.2016 1 / 62
  2. The Extension IntegerNet_Solr 2 / 62

  3. Improved Search and Layered Navigation More relevant search results Fuzzy

    search Autosuggest window Filters with multiple selection Search categories and CMS pages Boosting of products and attributes Fetch category pages from Solr for improved performance 3 / 62
  4. 4 / 62

  5. Metrics 2304 Logical Lines of Code 79 Classes (and 3

    interfaces) 10 Class rewrites 12 Observers 5 / 62
  6. Our Goal: Reuse 6 / 62

  7. Extract independent library 7 / 62

  8. Extract independent library 7 / 62

  9. Extract independent library 7 / 62

  10. Architecture 8 / 62

  11. Architecture 8 / 62

  12. Architecture 8 / 62

  13. Architecture 8 / 62

  14. The Plan: 1. Decouple / Extract library 2. Implement M2

    bridges 3. Glue together M2 module 9 / 62
  15. Step 1: Refactoring 10 / 62

  16. First: TESTS 11 / 62

  17. First: TESTS this is NOT optional!

  18. Must have: Integration Tests e.g. EcomDev_PHPUnit Nice to have: Functional

    Tests e.g. Selenium Useless: Unit Tests Don't write unit tests for existing code! 12 / 62
  19. Start easy Find classes with least interaction with Magento Eliminate

    dependencies Small steps Introduce intermediate solutions if necessary Deprecate them immediately 13 / 62
  20. 14 / 62

  21. Mage Levels Mage::throwException() Mage::getStoreConfig() Mage::dispatchEvent() Mage::helper() Mage::getModel() Level 1 Final

    enemy 15 / 62
  22. Use own exceptions Mage::throwException($msg); ⇓ throw new IntegerNet_Solr_Exception($msg); 16 /

    62
  23. Inject configuration values public function __construct() { $this->host = Mage::getStoreConfig('integernet_solr/server/host');

    $this->port = Mage::getStoreConfig('integernet_solr/server/port'); } ⇓ public function __construct($host, $port) { $this->host = $host; $this->port = $port; } 17 / 62
  24. Better: Use Value Objects represent a value no identity immutable

    18 / 62
  25. Configuration Value Objects Definition namespace IntegerNet\Solr\Config; final class ServerConfig {

    private $host, $port; // ... public function __construct($host, $port, ...) { $this->host = $host; $this->port = $port; // ... } public function getHost() { return $this->host; } public function getPort() { return $this->port; } // ... } 19 / 62
  26. Configuration Value Objects Usage Read configuration in Magento module: $serverConfig

    = new ServerConfig( Mage::getStoreConfig('integernet_solr/server/host'), Mage::getStoreConfig('integernet_solr/server/port'), // ... ); Pass $serverConfig value object to library: $solr = new SolrResource($serverConfig, ...); 20 / 62
  27. Interfaces for Helpers public function __construct() { $this->helper = Mage::helper('integernet/solr');

    } $this->helper->getUserQueryText(); ⇓ public function __construct(UserQuery $userQuery) { $this->userQuery = $userQuery; } $this->userQuery->getText(); 21 / 62
  28. Introduce Interfaces First define interface for existing code class IntegerNet_Solr_Helper_Data

    extends Mage_Core_Helper_Abstract implements UserQuery { public function getUserQueryText(); ... } namespace IntegerNet\Solr\Implementor; interface UserQuery { /** * Returns query as entered by user * * @return string */ public function getUserQueryText(); } 22 / 62
  29. Dependency Injection Then pass helpers as implementation of the interface

    class IntegerNet_Solr_Helper_Factory { public function getSolrRequest() { return new SearchRequestFactory( Mage::helper('integernet_solr'), // constructor expects UserQuery ... )->createRequest(); } } Note: No Dependency Injection framework involved! 23 / 62
  30. Interface Segregation class IntegerNet_Solr_Helper_Data extends Mage_Core_Helper_Abstract implements UserQuery, EventDispatcher, SearchUrl

    { ... } Lean interfaces Better defined dependencies Single Responsibility Split helpers (later) 24 / 62
  31. Build bridges for models "Implementor" interfaces define what library needs

    from Magento Bridge implementation delegates to actual Magento models 25 / 62
  32. Remember? 26 / 62

  33. Example 27 / 62

  34. Service class ProductModification { public function uppercaseDescription($sku) { $product =

    $this->productRepository->getProduct($sku); $product->setDescription(strtoupper($product->getDescription()); $productRepository->saveProduct($product); } } Interfaces interface ProductRepository { /** @return Product */ public function getProduct($sku); public function saveProduct(Product $product); } interface Product { public function getDescription(); public function setDescription($description); } 28 / 62
  35. Bridge: Product class IntegerNet_Example_Model_Bridge_Product implements Product { /** @var Mage_Catalog_Model_Product

    */ protected $_product; public function __construct(Mage_Catalog_Model_Product $_product) { $this->_product = $_product; } public function getDescription() { return $this->_product->getDescription(); } public function setDescription($description) { $this->_product->setDescription($description); } ... } 29 / 62
  36. Bridge: Product (additional methods) class IntegerNet_Example_Model_Bridge_Product implements Product { ...

    /** @deprecated only use interface methods! */ public function __call($method, $args) { return call_user_func_array(array($this->_product, $method), $args); } /** @return Mage_Catalog_Model_Product */ public function getMagentoProduct() { return $this->_product; } } only use within the Magento module! __call() useful during refactoring (@deprecated) 30 / 62
  37. Bridge: Product Repository class IntegerNet_Example_Model_Bridge_ProductRepository implements ProductRepository { public function

    getProduct($sku) { $id = Mage::getModel('catalog/product')->getIdBySku($sku); $magentoProduct = Mage::getModel('catalog/product')->load($id); return new IntegerNet_Example_Model_Bridge_Product($magentoProduct); } public function saveProduct(Product $product) { /** @var IntegerNet_Example_Model_Bridge_Product $product */ $product->getMagentoProduct()->save(); } } Here we know the concrete type of $product So we are allowed to use getMagentoProduct() 31 / 62
  38. Usage from Magento Controller class IntegerNet_Example_Adminhtml_ModifierController extends Mage_Adminhtml_Controller_Action { public

    function uppercaseDescriptionAction() { // Instantiation: $modifier = new ProductModifier( new IntegerNet_Example_Model_Bridge_ProductRepository() ); // Call the Service: $modifier->uppercaseDescription($this->getParam('sku')); ... } } Tipp: move all intantiation into factory "helper" Less duplication Magento rewrite system can still be used 32 / 62
  39. Divide and Conquer 33 / 62

  40. Split big classes Approach 1 Extract methods that don’t use

    any mutable attributes of $this to other classes Approach 2 Extract mutable state, grouped with the methods operating on this state 34 / 62
  41. Example Big "Result" singleton 35 / 62

  42. Example Big "Result" singleton after first extraction 36 / 62

  43. Split big classes Keep as Facade (@deprecated) Delegate calls to

    new structure "Result" example: Before: 10 public methods, 221 LLOC After: 6 public methods, 31 LLOC 37 / 62
  44. Rebuild Components Small independent units Plan, develop bottom up, replace

    old implementation New code => TDD, Unit Tests (Yay!) 38 / 62
  45. Example Query strings were escaped with a helper Used in

    several places in original code Introduced value object final class SearchString { public function __construct($rawString) { ... } public function getRawString() { ... } public function getEscapedString() { ... } public static function escape ($string) { ... } } Replaced strings that represent query string with SearchString instance 39 / 62
  46. Visualize class dependencies Use doxygen or pen and paper Who

    is talking to whom? Where are the boundaries? Where do we want them to be? Can we minimize the interfaces? 40 / 62
  47. Avoid "gold-plating" No need for perfect implementation Focus on good

    abstraction instead Small and well-defined interface is priority When it's good enough, stop. 41 / 62
  48. Before/After 42 / 62

  49. Bigger size Logical Lines of Code 2304 → 5144 +2840

    Classes 79 → 217 +138 Interfaces 3 → 58 +55 43 / 62
  50. Less complex units Ø Cyclomatic complexity / method 2.88 →

    2.09 -0.79 Ø Cyclomatic complexity / class 11.35 → 5.34 -6.01 44 / 62
  51. Reactions of a Magento 1 developer wow, that's complicated unfamiliar

    code style, not Magento standard further development seems to take more effort 45 / 62
  52. Reactions of a Magento 1 developer wow, that's complicated unfamiliar

    code style, not Magento standard further development seems to take more effort After working with it for a few days got used to it faster than expected way better IDE integration more reliable thanks to automated tests higher code quality 45 / 62
  53. Step 2: M2 Bridge 46 / 62

  54. Code Convertion Tools Useful to get started with module XML

    files Don't use them for actual code Unirgy ConvertM1M2: https://github.com/unirgy/convertm1m2 Magento Code Migration (official): https://github.com/magento/code-migration 47 / 62
  55. Implement Interfaces Completely test driven Do not test (and use)

    services from the library yet Unit tests where you know which levers to pull in the core Integration tests (exploratory) if you still have to figure it out 48 / 62
  56. Step 3: Integrate 49 / 62

  57. Integrate library and Magento 2 take remaining M1 module as

    example drive development with integration tests implement feature by feature prefer plugins over events and rewrites 50 / 62
  58. Use the latest Magento version you don't want to support

    Magento 2.0 with a new extension lock module dependencies in composer.json "magento/catalog": "^101.0.0" 51 / 62
  59. Magento 2 Module Development follow recommended Magento 2 development practices

    if possible core code should not be taken as an example refer to devdocs, Stack Exchange (there is a "best- practice" tag), presentations 52 / 62
  60. Magento 2 Module Development follow recommended Magento 2 development practices

    if possible core code should not be taken as an example refer to devdocs, Stack Exchange (there is a "best- practice" tag), presentations be pragmatic service contracts are still incomplete models and collections are more flexible 52 / 62
  61. But never use the Object Manager! 53 / 62

  62. Dependency Injection - Interfaces always depend on the interfaces defin

    bridge as preference for implementor 54 / 62
  63. Dependency Injection - Services di.xml also for library classes types

    and preferences factories 55 / 62
  64. Dependency Injection - Virtual Types prefer type specific dependencies over

    global preferences virtual types are great 56 / 62
  65. Example: Product collection with disabled flat index <type name="IntegerNet\Solr\Model\Indexer\ProductCollectionFactory"> <arguments>

    <argument name="collectionFactory" xsi:type="object">disabledFlatStateProductCollectionFactory </argument> </arguments> </type> <virtualType name="disabledFlatStateProductCollectionFactory" type="Magento\Catalog\Model\ResourceModel\Product\CollectionFactory"> <arguments> <argument name="instanceName" xsi:type="string">disabledFlatStateProductCollection</argument> </arguments> </virtualType> <virtualType name="disabledFlatStateProductCollection" type="Magento\Catalog\Model\ResourceModel\Product\Collection"> <arguments> <argument name="catalogProductFlatState" xsi:type="object">disabledFlatState</argument> </arguments> </virtualType> <virtualType name="disabledFlatState" type="Magento\Catalog\Model\Indexer\Product\Flat\State"> <arguments> <argument name="isAvailable" xsi:type="boolean">false</argument> </arguments> </virtualType> 57 / 62
  66. Questions FAQ 58 / 62

  67. How long did this take? Original M1 module: ~300 hours

    Refactoring: ~150 hours M2 module: ~300 hours 59 / 62
  68. Can you replace Solr with ElasticSearch If you take the

    framework agnostic approach to the next level: yes create the same boundary between library and search engine than between library and shop framework write different adapters 60 / 62
  69. Can you replace Solr with ElasticSearch If you take the

    framework agnostic approach to the next level: yes create the same boundary between library and search engine than between library and shop framework write different adapters But why should we? Solr is more advanced, ElasticSearch is easier to learn We already learned Solr YAGNI 60 / 62
  70. More useful resources Eating ElePHPants keynote (Larry Garfield on the

    Drupal 8 refactoring) https://www.youtube.com/watch?v=5jqY4NNnc3I SOLID MVC presentation (Stefan Priebsch on framework agnostic domain code) https://www.youtube.com/watch?v=NdBMQsp_CpE Mage2Katas video tutorials (Vinai Kopp on TDD with Magento 2) http://mage2katas.com/ 61 / 62
  71. Contact: www.integer-net.de solr@integer-net.de @integer_net @fschmengler I'm here, talk to me

    :-) Read more in our Blog integer-net.com/m1m2 Shared Code for Extensions (7 articles) 62 / 62