Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Porting a complex extension to Magento 2 (Mage Titans)

Porting a complex extension to Magento 2 (Mage Titans)

The 30 minutes version of this talk, held at Mage Titans Manchester 2016

Slides as HTML: http://www.schmengler-se.de/slides/porting-a-complex-extension-to-magento-2-mage-titans-mcr/

45 minutes version: https://speakerdeck.com/schmengler/porting-a-complex-extension-to-magento-1

Fabian Schmengler

November 12, 2016
Tweet

More Decks by Fabian Schmengler

Other Decks in Technology

Transcript

  1. View Slide

  2. The Extension
    IntegerNet_Solr
    2 / 57

    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 / 57

    View Slide

  4. 4 / 57

    View Slide

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

    View Slide

  6. Our Goal:
    Reuse
    6 / 57

    View Slide

  7. Extract independent library
    7 / 57

    View Slide

  8. Extract independent library
    7 / 57

    View Slide

  9. Extract independent library
    7 / 57

    View Slide

  10. Architecture
    8 / 57

    View Slide

  11. Architecture
    8 / 57

    View Slide

  12. Architecture
    8 / 57

    View Slide

  13. Architecture
    8 / 57

    View Slide

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

    View Slide

  15. Step 1:
    Refactoring
    10 / 57

    View Slide

  16. First: TESTS
    11 / 57

    View Slide

  17. First: TESTS
    this is NOT optional!
    12 / 57

    View Slide

  18. Why tests?
    Make changes with confidence
    Receive fast feedback during development
    13 / 57

    View Slide

  19. 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!
    14 / 57

    View Slide

  20. Small steps
    Introduce intermediate solutions if necessary
    Deprecate them immediately
    Start easy
    Find classes with least interaction with Magento
    Eliminate dependencies
    15 / 57

    View Slide

  21. 16 / 57

    View Slide

  22. Mage Levels
    Mage::throwException()
    Mage::getStoreConfig()
    Mage::dispatchEvent()
    Mage::helper()
    Mage::getModel()
    Level 1
    Final enemy
    17 / 57

    View Slide

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

    throw new IntegerNet_Solr_Exception($msg);
    18 / 57

    View Slide

  24. 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;
    }
    19 / 57

    View Slide

  25. Better: Use Value Objects
    represent a value
    no identity
    immutable
    20 / 57

    View Slide

  26. 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;
    }
    // ...
    }
    21 / 57

    View Slide

  27. 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, ...);
    22 / 57

    View Slide

  28. 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();
    23 / 57

    View Slide

  29. 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();
    }
    24 / 57

    View Slide

  30. 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!
    25 / 57

    View Slide

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

    View Slide

  32. Remember?
    27 / 57

    View Slide

  33. Example
    28 / 57

    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);
    }
    29 / 57

    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);
    }
    ...
    }
    30 / 57

    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)
    31 / 57

    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()
    32 / 57

    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
    33 / 57

    View Slide

  39. Divide and
    Conquer
    34 / 57

    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
    35 / 57

    View Slide

  41. Example
    Big "Result" singleton
    36 / 57

    View Slide

  42. Example
    Big "Result" singleton after first extraction
    37 / 57

    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
    38 / 57

    View Slide

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

    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
    40 / 57

    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?
    41 / 57

    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.
    42 / 57

    View Slide

  48. Step 2:
    M2 Bridge
    43 / 57

    View Slide

  49. 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
    44 / 57

    View Slide

  50. 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
    45 / 57

    View Slide

  51. Step 3:
    Integrate
    46 / 57

    View Slide

  52. 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
    47 / 57

    View Slide

  53. 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"
    48 / 57

    View Slide

  54. 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
    49 / 57

    View Slide

  55. 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
    49 / 57

    View Slide

  56. But never use the
    Object Manager!
    50 / 57

    View Slide

  57. Dependency Injection - Interfaces
    always depend on the interfaces
    define bridge as preference for implementor
    51 / 57

    View Slide

  58. Dependency Injection - Services
    library classes can be used in types and
    preferences
    factories and proxies can be generated for
    library classes
    52 / 57

    View Slide

  59. Questions
    FAQ
    53 / 57

    View Slide

  60. How long did this take?
    Original M1 module: ~300 hours
    Refactoring: ~150 hours
    M2 module: ~300 hours
    54 / 57

    View Slide

  61. 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
    55 / 57

    View Slide

  62. 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
    55 / 57

    View Slide

  63. 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/
    56 / 57

    View Slide

  64. 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)
    57 / 57

    View Slide