Are there any Software Architects?
Are there any Senior Developers?
Are there any Junior Developers?
Every developer in this room is an architect
We are all responsible for our Software Design
Architecting software is hard...
For example in the beggining of 2007
ZF1 just went into first beta (0.8.0)
Symfony 1.0 just got released
Composer?
Unit testing was not yet a thing
Everyone was using Subversion (SVN)
I wish I knew everything I know now back then
Challenge #0: Using Magento 1.x approach
Magento 2.x is a huge step ahead and learning new concepts hard at first
But these new learning curve is going to pay off in the end
A lot of people tell that Magento 2 should have used Symfony and Doctrine...
But I am happy Magento 2.x does not use Symfony and Doctine
Symfony has an excessive configuration boilerplate
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<parameters>
<parameter key="doctrine.orm.configuration.class">Doctrine\ORM\Configuration</parameter>
<parameter key="doctrine.orm.entity_manager.class">Doctrine\ORM\EntityManager</parameter>
<parameter key="doctrine.orm.manager_configurator.class">Doctrine\Bundle\DoctrineBundle\ManagerConfigurator</parameter>
<!-- cache -->
<parameter key="doctrine.orm.cache.array.class">Doctrine\Common\Cache\ArrayCache</parameter>
<parameter key="doctrine.orm.cache.apc.class">Doctrine\Common\Cache\ApcCache</parameter>
<parameter key="doctrine.orm.cache.memcache.class">Doctrine\Common\Cache\MemcacheCache</parameter>
<parameter key="doctrine.orm.cache.memcache_host">localhost</parameter>
<parameter key="doctrine.orm.cache.memcache_port">11211</parameter>
<parameter key="doctrine.orm.cache.memcache_instance.class">Memcache</parameter>
<parameter key="doctrine.orm.cache.memcached.class">Doctrine\Common\Cache\MemcachedCache</parameter>
<parameter key="doctrine.orm.cache.memcached_host">localhost</parameter>
<parameter key="doctrine.orm.cache.memcached_port">11211</parameter>
<parameter key="doctrine.orm.cache.memcached_instance.class">Memcached</parameter>
<parameter key="doctrine.orm.cache.redis.class">Doctrine\Common\Cache\RedisCache</parameter>
<parameter key="doctrine.orm.cache.redis_host">localhost</parameter>
<parameter key="doctrine.orm.cache.redis_port">6379</parameter>
<parameter key="doctrine.orm.cache.redis_instance.class">Redis</parameter>
<parameter key="doctrine.orm.cache.xcache.class">Doctrine\Common\Cache\XcacheCache</parameter>
<parameter key="doctrine.orm.cache.wincache.class">Doctrine\Common\Cache\WinCacheCache</parameter>
<parameter key="doctrine.orm.cache.zenddata.class">Doctrine\Common\Cache\ZendDataCache</parameter>
<!-- metadata -->
<parameter key="doctrine.orm.metadata.driver_chain.class">Doctrine\Common\Persistence\Mapping\Driver\MappingDriverChain</parameter>
<parameter key="doctrine.orm.metadata.annotation.class">Doctrine\ORM\Mapping\Driver\AnnotationDriver</parameter>
<parameter key="doctrine.orm.metadata.xml.class">Doctrine\ORM\Mapping\Driver\SimplifiedXmlDriver</parameter>
<parameter key="doctrine.orm.metadata.yml.class">Doctrine\ORM\Mapping\Driver\SimplifiedYamlDriver</parameter>
<parameter key="doctrine.orm.metadata.php.class">Doctrine\ORM\Mapping\Driver\PHPDriver</parameter>
<parameter key="doctrine.orm.metadata.staticphp.class">Doctrine\ORM\Mapping\Driver\StaticPHPDriver</parameter>
Doctrine is bound to its multi-vendor approach and forces code to be your data representation
/**
* @Entity @Table(name="bugs")
**/
class Bug
{
/**
* @Id @Column(type="integer") @GeneratedValue
**/
protected $id;
/**
* @Column(type="string")
**/
protected $description;
/**
* @Column(type="datetime")
**/
protected $created;
/**
* @Column(type="string")
**/
protected $status;
/**
* @ManyToOne(targetEntity="User", inversedBy="assignedBugs")
**/
protected $engineer;
/**
* @ManyToOne(targetEntity="User", inversedBy="reportedBugs")
**/
protected $reporter;
/**
* @ManyToMany(targetEntity="Product")
**/
protected $products;
// ... (other code)
}
Do not drive design by your framework of choice, use it as a tool.
Challenge #1: Looking at the core modules to create your own functionality
There are some new code, but mostly it is still good old Magento 1.x code that is going to change
Do not look at the core module for an example of implemenation, do not copy-paste the code
Use core code only as a reference
Read http://devdocs.magento.com/ for module structure guidelines
Challenge #2: Data model dependencies
Global Website Store (GWS)
Binding your data model to GWS leads to scaling issues of your implementation
Avoid existing structures where possible and inverse your dependencies
Challenge #3: Extending existing functionality
Rewrite (preference) is a bad thing
Plugin is not a good thing either
Local Dependency override is a good thing
Challenge #4: Depending on concrete implementation
Injecting concrete class from another component leads to fragile dependency
Protect your code by introducing own abstractions
I need to export stock quantities of physical product SKUs for a specific store
So I want to use external module entity repository...
Create your own abstraction and implement Magento adapter for it
namespace Conference\StockExport\Source;
interface Entry
{
public function getName();
public function getSku();
public function getQty();
}
interface EntryGateway
{
public function getTotalPages($scopeId): int;
public function getEntriesByPage($scopeId, $page): Entry[];
}
namespace Conference\StockExport\Source;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\CatalogInventory\Api\Data\StockStatusInterface;
class MagentoEntry implements Entry
{
private $product;
private $stockStatus;
public function __construct(ProductInterface $product, StockStatusInterface $stockStatus)
{
$this->product = $product;
$this->stockStatus = $stockStatus;
}
public function getName()
{
return $this->product->getName();
}
public function getSku()
{
return $this->product->getSku();
}
public function getQty()
{
return $this->stockStatus->getQty();
}
}
namespace Conference\StockExport\Source;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\CatalogInventory\Api\StockRegistryInterface;
use Magento\Framework\Api\SearchCriteriaBuilder;
use Magento\Store\Model\StoreManagerInterface;
class MagentoEntryGateway implements EntryGateway
{
private $searchCriteriaBuilder;
private $stockRegistry;
private $productRepository;
private $storeManager;
private $pageSize;
private $magentoEntryFactory;
public function __construct(
SearchCriteriaBuilder $searchCriteriaBuilder,
ProductRepositoryInterface $productRepository,
StoreManagerInterface $storeManager,
StockRegistryInterface $stockRegistry,
MagentoEntryFactory $magentoEntryFactory
) {
$this->searchCriteriaBuilder = $searchCriteriaBuilder;
$this->productRepository = $productRepository;
$this->storeManager = $storeManager;
$this->stockRegistry = $stockRegistry;
$this->pageSize = 1000;
$this->magentoEntryFactory = $magentoEntryFactory;
}
// .. next slide
}
namespace Conference\StockExport\Source;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\CatalogInventory\Api\StockRegistryInterface;
use Magento\Framework\Api\SearchCriteriaBuilder;
use Magento\Store\Model\StoreManagerInterface;
class MagentoEntryGateway implements EntryGateway
{
// .. prev slide
public function getTotalPages($scopeId): int
{
$this->storeManager->setCurrentStore($scopeId);
$searchCriteria = $this->createProductSearchCriteria(1, 1);
$searchResult = $this->productRepository->getList($searchCriteria);
return ceil($searchResult->getTotalCount() / $this->pageSize);
}
public function getEntriesByPage($scopeId, $page): \Traversable
{
$this->storeManager->setCurrentStore($scopeId);
$searchCriteria = $this->createProductSearchCriteria($this->pageSize, $page);
$searchResult = $this->productRepository->getList($searchCriteria);
foreach ($searchResult->getItems() as $productItem) {
yield $this->magentoEntryFactory->create([
'product' => $productItem,
'stockStatus' => $this->stockRegistry->getStockStatus(
$productItem->getId(),
$scopeId
),
]);
}
}
// .. next slide
}
namespace Conference\StockExport\Source;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\CatalogInventory\Api\StockRegistryInterface;
use Magento\Framework\Api\SearchCriteriaBuilder;
use Magento\Store\Model\StoreManagerInterface;
class MagentoEntryGateway implements EntryGateway
{
// .. prev slide
private function createProductSearchCriteria($pageSize, $currentPage): \Magento\Framework\Api\SearchCriteria
{
$searchCriteria = $this->searchCriteriaBuilder
->addFilter('type_id', 'simple')
->addFilter('status', '1')
->setPageSize($pageSize)
->setCurrentPage($currentPage)
->create()
;
return $searchCriteria;
}
}
namespace Conference\StockExport;
use Conference\StockExport\Source\EntryGateway;
class Export
{
private $entryGateway;
public function __construct(EntryGateway $entryGateway)
{
$this->entryGateway = $entryGateway;
}
public function export(\SplFileObject $file, $scopeId)
{
$file->fputcsv(['name', 'sku', 'qty']);
$totalPages = $this->entryGateway->getTotalPages($scopeId);
for ($page = 1; $page <= $totalPages; $page ++) {
foreach ($this->entryGateway->getEntriesByPage($scopeId, $page) as $entry) {
$file->fputcsv([$entry->getName(), $entry->getSku(), $entry->getQty()]);
}
}
}
}
I want to auto-complete address by postcode and house number
So I need to customize Magento behaviour
namespace Magento\Checkout\Model;
class GuestShippingInformationManagement implements \Magento\Checkout\Api\GuestShippingInformationManagementInterface
{
protected $quoteIdMaskFactory;
protected $shippingInformationManagement;
public function __construct(
\Magento\Quote\Model\QuoteIdMaskFactory $quoteIdMaskFactory,
\Magento\Checkout\Api\ShippingInformationManagementInterface $shippingInformationManagement
) {
$this->quoteIdMaskFactory = $quoteIdMaskFactory;
$this->shippingInformationManagement = $shippingInformationManagement;
}
public function saveAddressInformation(
$cartId,
\Magento\Checkout\Api\Data\ShippingInformationInterface $addressInformation
) {
$quoteIdMask = $this->quoteIdMaskFactory->create()->load($cartId, 'masked_id');
return $this->shippingInformationManagement->saveAddressInformation(
$quoteIdMask->getQuoteId(),
$addressInformation
);
}
}
namespace Conference\CompleteAddress\Model;
interface CompletableAddress
{
public function isComplete(): bool;
public function getPostcode(): string;
public function getHouseNumber(): string;
public function complete(string $street, string $city);
}
namespace Conference\CompleteAddress\Model;
class CompleteService
{
public function completeAdddress(CompletableAddress $address)
{
if ($address->isComplete()) {
return;
}
// .. do something with house number and postcode
$address->complete($streetName, $city);
}
}
namespace Conference\CompleteAddress\Model;
use Magento\Checkout\Api\Data\ShippingInformationInterface;
class MagentoCompletableAddress implements CompletableAddress
{
private $shippingInformation;
public function __construct(ShippingInformationInterface $shippingInformation)
{
$this->shippingInformation = $shippingInformation;
}
// ... other methods
public function complete(string $street, string $city)
{
$this->getShippingAddress()->setCity($city);
$this->getShippingAddress()->setStreet([
$this->getShippingAddress()->getStreet()[0],
$street
]);
}
private function getShippingAddress(): \Magento\Quote\Api\Data\AddressInterface
{
return $this->shippingInformation->getShippingAddress();
}
}
namespace Conference\CompleteAddress\Model;
use Magento\Checkout\Api\ShippingInformationManagementInterface;
use Magento\Checkout\Api\Data\ShippingInformationInterface;
class ShippingInfromationManagementDecorator implements ShippingInformationManagementInterface
{
private $shippingInformationManagement;
private $completableAddressFactory;
private $completeService;
public function __construct(
ShippingInformationManagementInterface $shippingInformationManagement,
MagentoCompletableAddressFactory $completableAddressFactory,
CompleteService $completeService
) {
$this->shippingInformationManagement = $shippingInformationManagement;
$this->completableAddressFactory = $completableAddressFactory;
$this->completeService = $completeService;
}
public function saveAddressInformation($cartId, ShippingInformationInterface $addressInformation)
{
$completableAddress = $this->completableAddressFactory->create(
['shippingInformation' => $addressInformation]
);
$this->completeService->completeAdddress($completableAddress);
return $this->shippingInformationManagement->saveAddressInformation(
$cartId,
$addressInformation
);
}
}
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<type name="Magento\Checkout\Model\GuestShippingInformationManagement">
<arguments>
<argument
name="shippingInformationManagement"
xsi:type="object">
Conference\CompleteAddress\Model\ShippingInformationManagementDecorator
</argument>
</arguments>
</type>
</config>
I want to customize Magento behaviour but there is no interface
Than you can create a plugin
namespace Conference\CompleteAddress\Model;
use Magento\Checkout\Model\ShippingInformationManagement;
class ShippingInformationManagementPlugin
{
private $completableAddressFactory;
private $completeService;
public function __construct(
MagentoCompletableAddressFactory $completableAddressFactory,
CompleteService $completeService
) {
$this->completableAddressFactory = $completableAddressFactory;
$this->completeService = $completeService;
}
public function beforeSaveAddressInformation(
ShippingInformationManagement $subject,
$cartId,
ShippingInformationInterface $addressInformation
) {
$completableAddress = $this->completableAddressFactory->create(
['shippingInformation' => $addressInformation]
);
$this->completeService->completeAdddress($completableAddress);
return [$cartId, $addressInformation];
}
}
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<type name="Magento\Checkout\Model\ShippingInformationManagement">
<plugin
type="Conference\CompleteAddress\Model\ShippingInformationManagementPlugin"
name="shipping_information_management_plugin" />
</type>
</config>
@IvanChepurnyi
ivan@ecomdev.org