1<!doctype html> 2<html lang="en"> 3 4<head> 5 <meta charset="utf-8"> 6 7 <title>MeetMagento Sweden 2017 - Architecture Challenges of Magento 2 Customizations</title> 8 9 <meta name="description" content="Architecture Challenges of Magento 2 Customizations"> 10 <meta name="author" content="Ivan Chepurnyi"> 11 12 <meta name="apple-mobile-web-app-capable" content="yes"> 13 <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"> 14 15 <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, minimal-ui"> 16 17
17<script> 18 window.baseUrl = '/bower_components/reveal.js'; 19 20 window.addJs = function (jsSrc, callback) { 21 var script = document.createElement('script'); 22 script.src = baseUrl + jsSrc; 23 script.addEventListener('load', callback || function () {}); 24 document.getElementsByTagName('body')[0].appendChild(script); 25 }; 26 27 window.addCss = function (cssSrc) { 28 var link = document.createElement( 'link' ); 29 link.rel = 'stylesheet'; 30 link.type = 'text/css'; 31 link.href = baseUrl + cssSrc; 32 document.getElementsByTagName('head')[0].appendChild( link ); 33 } 34 </script>
34 35 36 <!-- Printing and PDF exports --> 37
37<script> 38 addCss('/css/reveal.css'); 39 addCss('/css/theme/white.css'); 40 addCss('/lib/css/zenburn.css'); 41 addCss((window.location.search.match( /print-pdf/gi ) ? '/css/print/pdf.css' : '/css/print/paper.css')); 42 </script>
42 43 44 <!--[if lt IE 9]> 45
45<script> 46 addJs('/lib/js/html5shiv.js'); 47 </script>
47 48 <![endif]--> 49 50 <style type="text/css"> 51 52section .large { 53 font-size: 1.4em; 54} 55 56section .x-large { 57 font-size: 2.5em; 58} 59 60.template { 61 display: none; 62} 63 64.reveal section footer { 65 font-size: 1.0rem; 66} 67 68.reveal footer { 69 position: absolute; 70 top: 0px; 71 left: 0px; 72 display: block; 73 width: 100%; 74} 75 76.reveal footer div { 77 position: absolute; 78} 79 80@media print { 81 82 .reveal footer div { 83 position: fixed; 84 } 85 86 .reveal section.title-slide > div { 87 margin-top: 20%; 88 } 89} 90 91.reveal footer .left { 92 left: 10px; 93 top: 10px; 94} 95 96.reveal footer .right { 97 top: 10px; 98 right: 10px; 99} 100.reveal pre code { 101 font-size: 0.9rem; 102 line-height: normal; 103} 104.reveal section.title-slide { 105 height: 100%; 106 position: relative; 107} 108 109.reveal section.title-slide > div { 110 position: absolute; 111 top: 30%; 112 width: 100%; 113} 114 115.reveal section.title-slide > div > * { 116 display: block; 117 float: left; 118} 119 120.reveal section.title-slide img { 121 margin: 0; 122} 123 124.reveal section.title-slide > div .flex { 125 font-size: 1.4rem; 126 max-width: 78%; 127 padding-left: 10px; 128} 129 130.reveal section.title-slide h1 { 131 font-size: 3.6em; 132 padding-bottom: 10px; 133} 134 135.reveal section.title-slide h2 { 136 font-size: 2.2em; 137 color: #2081C4; 138} 139 140.reveal section.title-slide h1, 141.reveal section.title-slide h2, 142.reveal section.title-slide p { 143 line-height: 0.85; 144 margin: 0; 145} 146 147.reveal section.title-slide > div .left { 148 width: 20%; 149} 150</style> 151<!--[if lt IE 9]>
152<script src="lib/js/html5shiv.js"></script>
152 153<![endif]--> 154</head> 155 156<body> 157 158<div class="reveal"> 159 <footer> 160 <div class="left">MeetMagento Sweden 2017</div><div class="right">#mm17se</div> 161 </footer> 162 <!-- Any section element inside of this container is displayed as a slide --> 163 <div class="slides"> 164 165 <section class="title-slide"> 166 <div> 167 <div class="left"><img src="/images/logo-icon.svg" class="plain" /></div> 168 <div class="flex"> 169 <h1>Architecture Challenges</h1> 170 <h2>Magento 2 Customizations</h2> 171 </div> 172 </div> 173 </section> 174 175 <section> 176 <h2>About Me</h2> 177 <ul> 178 <li class="fragment">Magento addict since May 2007</li> 179 <li class="fragment">I love to take challenges</li> 180 <li class="fragment">I love to optimize performance</li> 181 <li class="fragment">I love to train others on those matters</li> 182 </ul> 183 </section> 184 185 <section> 186 <p class="large fragment">Are there any Software Architects?</p> 187 <p class="large fragment">Are there any Senior Developers?</p> 188 <p class="large fragment">Are there any Junior Developers?</p> 189 </section> 190 191 <section> 192 <p class="large">Every developer in this room is an architect</p> 193 </section> 194 195 <section> 196 <p class="large">We are all responsible for our Software Design</p> 197 </section> 198 199 <section> 200 <p class="large">Architecting software is hard...</p> 201 </section> 202 203 <section> 204 <p class="large">For example in the beggining of 2007</p> 205 </section> 206 207 <section> 208 <p class="large">ZF1 just went into first beta (0.8.0)</p> 209 </section> 210 211 <section> 212 <p class="large">Symfony 1.0 just got released</p> 213 </section> 214 215 <section> 216 <p class="large">Composer?</p> 217 </section> 218 219 <section> 220 <p class="large">Unit testing was not yet a thing</p> 221 </section> 222 223 <section> 224 <p class="large">Everyone was using Subversion (SVN)</p> 225 </section> 226 227 <section> 228 <p class="large">I wish I knew everything I know now back then</p> 229 </section> 230 231 <section> 232 <section> 233 <p class="large">Challenge #0: Using Magento 1.x approach</p> 234 </section> 235 236 <section> 237 <p class="large">Magento 2.x is a huge step ahead and learning new concepts hard at first</p> 238 </section> 239 240 <section> 241 <p class="large">But these new learning curve is going to pay off in the end</p> 242 </section> 243 244 <section> 245 <p class="large">A lot of people tell that Magento 2 should have used Symfony and Doctrine...</p> 246 </section> 247 248 <section> 249 <p class="large">But I am happy Magento 2.x does not use Symfony and Doctine</p> 250 </section> 251 252 <section> 253 <p>Symfony has an excessive configuration boilerplate</p> 254 <pre><code class="xml stretch" data-trim> 255<?xml version="1.0" ?> 256 257<container xmlns="http://symfony.com/schema/dic/services" 258 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 259 xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> 260 261 <parameters> 262 <parameter key="doctrine.orm.configuration.class">Doctrine\ORM\Configuration</parameter> 263 <parameter key="doctrine.orm.entity_manager.class">Doctrine\ORM\EntityManager</parameter> 264 <parameter key="doctrine.orm.manager_configurator.class">Doctrine\Bundle\DoctrineBundle\ManagerConfigurator</parameter> 265 266 <!-- cache --> 267 <parameter key="doctrine.orm.cache.array.class">Doctrine\Common\Cache\ArrayCache</parameter> 268 <parameter key="doctrine.orm.cache.apc.class">Doctrine\Common\Cache\ApcCache</parameter> 269 <parameter key="doctrine.orm.cache.memcache.class">Doctrine\Common\Cache\MemcacheCache</parameter> 270 <parameter key="doctrine.orm.cache.memcache_host">localhost</parameter> 271 <parameter key="doctrine.orm.cache.memcache_port">11211</parameter> 272 <parameter key="doctrine.orm.cache.memcache_instance.class">Memcache</parameter> 273 <parameter key="doctrine.orm.cache.memcached.class">Doctrine\Common\Cache\MemcachedCache</parameter> 274 <parameter key="doctrine.orm.cache.memcached_host">localhost</parameter> 275 <parameter key="doctrine.orm.cache.memcached_port">11211</parameter> 276 <parameter key="doctrine.orm.cache.memcached_instance.class">Memcached</parameter> 277 <parameter key="doctrine.orm.cache.redis.class">Doctrine\Common\Cache\RedisCache</parameter> 278 <parameter key="doctrine.orm.cache.redis_host">localhost</parameter> 279 <parameter key="doctrine.orm.cache.redis_port">6379</parameter> 280 <parameter key="doctrine.orm.cache.redis_instance.class">Redis</parameter> 281 <parameter key="doctrine.orm.cache.xcache.class">Doctrine\Common\Cache\XcacheCache</parameter> 282 <parameter key="doctrine.orm.cache.wincache.class">Doctrine\Common\Cache\WinCacheCache</parameter> 283 <parameter key="doctrine.orm.cache.zenddata.class">Doctrine\Common\Cache\ZendDataCache</parameter> 284 285 <!-- metadata --> 286 <parameter key="doctrine.orm.metadata.driver_chain.class">Doctrine\Common\Persistence\Mapping\Driver\MappingDriverChain</parameter> 287 <parameter key="doctrine.orm.metadata.annotation.class">Doctrine\ORM\Mapping\Driver\AnnotationDriver</parameter> 288 <parameter key="doctrine.orm.metadata.xml.class">Doctrine\ORM\Mapping\Driver\SimplifiedXmlDriver</parameter> 289 <parameter key="doctrine.orm.metadata.yml.class">Doctrine\ORM\Mapping\Driver\SimplifiedYamlDriver</parameter> 290 <parameter key="doctrine.orm.metadata.php.class">Doctrine\ORM\Mapping\Driver\PHPDriver</parameter> 291 <parameter key="doctrine.orm.metadata.staticphp.class">Doctrine\ORM\Mapping\Driver\StaticPHPDriver</parameter> 292 </code></pre> 293 </section> 294 295 <section> 296 <p class="large">Doctrine is bound to its multi-vendor approach and forces code to be your data representation</p> 297 <pre><code class="php stretch" data-trim> 298/** 299 * @Entity @Table(name="bugs") 300 **/ 301class Bug 302{ 303 /** 304 * @Id @Column(type="integer") @GeneratedValue 305 **/ 306 protected $id; 307 /** 308 * @Column(type="string") 309 **/ 310 protected $description; 311 /** 312 * @Column(type="datetime") 313 **/ 314 protected $created; 315 /** 316 * @Column(type="string") 317 **/ 318 protected $status; 319 320 /** 321 * @ManyToOne(targetEntity="User", inversedBy="assignedBugs") 322 **/ 323 protected $engineer; 324 325 /** 326 * @ManyToOne(targetEntity="User", inversedBy="reportedBugs") 327 **/ 328 protected $reporter; 329 330 /** 331 * @ManyToMany(targetEntity="Product") 332 **/ 333 protected $products; 334 335 // ... (other code) 336} 337 </code></pre> 338 </section> 339 340 <section> 341 <h3 class="large">Focus on your domain model</h3> 342 <p class="large">Do not drive design by your framework of choice, use it as a tool.</p> 343 </section> 344 </section> 345 346 <section> 347 348 <section> 349 <p class="large">Challenge #1: Looking at the core modules to create your own functionality</p> 350 </section> 351 352 <section> 353 <p class="large">There are some new code, but mostly it is still good old Magento 1.x code that is going to change</p> 354 </section> 355 356 <section> 357 <p class="large">Do not look at the core module for an example of implemenation, do not copy-paste the code</p> 358 </section> 359 360 <section> 361 <p class="large">Use core code only as a reference</p> 362 </section> 363 364 <section> 365 <p class="large">Read <a href="http://devdocs.magento.com/">http://devdocs.magento.com/</a> for module structure guidelines</p> 366 </section> 367 </section> 368 369 <section> 370 371 <section> 372 <p class="large">Challenge #2: Data model dependencies</p> 373 </section> 374 375 <section> 376 <p class="large">Global Website Store (GWS)</p> 377 378 <aside class="notes"> 379 <p>Ask who is modeling data based on GWS?</p> 380 </aside> 381 </section> 382 383 <section> 384 <p class="large">Binding your data model to GWS leads to scaling issues of your implementation</p> 385 </section> 386 387 <section> 388 <h3>Example</h3> 389 <ul> 390 <li class="fragment">EU Merchant can sell to 28 member states</li> 391 <li class="fragment">Countries might have more then one language</li> 392 <li class="fragment">Every country might get own domain name</li> 393 <li class="fragment">Price might differ per country</li> 394 </ul> 395 </section> 396 397 <section> 398 <h3>Problem</h3> 399 <ul> 400 <li class="fragment">You might end up with 100+ store views</li> 401 <li class="fragment">Language+Country results in data duplications in EAV structure</li> 402 </ul> 403 </section> 404 405 <section> 406 <p class="large">Avoid existing structures where possible and inverse your dependencies</p> 407 </section> 408 409 <section> 410 <h3>Solution</h3> 411 <ul> 412 <li class="fragment">Create own entity: Country Scope</li> 413 <li class="fragment">Create an adapter around store view to override configuration at runtime</li> 414 <li class="fragment">Use store view as a language and link it to scope</li> 415 <li class="fragment">Create country access attribute on product level if needed</li> 416 </ul> 417 </section> 418 419 </section> 420 421 <section> 422 423 <section> 424 <p class="large">Challenge #3: Extending existing functionality</p> 425 </section> 426 427 <section> 428 <p class="large">Rewrite (preference) is a bad thing</p> 429 </section> 430 431 <section> 432 <p class="large">Plugin is not a good thing either</p> 433 </section> 434 435 <section> 436 <h3>Plugin disadvantages</h3> 437 <ul> 438 <li class="fragment">It is very hard to debug</li> 439 <li class="fragment">It is global for an area</li> 440 <li class="fragment">It can affect performance</li> 441 </ul> 442 </section> 443 444 <section> 445 <p class="large">Local Dependency override is a good thing</p> 446 </section> 447 448 <section> 449 <h3>How To</h3> 450 <ul> 451 <li class="fragment">Find exact dependency you need to modify for customization</li> 452 <li class="fragment">Create a decorator that implements same interface</li> 453 <li class="fragment">Pass it into a class via di.xml</li> 454 </ul> 455 </section> 456 </section> 457 458 <section> 459 460 <section> 461 <p class="large">Challenge #4: Depending on concrete implementation</p> 462 </section> 463 464 <section> 465 <p class="large">Injecting concrete class from another component leads to fragile dependency</p> 466 </section> 467 468 <section> 469 <p class="large">
469Protect your code by introducing own abstractions</p> 470 </section> 471 472 <section> 473 <h3>How To</h3> 474 <ul> 475 <li class="fragment">Find functionality you want to use from Magento 2 module</li> 476 <li class="fragment">Create your own interface for that implementation</li> 477 <li class="fragment">Create an adapter based on that interface with Magento 2 module class as dependency</li> 478 </ul> 479 </section> 480 481 </section> 482 483 <section> 484 <h2>Concrete Examples</h2> 485 </section> 486 487 <section> 488 <section> 489 <p class="large">I need to export stock quantities of physical product SKUs for a specific store</p> 490 <p class="fragment">So I want to use external module entity repository...</p> 491 </section> 492 493 <section> 494 <h2>Drawbacks</h2> 495 <ul> 496 <li class="fragment">Direct use of repository requires too much boilerplate inside of business logic</li> 497 <li class="fragment">Request parameters are not revealing the intent</li> 498 <li class="fragment">Performance optimization would require complete rewrite of the code</li> 499 </ul> 500 </section> 501 502 <section> 503 <p class="large">Create your own abstraction and implement Magento adapter for it</p> 504 </section> 505 506 <section> 507 <h3>Component level abstraction</h3> 508 <pre><code class="php stretch" data-trim> 509namespace Conference\StockExport\Source; 510 511interface Entry 512{ 513 public function getName(); 514 515 public function getSku(); 516 517 public function getQty(); 518} 519 520interface EntryGateway 521{ 522 public function getTotalPages($scopeId): int; 523 524 public function getEntriesByPage($scopeId, $page): Entry[]; 525} 526</code></pre> 527 </section> 528 529 <section> 530 <h3>Adapter for entry</h3> 531 <pre><code class="php stretch" data-trim> 532namespace Conference\StockExport\Source; 533 534 535use Magento\Catalog\Api\Data\ProductInterface; 536use Magento\CatalogInventory\Api\Data\StockStatusInterface; 537 538class MagentoEntry implements Entry 539{ 540 private $product; 541 private $stockStatus; 542 543 public function __construct(ProductInterface $product, StockStatusInterface $stockStatus) 544 { 545 $this->product = $product; 546 $this->stockStatus = $stockStatus; 547 } 548 549 public function getName() 550 { 551 return $this->product->getName(); 552 } 553 554 public function getSku() 555 { 556 return $this->product->getSku(); 557 } 558 559 public function getQty() 560 { 561 return $this->stockStatus->getQty(); 562 } 563} 564</code></pre> 565 </section> 566 567 <section> 568 <h3>Adapter for entry gateway</h3> 569 <pre><code class="php stretch" data-trim> 570namespace Conference\StockExport\Source; 571 572use Magento\Catalog\Api\ProductRepositoryInterface; 573use Magento\CatalogInventory\Api\StockRegistryInterface; 574use Magento\Framework\Api\SearchCriteriaBuilder; 575use Magento\Store\Model\StoreManagerInterface; 576 577class MagentoEntryGateway implements EntryGateway 578{ 579 private $searchCriteriaBuilder; 580 581 private $stockRegistry; 582 583 private $productRepository; 584 585 private $storeManager; 586 587 private $pageSize; 588 589 private $magentoEntryFactory; 590 591 public function __construct( 592 SearchCriteriaBuilder $searchCriteriaBuilder, 593 ProductRepositoryInterface $productRepository, 594 StoreManagerInterface $storeManager, 595 StockRegistryInterface $stockRegistry, 596 MagentoEntryFactory $magentoEntryFactory 597 ) { 598 $this->searchCriteriaBuilder = $searchCriteriaBuilder; 599 $this->productRepository = $productRepository; 600 $this->storeManager = $storeManager; 601 $this->stockRegistry = $stockRegistry; 602 $this->pageSize = 1000; 603 $this->magentoEntryFactory = $magentoEntryFactory; 604 } 605 // .. next slide 606} 607</code></pre> 608 </section> 609 <section> 610 <h3>Adapter for entry gateway</h3> 611 <pre><code class="php stretch" data-trim> 612namespace Conference\StockExport\Source; 613 614use Magento\Catalog\Api\ProductRepositoryInterface; 615use Magento\CatalogInventory\Api\StockRegistryInterface; 616use Magento\Framework\Api\SearchCriteriaBuilder; 617use Magento\Store\Model\StoreManagerInterface; 618 619class MagentoEntryGateway implements EntryGateway 620{ 621 // .. prev slide 622 public function getTotalPages($scopeId): int 623 { 624 $this->storeManager->setCurrentStore($scopeId); 625 $searchCriteria = $this->createProductSearchCriteria(1, 1); 626 $searchResult = $this->productRepository->getList($searchCriteria); 627 628 return ceil($searchResult->getTotalCount() / $this->pageSize); 629 } 630 631 public function getEntriesByPage($scopeId, $page): \Traversable 632 { 633 $this->storeManager->setCurrentStore($scopeId); 634 635 $searchCriteria = $this->createProductSearchCriteria($this->pageSize, $page); 636 $searchResult = $this->productRepository->getList($searchCriteria); 637 638 foreach ($searchResult->getItems() as $productItem) { 639 yield $this->magentoEntryFactory->create([ 640 'product' => $productItem, 641 'stockStatus' => $this->stockRegistry->getStockStatus( 642 $productItem->getId(), 643 $scopeId 644 ), 645 ]); 646 } 647 } 648 // .. next slide 649} 650</code></pre> 651 </section> 652 653 <section> 654 <h3>Adapter for entry gateway</h3> 655 <pre><code class="php stretch" data-trim> 656namespace Conference\StockExport\Source; 657 658use Magento\Catalog\Api\ProductRepositoryInterface; 659use Magento\CatalogInventory\Api\StockRegistryInterface; 660use Magento\Framework\Api\SearchCriteriaBuilder; 661use Magento\Store\Model\StoreManagerInterface; 662 663class MagentoEntryGateway implements EntryGateway 664{ 665 // .. prev slide 666 private function createProductSearchCriteria($pageSize, $currentPage): \Magento\Framework\Api\SearchCriteria 667 { 668 $searchCriteria = $this->searchCriteriaBuilder 669 ->addFilter('type_id', 'simple') 670 ->addFilter('status', '1') 671 ->setPageSize($pageSize) 672 ->setCurrentPage($currentPage) 673 ->create() 674 ; 675 return $searchCriteria; 676 } 677} 678</code></pre> 679</section> 680 681<section> 682 <h3>Somewhere in your component</h3> 683 <pre><code class="php stretch" data-trim> 684namespace Conference\StockExport; 685 686use Conference\StockExport\Source\EntryGateway; 687 688class Export 689{ 690 private $entryGateway; 691 692 public function __construct(EntryGateway $entryGateway) 693 { 694 $this->entryGateway = $entryGateway; 695 } 696 697 public function export(\SplFileObject $file, $scopeId) 698 { 699 $file->fputcsv(['name', 'sku', 'qty']); 700 701 $totalPages = $this->entryGateway->getTotalPages($scopeId); 702 703 for ($page = 1; $page <= $totalPages; $page ++) { 704 foreach ($this->entryGateway->getEntriesByPage($scopeId, $page) as $entry) { 705 $file->fputcsv([$entry->getName(), $entry->getSku(), $entry->getQty()]); 706 } 707 } 708 } 709} 710 </code></pre> 711</section> 712 713<section> 714<h3>Benefits</h3> 715<ul> 716 <li class="fragment">EntryGateway is much easier to use as test dependency</li> 717 <li class="fragment">It is less fragile, as Magento glue code is isolated from your co
717de</li> 718 <li class="fragment">Easy to implement new adapter for your component</li> 719</ul> 720</section> 721 </section> 722 723 <section> 724 <section> 725 <p class="large">I want to auto-complete address by postcode and house number</p> 726 <p class="fragment">So I need to customize Magento behaviour</p> 727 </section> 728 729 <section> 730 <h2>Extension Point</h2> 731 <pre><code class="php stretch" data-trim> 732 namespace Magento\Checkout\Model; 733 734class GuestShippingInformationManagement implements \Magento\Checkout\Api\GuestShippingInformationManagementInterface 735{ 736 protected $quoteIdMaskFactory; 737 738 protected $shippingInformationManagement; 739 740 public function __construct( 741 \Magento\Quote\Model\QuoteIdMaskFactory $quoteIdMaskFactory, 742 \Magento\Checkout\Api\ShippingInformationManagementInterface $shippingInformationManagement 743 ) { 744 $this->quoteIdMaskFactory = $quoteIdMaskFactory; 745 $this->shippingInformationManagement = $shippingInformationManagement; 746 } 747 748 public function saveAddressInformation( 749 $cartId, 750 \Magento\Checkout\Api\Data\ShippingInformationInterface $addressInformation 751 ) { 752 $quoteIdMask = $this->quoteIdMaskFactory->create()->load($cartId, 'masked_id'); 753 return $this->shippingInformationManagement->saveAddressInformation( 754 $quoteIdMask->getQuoteId(), 755 $addressInformation 756 ); 757 } 758} 759</code></pre> 760 </section> 761 762 <section> 763 <h2>Abstraction</h2> 764 <pre><code class="php stretch" data-trim> 765namespace Conference\CompleteAddress\Model; 766 767interface CompletableAddress 768{ 769 public function isComplete(): bool; 770 771 public function getPostcode(): string; 772 773 public function getHouseNumber(): string; 774 775 public function complete(string $street, string $city); 776} 777 </code></pre> 778 </section> 779 780 <section> 781 <h2>Domain Logic</h2> 782 <pre><code class="php stretch" data-trim> 783namespace Conference\CompleteAddress\Model; 784 785class CompleteService 786{ 787 public function completeAdddress(CompletableAddress $address) 788 { 789 if ($address->isComplete()) { 790 return; 791 } 792 793 // .. do something with house number and postcode 794 795 $address->complete($streetName, $city); 796 } 797} 798 </code></pre> 799 </section> 800 801 <section> 802 <h2>Address Adapter</h2> 803 <pre><code class="php stretch" data-trim> 804namespace Conference\CompleteAddress\Model; 805 806use Magento\Checkout\Api\Data\ShippingInformationInterface; 807 808class MagentoCompletableAddress implements CompletableAddress 809{ 810 private $shippingInformation; 811 812 public function __construct(ShippingInformationInterface $shippingInformation) 813 { 814 $this->shippingInformation = $shippingInformation; 815 } 816 // ... other methods 817 public function complete(string $street, string $city) 818 { 819 $this->getShippingAddress()->setCity($city); 820 $this->getShippingAddress()->setStreet([ 821 $this->getShippingAddress()->getStreet()[0], 822 $street 823 ]); 824 } 825 826 private function getShippingAddress(): \Magento\Quote\Api\Data\AddressInterface 827 { 828 return $this->shippingInformation->getShippingAddress(); 829 } 830}</code></pre> 831 </section> 832 833 <section> 834 <h2>Decorator</h2> 835 <pre><code class="php stretch" data-trim> 836namespace Conference\CompleteAddress\Model; 837 838use Magento\Checkout\Api\ShippingInformationManagementInterface; 839use Magento\Checkout\Api\Data\ShippingInformationInterface; 840 841class ShippingInfromationManagementDecorator implements ShippingInformationManagementInterface 842{ 843 private $shippingInformationManagement; 844 845 private $completableAddressFactory; 846 847 private $completeService; 848 849 public function __construct( 850 ShippingInformationManagementInterface $shippingInformationManagement, 851 MagentoCompletableAddressFactory $completableAddressFactory, 852 CompleteService $completeService 853 ) { 854 $this->shippingInformationManagement = $shippingInformationManagement; 855 $this->completableAddressFactory = $completableAddressFactory; 856 $this->completeService = $completeService; 857 } 858 859 public function saveAddressInformation($cartId, ShippingInformationInterface $addressInformation) 860 { 861 $completableAddress = $this->completableAddressFactory->create( 862 ['shippingInformation' => $addressInformation] 863 ); 864 865 $this->completeService->completeAdddress($completableAddress); 866 return $this->shippingInformationManagement->saveAddressInformation( 867 $cartId, 868 $addressInformation 869 ); 870 } 871} 872 </code></pre> 873 </section> 874 875 <section> 876 <h2>Configuration</h2> 877 <pre><code class="xml stretch" data-trim> 878<?xml version="1.0"?> 879<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 880 xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> 881 882 <type name="Magento\Checkout\Model\GuestShippingInformationManagement"> 883 <arguments> 884 <argument 885 name="shippingInformationManagement" 886 xsi:type="object"> 887 Conference\CompleteAddress\Model\ShippingInformationManagementDecorator 888 </argument> 889 </arguments> 890 </type> 891</config> 892 </code></pre> 893 </section> 894 895 <section> 896 <p class="large">I want to customize Magento behaviour but there is no interface</p> 897 </section> 898 899 <section> 900 <p class="large">Than you can create a plugin</p> 901 </section> 902 903 <section> 904 <h2>Plugin</h2> 905 <pre><code class="php stretch" data-trim> 906namespace Conference\CompleteAddress\Model; 907 908use Magento\Checkout\Model\ShippingInformationManagement; 909 910class ShippingInformationManagementPlugin 911{ 912 private $completableAddressFactory; 913 914 private $completeService; 915 916 public function __construct( 917 MagentoCompletableAddressFactory $completableAddressFactory, 918 CompleteService $completeService 919 ) { 920 $this->completableAddressFactory = $completableAddressFactory; 921 $this->completeService = $completeService; 922 } 923 924 public function beforeSaveAddressInformation( 925 ShippingInformationManagement $subject, 926 $cartId, 927 ShippingInformationInterface $addressInformation 928 ) { 929 $completableAddress = $this->completableAddressFactory->create( 930 ['shippingInformation' => $addressInformation] 931 ); 932 933 $this->completeService->completeAdddress($completableAddress); 934 935 return [$cartId, $addressInformation]; 936 } 937} 938 </code></pre> 939 </section> 940 <section> 941 <h2>Configuration</h2> 942 <pre><code class="xml stretch" data-trim> 943<?xml version="1.0"?> 944<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 945 xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> 946 <type name="Magento\Checkout\Model\ShippingInformationManagement"> 947 <plugin 948 type="Conference\CompleteAddress\Model\ShippingInformationManagementPlugin" 949 name="shipping_information_management_plugin" /> 950 </type> 951</config> 952 </code></pre> 953 </section> 954 955 </section> 956 957 <section> 958 <h2>Summary</h2> 959 960 <ul> 961 <li class="fragment">Create your own abstractions and write adapter for a framework you use</li> 962 <li class="fragment">Use decorator for customizations and delegate to your domain model</li> 963 <li class="fragment">Use plugins only when no interface available for decoration</li> 964 </ul> 965 </section> 966 967 <section> 968 <h2>Learn more</h2> 969 <ul> 970 <li class="fragment">Clean Architecture by Uncle Bob. <br /><a href="https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html">bit.ly/clean-architecture</a></li> 971 <li class="fragment">Hexagonal Architecture by Alistair Cockburn <br /> <a href="http://alistair.cockburn.us/Hexagonal+architecture">bit.ly/hexagonal-architecture</a></li> 972 973 <li class="fragment">Book: Growing Object-Oriented Software, Guided by Tests by Steve Freeman & Nat Pryce</li> 974 </ul> 975 </section> 976 977 <section> 978 <h1 class="fragment current-visible" data-fragment-index="0">Thank You</h1> 979 <h2 class="fragment" data-fragment-index="2">Q&A</h2> 980 <p class="fragment twitter" data-fragment-index="2">@IvanChepurnyi</p> 981 <p class="fragment email" data-fragment-index="2">[email protected]</p> 982 </section> 983 </div> 984</div> 985 986 987
988<script> 989 addJs('/lib/js/head.min.js'); 990 addJs('/js/reveal.js', function () { 991 // Full list of configuration options available at: 992 // https://github.com/hakimel/reveal.js#configuration 993 Reveal.initialize({ 994 controls: true, 995 progress: true, 996 history: true, 997 center: false, 998 transition: 'convex', // none/fade/slide/convex/concave/zoom 999 keyboard: { 1000 66: 'toggleOverview', 1001 33: function () { 1002 if (Reveal.isOverview()) { 1003 return Reveal.navigateLeft(); 1004 } 1005 1006 return Reveal.navigatePrev(); 1007 }, 1008 34: function () { 1009 if (Reveal.isOverview()) { 1010 return Reveal.navigateRight(); 1011 } 1012 1013 return Reveal.navigateNext(); 1014 } 1015 }, 1016 // Optional reveal.js plugins 1017 dependencies: [ 1018 { src: baseUrl + '/lib/js/classList.js', condition: function() { return !document.body.classList; } }, 1019 { src: baseUrl + '/plugin/highlight/highlight.js', async: true, callback: function() { hljs.initHighlightingOnLoad(); } }, 1020 { src: baseUrl + '/plugin/zoom-js/zoom.js', async: true }, 1021 { src: baseUrl + '/plugin/notes/notes.js', async: true } 1022 ] 1023 }); 1024 1025 var fixZoom = function () { 1026 var footer = document.querySelector('.reveal > footer'); 1027 var slides = document.querySelector('.reveal > .slides'); 1028 if (footer && slides) { 1029 if (slides.style.zoom) { 1030 footer.style.zoom = slides.style.zoom; 1031 } else if (slides.style.transform) { 1032 footer.style.transform = slides.style.transform.replace(/translate\([^\)]+\)/, ''); 1033 } 1034 } 1035 } 1036 1037 Reveal.addEventListener('slidechanged', function () { 1038 fixZoom(); 1039 }); 1040 1041 Reveal.addEventListener('ready', function () { 1042 var slides = document.querySelectorAll('.reveal section'); 1043 for (var i in slides) { 1044 var slide = slides.item(i); 1045 1046 if (slide.classList.contains('title-slide')) { 1047 continue; 1048 } 1049 1050 slide.classList.add('center'); 1051 } 1052 fixZoom(); 1053 }); 1054 }); 1055</script>
1055 1056 1057</body> 1058</html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.