$30 off During Our Annual Pro Sale. View Details »

Porting a complex extension to Magento 2

Porting a complex extension to Magento 2

Fabian Schmengler

October 29, 2016
Tweet

More Decks by Fabian Schmengler

Other Decks in Technology

Transcript

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

    View Slide

  2. The Extension
    IntegerNet_Solr
    2 / 62

    View Slide

  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

    View Slide

  4. 4 / 62

    View Slide

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

    View Slide

  6. Our Goal:
    Reuse
    6 / 62

    View Slide

  7. Extract independent library
    7 / 62

    View Slide

  8. Extract independent library
    7 / 62

    View Slide

  9. Extract independent library
    7 / 62

    View Slide

  10. Architecture
    8 / 62

    View Slide

  11. Architecture
    8 / 62

    View Slide

  12. Architecture
    8 / 62

    View Slide

  13. Architecture
    8 / 62

    View Slide

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

    View Slide

  15. Step 1:
    Refactoring
    10 / 62

    View Slide

  16. First: TESTS
    11 / 62

    View Slide

  17. First: TESTS
    this is NOT optional!

    View Slide

  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

    View Slide

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

    View Slide

  20. 14 / 62

    View Slide

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

    View Slide

  22. Use own exceptions
    Mage::throwException($msg);

    throw new IntegerNet_Solr_Exception($msg);
    16 / 62

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

  32. Remember?
    26 / 62

    View Slide

  33. Example
    27 / 62

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  39. Divide and
    Conquer
    33 / 62

    View Slide

  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

    View Slide

  41. Example
    Big "Result" singleton
    35 / 62

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  48. Before/After
    42 / 62

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  53. Step 2:
    M2 Bridge
    46 / 62

    View Slide

  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

    View Slide

  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

    View Slide

  56. Step 3:
    Integrate
    49 / 62

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  61. But never use the
    Object Manager!
    53 / 62

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  65. Example: Product collection with disabled flat index


    disabledFlatStateProductCollectionFactory



    type="Magento\Catalog\Model\ResourceModel\Product\CollectionFactory">

    disabledFlatStateProductCollection


    type="Magento\Catalog\Model\ResourceModel\Product\Collection">

    disabledFlatState




    false


    57 / 62

    View Slide

  66. Questions
    FAQ
    58 / 62

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  71. 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

    View Slide