Slide 1

Slide 1 text

Porting a complex extension to Magento 2 Fabian Schmengler @fschmengler Meet Magento Romania 29.10.2016 1 / 62

Slide 2

Slide 2 text

The Extension IntegerNet_Solr 2 / 62

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

4 / 62

Slide 5

Slide 5 text

Metrics 2304 Logical Lines of Code 79 Classes (and 3 interfaces) 10 Class rewrites 12 Observers 5 / 62

Slide 6

Slide 6 text

Our Goal: Reuse 6 / 62

Slide 7

Slide 7 text

Extract independent library 7 / 62

Slide 8

Slide 8 text

Extract independent library 7 / 62

Slide 9

Slide 9 text

Extract independent library 7 / 62

Slide 10

Slide 10 text

Architecture 8 / 62

Slide 11

Slide 11 text

Architecture 8 / 62

Slide 12

Slide 12 text

Architecture 8 / 62

Slide 13

Slide 13 text

Architecture 8 / 62

Slide 14

Slide 14 text

The Plan: 1. Decouple / Extract library 2. Implement M2 bridges 3. Glue together M2 module 9 / 62

Slide 15

Slide 15 text

Step 1: Refactoring 10 / 62

Slide 16

Slide 16 text

First: TESTS 11 / 62

Slide 17

Slide 17 text

First: TESTS this is NOT optional!

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

Start easy Find classes with least interaction with Magento Eliminate dependencies Small steps Introduce intermediate solutions if necessary Deprecate them immediately 13 / 62

Slide 20

Slide 20 text

14 / 62

Slide 21

Slide 21 text

Mage Levels Mage::throwException() Mage::getStoreConfig() Mage::dispatchEvent() Mage::helper() Mage::getModel() Level 1 Final enemy 15 / 62

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

Better: Use Value Objects represent a value no identity immutable 18 / 62

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

Build bridges for models "Implementor" interfaces define what library needs from Magento Bridge implementation delegates to actual Magento models 25 / 62

Slide 32

Slide 32 text

Remember? 26 / 62

Slide 33

Slide 33 text

Example 27 / 62

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

Divide and Conquer 33 / 62

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

Example Big "Result" singleton 35 / 62

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

Rebuild Components Small independent units Plan, develop bottom up, replace old implementation New code => TDD, Unit Tests (Yay!) 38 / 62

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

Before/After 42 / 62

Slide 49

Slide 49 text

Bigger size Logical Lines of Code 2304 → 5144 +2840 Classes 79 → 217 +138 Interfaces 3 → 58 +55 43 / 62

Slide 50

Slide 50 text

Less complex units Ø Cyclomatic complexity / method 2.88 → 2.09 -0.79 Ø Cyclomatic complexity / class 11.35 → 5.34 -6.01 44 / 62

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

Step 2: M2 Bridge 46 / 62

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

Step 3: Integrate 49 / 62

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

But never use the Object Manager! 53 / 62

Slide 62

Slide 62 text

Dependency Injection - Interfaces always depend on the interfaces defin bridge as preference for implementor 54 / 62

Slide 63

Slide 63 text

Dependency Injection - Services di.xml also for library classes types and preferences factories 55 / 62

Slide 64

Slide 64 text

Dependency Injection - Virtual Types prefer type specific dependencies over global preferences virtual types are great 56 / 62

Slide 65

Slide 65 text

Example: Product collection with disabled flat index disabledFlatStateProductCollectionFactory disabledFlatStateProductCollection disabledFlatState false 57 / 62

Slide 66

Slide 66 text

Questions FAQ 58 / 62

Slide 67

Slide 67 text

How long did this take? Original M1 module: ~300 hours Refactoring: ~150 hours M2 module: ~300 hours 59 / 62

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

Contact: www.integer-net.de [email protected] @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