Training for PHP developers
Training based on Magento 2.3
Get the knowledge that enables you
to work confidently with Magento
and develop your practical skills.
Quick start
A lot of extensions and integrations available.
Full extensibility
Magento 2 enables you to adjust almost everything
Support available
Rich know-how ailable on-line and huge community
Magento Developer for
E-commerce platform
Enterprise-class software for e-commerce
regardless of the scale and the intdustry
Open-source project
Software developed by contributors
from companies all around the world.
March 2007
November 2015
June 2020
Magento 1
End of life
Developers should understand that Magento 2 way of doing something is actually OOP way of doing something. We did not invent a wheel.
We just try to follow good OOP/OOD practices.
IGOR MINIAILO Magento 2 architect, part of Community Engineering Team
Magento 2 Commerce
The paid version (Subscription) that includes modules:
B2B, PageBuilder, Marketing Automation...
Magento 2 Open Source
E-commerce software available for free
with it's modules (management, payment, shipment...)
You are not allowed to reuse modules of Magento Commerce in projects that are not licensed.
How Magento 2 is developed?
Mostly by Community that use public Github repository
to submit their Pull Requests with changes.
... and Magento 2 Commerce and it's modules?
Developed by Magento Core teams, but also Partners get access
to Github copies of Commerce modules, to contribute.
🔎 Play it yourself!
Execute PHP code snippets yourself at: 3v4l.org
__construct()
__destruct()
This "magic" method is invoked when new instance of class is being instantiated.
💡 Best Practice
You can use the method to perform explicit cleanup after your code.
Avoid other operations than assignment in constructors (!)
__call(string $name, array $arguments)
This "magic" method is invoked when calling non-existing interface method.
https://www.php.net/manual/en/language.oop5.overloading.php#object.call
class MethodTest
{
public function __call($name, $arguments)
{
// Note: value of $name is case sensitive.
echo "Calling object method '$name' "
. implode(', ', $arguments). "\n";
}
}
$obj = new MethodTest;
$obj->runTest('in object context');
// Calling object method 'runTest'
// in object context
Implementation
Example
public function __call($method, $args)
{
switch (substr($method, 0, 3)) {
case 'get':
$key = $this->_underscore(substr($method, 3));
$index = isset($args[0]) ? $args[0] : null;
return $this->getData($key, $index);
case 'set':
$key = $this->_underscore(substr($method, 3));
$value = isset($args[0]) ? $args[0] : null;
return $this->setData($key, $value);
case 'uns':
$key = $this->_underscore(substr($method, 3));
return $this->unsetData($key);
case 'has':
$key = $this->_underscore(substr($method, 3));
return isset($this->_data[$key]);
}
throw new \Magento\Framework\Exception\LocalizedException(
new \Magento\Framework\Phrase('Invalid method %1::%2', [get_class($this), $method])
);
}
\Magento\Framework\DataObject
Meaning | Syntax | Juggling |
---|---|---|
Equal | $a == $b | ☑️ |
Unequal | $a != $b | ☑️ |
Identical | $a === $b | ❌ |
Not identical | $a !== $b | ❌ |
Less than | $a < $b | ☑️ |
Less or equal | $a <= $b | ☑️ |
Greater than | $a > $b | ☑️ |
Greater or equal | $a >= $b | ☑️ |
Spaceship 🚀 | $a <=> $b | ☑️ |
http://php.net/manual/en/language.operators.comparison.php
https://www.php.net/manual/en/function.bccomp.php
Safe bcmath equivalent:
bccomp(string $left_operand, string $right_operand[, int $scale = 0]): int
Equals operator ( == )
Identity operator ( === )
Two object instances are equal if they have the same attributes and values (compared with ==), and are instances of the same class
object variables are identical if and only if they refer to the same instance of the class
http://php.net/manual/en/language.oop5.object-comparison.php
"secure" == 0
// true
$_POST['password'] == "AdminPassword"
{“password”: “0”}
{“password”: 0}
// false
// true
$values = array("add","edit,"view","delete");
// true
in_array(0, $values);
https://www.php.net/manual/en/types.comparisons.php
1 == "1" // true
1 === "1" // false
0 == "0" // true
0 == "00" // false
0 == false // true
1 == true // true
0 === false // false
1 === true // false
100.0 <=> 1e2 // 0
🚀
1.75 <=> 0.5 // -1
"d" <=> "a" // 1
100 === 1e2 // false
100.0 === 1e2 // true
❗ It's not recommended to compare float due to the internal mechanism
of storing values
$a = 1.23456789;
$b = 1.23456780;
$epsilon = 0.00001; // 1e-5
function areEqual($a, $b, $epsilon) {
return abs($a-$b) < $epsilon;
}
echo [] <=> []; // 0
echo [1, 2, 3] <=> [1, 2, 3]; // 0
echo [1, 2, 3] <=> []; // 1
echo [1, 2, 3] <=> [1, 2, 1]; // 1
echo [1, 2, 3] <=> [1, 2, 4]; // -1
🚀
$a = (object) ["a" => "b"];
$b = (object) ["a" => "b"];
echo $a <=> $b; // 0
$a = (object) ["a" => "b"];
$b = (object) ["a" => "c"];
echo $a <=> $b; // -1
$a = (object) ["a" => "c"];
$b = (object) ["a" => "b"];
echo $a <=> $b; // 1
🚀
&&
and
vs
&&
vs
||
==
vs
=
🔗 http://php.net/manual/en/language.operators.precedence.php
🔗 http://php.net/manual/en/language.types.array.php
$array = array(
1 => "a",
"1" => "b",
1.5 => "c",
true => "d",
);
var_dump($array);
/*
array(1) {
[1]=>
string(1) "d"
}
*/
function login() {
if (! hasUsername()) {
return [
'success' => false,
'message' => 'Username is required'
];
}
if (! hasPassword()) {
return [
'success' => false,
'message' => 'Password is required'
];
}
if (! isPasswordCorrect()) {
return [
'success' => false,
'message' => 'Your password is invalid'
];
}
return [
'success' => true,
'message' => 'You are logged in'
];
}
echo true == $x ? 'Yes' : 'No';
if (true == $x) {
echo 'Yes';
} else {
echo 'No';
}
echo $x ?? $y;
$x = NULL;
echo $x ?? 'Alternatywa'; // Alternatywa
$x = 'Cokolwiek';
echo $x ?? 'Alternatywa'; // Cokolwiek
echo $x ?: '$x is false';
Null Coalescing Operator
Ternary operator
The special NULL value represents a variable with no value.
https://www.php.net/manual/en/language.types.null.php
https://www.php.net/manual/en/functions.arguments.php#functions.arguments.type-declaration
https://www.php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration
If the given value is of the incorrect type, then TypeError exception is being thrown.
function getSomeNumber(string $serviceCode): ?int {
if (!$this->isEnabled()) {
return null;
}
return $this->service->getServiceNumber($serviceCode);
}
Be careful with strict_type mode
🔎 Play it yourself!
learngitbranching.js.org and git-school.github.io/visualizing-git/
Working
directory
Staging
Area
Local
repository
Remote
repository
Online server
Local environment
$ git add
$ git commit
$ git push
$ git pull
$ git fetch
$ git reset [FILE]
$ git reset [COMMIT]
Feature
Master
Feature
Master
Feature
Master
Feature
Master
🕷️
Master
A
B
D
A
B
D
Feature
Master
A
B
C
D
Feature
Master
A-D
+
?
?
https://github.com/markshust/docker-magento
Run your environment according to the instructions
Remember about adding magento2.test to /etc/hosts
Make sure, that https://magento2.test serves Magento Store
Contianer's name
Exposed TCP ports
Links between services
Declaration of storage resources
Service name
Contianer's name
Exposed TCP ports
Links between services
Declaration of storage resources
Service name
1. It's integrated with code (reacts to Code changes)
2. Thousands of extensions available (paid and free)
3. Evolves with PHP language (supports latest solutions)
1. Set up PHP interpreter
2. Set PSR-2 coding standard
3. Configure "Schemas and DTDs"
💡 Best Practice
We recommend installing "Magento 2" extension, which is available for free!
Configure your favourite Database GUI
or Database plugin in PhpStorm
To connect your development environment.
A dependency manager for PHP.
Equivalent to apt in Debian or npm for Node JS
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
php composer-setup.php
php -r "unlink('composer-setup.php');"
sudo mv composer.phar /usr/local/bin/composer
If you want to use composer globally in environment, run:
https://getcomposer.org/doc/00-intro.md
composer install
composer update
composer require
Installs packages exactly in the version provided in composer.lock
Installs the latest versions for packages defined in .json
If .lock file does not exist, it's going to be created.
.lock file is going to be updated
Adds dependency to
composer.json
and installs it
https://getcomposer.org/doc/03-cli.md
.lock file is going to be updated
Find on GitHub the name of package with Polish translation
https://github.com/magento-l10n/language-pl_PL/
and install the package with Composer
{
"name": "symfony/symfony",
"type": "library",
"license": "MIT",
...
"require": {
"php": "^7.1.3",
"ext-xml": "*",
"doctrine/common": "~2.4",
...
},
"require-dev": {
"cache/integration-tests": "dev-master",
"doctrine/annotations": "~1.0",
"doctrine/cache": "~1.6",
...
},
...
"autoload": {
"psr-4": {
"Symfony\\Bridge\\Doctrine\\": "src/Symfony/Bridge/Doctrine/",
"Symfony\\Bridge\\Monolog\\": "src/Symfony/Bridge/Monolog/",
"Symfony\\Bridge\\ProxyManager\\": "src/Symfony/Bridge/ProxyManager/",
...
},
}
}
Information about package / project, that composer.json describes
Dependencies required to run project/package
Dependencies required for development
Autoloader configuration
https://getcomposer.org/doc/04-schema.md
[ Major ] . [ Minor ] . [ Patch ]
incompatible API changes
add functionality in a backwards compatible manner
backwards compatible bug fixes
https://semver.org/
"doctrine/common": "2.4"
Exact version constraint:
"doctrine/common": "~2.4"
Major version constraint:
"doctrine/common": "2.4.*"
Version range:
"doctrine/common": ">=1.1 <2.0"
Git branch reference:
"doctrine/common": "dev-{my-feature}"
Global: Minimum-stability setting
{
"minimum-stability": "stable"
}
Local: Definition with @ - eg. @beta
{
"require": {
"monolog/monolog": "1.0.*@beta",
"m2coach/magic": "@dev"
}
}
Backward-compatible best practices
If you implement one of such interfaces, remember about using proper version constraint.
Application Programming Interface
Service Provider Interface
API | SPI | API & SPI | |
---|---|---|---|
Removing Behaviour | ❌ | ✔️ | ❌ |
Modification of existing behaviour | ❌ | ❌ | ❌ |
Adding new behaviour | ✔️ | ❌ | ❌ |
https://community.magento.com/t5/Magento-DevBlog/Best-Practices-for-API-Design/ba-p/119868
~7.0.0
~7.1.3
~7.2.0
~7.3.0
Magento 2.3.3
(Magento 2.3.4)
https://www.php.net/supported-versions.php
https://devdocs.magento.com/guides/v2.3/install-gde/system-requirements-tech.html
ext-bcmath
ext-ctype
ext-curl
ext-dom
ext-gd
ext-hash
ext-iconv
ext-int
ext-mbstring
ext-openssl
ext-pdo_mysql
ext-simplexml
ext-soap
ext-xsl
ext-zip
lib-libxml
Open Magento 2 repository on Github
Check the contents of `composer.json` file.
Verify how PHP modules are specified.
(Magento 2.3)
2.4
1.x
https://devdocs.magento.com/guides/v2.3/install-gde/system-requirements-tech.html
(Magento 2.3)
5.6 / 5.7
10.0 / 10.1 / 10.2
5.7
https://devdocs.magento.com/guides/v2.3/install-gde/system-requirements-tech.html
.
|- app
| |- code
| | | ...
| |- design
| | | ...
| |- etc
| | |- config.php
| | |- env.php.sample
|- patches
|- composer.json
|- composer.lock
|- robots.txt
|- Gruntfile.js
|- package.json
Sample GitIgnore files
github.com/davidalger/warden-env-magento2/blob/develop/.gitignore
gist.github.com/yireo/744edcb4c70d33514ca35ebb14902056
github.com/magento/magento2/blob/2.4-develop/.gitignore
<Magento Directory>
app/code
app/design/frontend
app/design/adminhtml
app/i18n
Local non-reusable modules
Themes used by Storefront
Themes used by Admin Panel
Translation files for Store
vendor
External modules and libraries
<Magento Directory>
generated
var/log
var/view_preprocessed
var/cache
Generated code
Default log path
Pre-compiled frontend assets
Default path for file cache
var/di
Dependency Injection cache
pub
Files available from web
- DB connection configuration
- Magento Framework files
- Logs of current environment
- DB connection configuration - /app/etc/env.php
- Magento Framework files - /vendor/magento/
- Logs of current environment - /var/log/
admin:user:create
cron:run
indexer:reindex
Creates Admin user
Runs cron jobs once
Reindex
maintenance:status
Returns maintenance mode
module:status
Status of modules installed
info:adminuri
Returns Admin Panel path
Use bin/magento to create new Administrator's account.
1. With interactive mode
2. Provide all the required data as argument
Find the path of Magento Admin Panel
cache:clean
setup:upgrade
config:set
Clean only current store's cache
Installation and upgrade of modules
Set the configuration value
i18n:collect-phrases
Parse files for strings to translate
module:enable
Enable module (without running installation)
cache:flush
Flush whole cache (all scopes)
developer
production
default
maintenance
Access to the store is forbidden.
Turned on with /var/.maintenance.flag file
Code needs to be already compiled, cache settings are locked.
Errors are being saved to the logs. Static files are served from cache.
Code is compiled automatically on a runtime with static files generation.
Errors are being displayed on front.
Code is compiled automatically on a runtime with static files generation.
Exceptions are being saved to logs.
https://devdocs.magento.com/guides/v2.3/config-guide/bootstrap/magento-modes.html
Verify what mode is your Magento running now
and which mechanisms are being used by it.
Change to production mode. Try to load the storefront.
Layout
Block HTML
Configuration
Compiled page layouts from all components and themes.
Includes: default.xml, x_y_z.xml
Collects configuration of all modules from different sources.
Includes: config.xml and core_config_data.
Caches HTML output from blocks.
Includes: Rendered blocks and CMS blocks.
Page cache
https://devdocs.magento.com/guides/v2.3/config-guide/cli/config-cli-subcommands-cache.html
Generated HTML pages output.
Does not include: Customer and Checkout pages
Translations
Merged translations from all modules.
Includes: en_US.csv and other *.csv translations
https://youtu.be/3ZvnBTK8uUM
The Ultimate Guide to Caching in Magento 2 | John Hughes
https://docs.google.com/presentation/d/1NdtNz_LBxk-JsCBy8AvekZAkSPWOE0WCzE14Y9ki-5Q
Cache | Recommendation | Key |
---|---|---|
Configuration | Never disable. Clean only when needed. | config |
Layout | Disable during most frontend tasks | layout |
Block HTML | Leave on most of frontend tasks, disable when working with blocks | block_html |
Translate | Disable if making many translations or clean occasionally | translate |
Full Page Cache | Disable for almost all frontend tasks, enable for testing mainly | full_page |
Check status
bin/magento cache:status
Disable cache
bin/magento cache:disable {key}
Mage2.tv to remove only the cache affected
adminhtml
frontend
base
cron
Magento Admin
Storefront
Fallback for Admin and Storefront
Cron Jobs
webapi_rest
REST API
webapi_soap
SOAP API
https://devdocs.magento.com/guides/v2.3/architecture/archi_perspectives/components/modules/mod_and_areas.html
💡 Best Practice
Declare `di.xml` for specific scope
https://docs.magento.com/m2/ce/user_guide/stores/store-scope-reference.html
https://docs.magento.com/m2/ce/user_guide/stores/store-mode-single.html
Subcategories, Locale: Currency, Language
Root category
Domain, Customers, Currency, Products,
Prices, Payment Methods, Checkout
<?php
use \Magento\Framework\Component\ComponentRegistrar;
ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Vendor_Module', __DIR__);
registration.php
etc/module.xml
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
<module name="Vendor_Module" setup_version="0.1.0">
<sequence>
<module name="Magento_Eav"/>
</sequence>
</module>
</config>
bin/magento setup:upgrade
• Turns on all registered modules
• Execute all installation / upgrade scripts
bin/magento module:enable Vendor_Module
• Turns on single module
• Does not execute installation / upgrade scripts
{
"name": "vendor/module",
"description": "N/A",
"require": {
"magento/framework": "102.0.*",
"php": "~7.1.3||~7.2.0||~7.3.0"
},
"type": "magento2-module",
"autoload": {
"files": [
"registration.php"
],
"psr-4": {
"Vendor\\Module\\": ""
}
},
"version": "0.1.0"
}
composer.json
etc/language.xml
<language xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:App/Language/package.xsd">
<code>en_US</code>
<vendor>Magento</vendor>
<package>en_US</package>
</language>
Create required files in app/code
Make sure that your module is displayed in
bin/magento module:status
across other installed modules.
<?php
use \Magento\Framework\Component\ComponentRegistrar;
ComponentRegistrar::register(ComponentRegistrar::THEME, 'frontend/Vendor/name', __DIR__);
registration.php
etc/theme.xml
<theme xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Config/etc/theme.xsd">
<title>Magento Luma</title>
<parent>Magento/blank</parent>
<media>
<preview_image>media/preview.jpg</preview_image>
</media>
</theme>
etc/view.xml
Theme confiruation (image sizes, js bundling settings etc.)
i18n/xx_XX.csv
Custom Theme translation overrides
web/
Theme specific public files (eg. css/ for styles, js/ for Javascript)
Area / Package Name / Theme Name
- adminhtml/Magento/backend - frontend/Magento/blank - frontend/Magento/luma
<Vendor>_<Module>/
eg. Magento_Cms/
Templates
template not found?
template not found?
template not found?
template not found?
<preference for="Magento\Module\Model\SomeClass"
type="M2Coach\Module\Module\MyImplementation"/>
<preference for="Magento\Module\Api\SomeInterface"
type="M2Coach\Module\Module\MyImplementation"/>
Replacing existing implementation
Preference of interface implementation
Configured using di.xml file located in <Module>/etc/ directory.
Vendor decided to disable the possibility of registration
Make sure that Store Visitors won't be able to register.
Hint: Magento\Customer\Model\Registration::isAllowed
Common logical domain and Subtype of parent class
Example: Product Type
Example: Value Object (Data Object)
Downloadable Product
E-book
Company
DataObject
Video Course
Sales Representative
__construct( . . . )
Psr\Log\LoggerInterface
Magento\Framework\Event\ManagerInterface
Magento\Catalog\Api\Data\ProductInterface
public function __construct(
\Magento\Catalog\Api\ProductRepositoryInterface $productRepository
) {
$this->productRepository = $productRepository;
}
// ...
public function getProductBySku(string $productSku): ProductInterface
{
return $this->productRepository->get($productSku);
}
🔗 https://edux.pjwstk.edu.pl/mat/205/lec/Wyklad-MAS-nr-06.html
Change to original object (Event) notifies other object (Observer),
that enables you to create 1:n dependency.
+ Event Publisher is not
coupled with Observer
+ Observers can be
dynamically attached
XML declaration
<event name="controller_action_predispatch">
<observer name="MyCustomerLogger"
instance="M2Coach\Customer\Observer\LogCustomerAction" />
</event>
class LogCustomerAction implements ObserverInterface {
public function execute(Observer $observer) {
$event = $observer->getEvent();
$customer = $event->getCustomer();
$this->logger->log(
__('Customer %1 performed %2', $customer->getId(), $event->getAction())
);
}
}
ObserverInterface implemantation
According to the Best Practices, every event
should be written the way that we can store it in a queue,
and then run in independent from the main process.
In practice, this means that
Event Observer should treat all received payload
as immutable - it's forbidden to modify the data.
Given that you write integration with external ERP
Immediately after placing an order, you want to notify ERP
about placed order (Order ID, SKU and Quantity)
Create Observer that is going to write information
about placed order in the log files.
This Design Pattern enables you to dynamically add the code between the called method and final Object using Proxy. As a result, you can replace arguments, values returned by the method or completely replace the original method.
// **Before** - Replace method arguments
public function beforeExecute(CreatePost $subject, $first, $second, $last) {
return [$first, 'secondReplaced', $last];
}
// **After** - Replace the method results
public function afterExecute(CreatePost $subject, $methodResult) {
...
return $modifiedMethodResult;
}
// **Around Plugin** - Interrupt the original method call
public function aroundExecute(CreatePost $subject, Closure $proceed, ...$args) {
return $this->myExecute($args);
}
<type name="Magento\Customer\Controller\Account\CreatePost">
<plugin name="customerAccountCreatePostRedirect"
type="M2Coach\Customers\Plugin\CreatePostRedirectionPlugin"/>
</type>
Configured using di.xml file located in <Module>/etc/ directory.
Having multiple Plugins, the order of exectuion
both for before and after plugin follows sortOrder
("after" plugin changed after Magento 2.0)
❗ Your plugin should be completely independent from other plugins
The exception is around plugin:
You need to be cautious!
The Plugin precedence
is not always obvious!
❗
According to best practices
around plugin should be used
only to interrupt
original method execution
In any other case
only before or after plugin
should be used
Your client asked you to block the registration from `mailinator.com` domains using front-end. He still wants to have such possibility using Administrator Panel.
Change the behaviour of Controller to avoid registration.
Using previously introduced Admin Configuration
enable your module to use the configuration values.
Hint: To fetch the configuration values, use ScopeConfigInterface
<tab/>
<section/>
<group/>
<field/>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
<system>
<section id="customer">
<group id="create_account">
<field id="enabled" translate="label" type="select"
sortOrder="5" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Enable Customer Registration</label>
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
</field>
</group>
</section>
</system>
</config>
Reference: customer/create_account/enabled
<field type="select">
<source_model>...</source_model>
</field>
<field type="multiselect">
<source_model>...</source_model>
</field>
<field type="text">
<validate>validate-zero-or-greater</validate>
</field>
<field type="textarea">
<validate>required-entry</validate>
</field>
<field type="password"/>
<field type="obscure">
<backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model>
</field>
/lib/internal/Magento/Framework/Data/Form/Element/Factory.php#L26
Create configuration Section for our
Forbid Registration by e-mail feature.
Fields:
1. Enabled
2. The domain
3. Is the domain whitelist or blacklist
Whitelist / Blacklist requires to implement OptionSourceInterface
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd">
<default>
<customer>
<create_account>
<enable>1</enable>
</create_account>
</customer>
</default>
</config>
Defaults file: config.xml
To the Settings you've just created,
make sure that by default feature is disabled,
but the default domain is `mailinator.com`,
and the domain name is blacklisted.
<type name="Magento\Framework\Console\CommandListInterface">
<arguments>
<argument name="commands" xsi:type="array">
<item name="myCommand" xsi:type="object">M2Coach\Module\Console\Command\MyCommand</item>
</argument>
</arguments>
</type>
Notice that DI is not verifying if the argument against constructor
For the purpose of workshop,
we need the CLI command: `my:heavy:command`.
Extend <arguments> at CommandListInterface
<virtualType name="M2Coach\MyModule\Model\DataObjectVirtual"
type="Magento\Framework\DataObject">
<arguments>
<argument name="data" xsi:type="array">
<item name="my_data" xsi:type="string">this is string</item>
</argument>
</arguments>
</virtualType>
<type name="M2Coach\MyModule\Console\Command\SomeCommand">
<arguments>
<argument name="data_container"
xsi:type="object">M2Coach\MyModule\Model\DataObjectVirtual</argument>
</arguments>
</type>
We need to introduce EmailBlockerService for this task
We would like to know what e-mail addresses were locked
Such events should be saved in /var/log/nasty.log.
Hint: \Magento\Framework\Logger\Handler\Base
Hint: \Magento\Framework\Logger\Monolog
<type name="Magento\Module\Console\Command\MyCommand">
<arguments>
<argument name="someService"
xsi:type="object">M2Coach\MyModule\Service\MySomeService</argument>
</arguments>
</type>
1. Proxy implements Interface
2. The Constructor takes the original class
as a parameter
3. In most of the cases, Proxy passes
the requests to original class
4. Life cycle of Proxy class ends
with original class
➕ Access control to unaware class
➕ Target class does not have to be instantiated
➖ Additional delay on response
class VeryHeavyClass {
public function __construct(ApiBasedInterface $apiBased) {
$this->someData = $apiBased->getSomeData();
}
}
<type name="M2Coach\Module\Model\CustomerModel">
<arguments>
<argument name="heavy" xsi:type="object">Vendor\Module\VeryHeavyClass\Proxy</argument>
</arguments>
</type>
Use `\Proxy` for MyHeavyDependency in `di.xml`
High Availability pattern
Environment alignment
Blue and Green are serving the same version of software, have the same settings
New version deployment
At [n+1] environment (Blue) we execute deployment of new version of application. When we’re sure that deployment has finished successfully, and services are working properly - we switch the traffic to [n+1] (Blue). Otherwise we switch back to [n] (Green).
Spreading new version
Last step, when we are sure that service works properly, we spread the new version across all the machines.
Following implementations
Based on exactly the same procedure.
After deployment we have possibility to previous, stable versions using load-balancer.
Front Name / Action Path / Action Name
Front Name / Action Path / Action Name
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
<router id="standard">
<route id="catalog" frontName="catalog">
<module name="Magento_Catalog" />
</route>
</router>
</config>
Configuration in etc/[area]/routes.xml
Example: Magento/Catalog/etc/frontend/routes.xml
Front Name / Action Path / Action Name
Action path declares path to access action
For Vendor\Module\Controller\Showrooms\List
it becomes: Showrooms
Front Name / Action Path / Action Name
Action name
For Vendor\Module\Controller\Showrooms\List
becomes List
but
Calling POST company/showrooms/list
action name becomes ListPost
Create endpoint m2coach/registration/lock
which returns information in JSON format:
- Whether Registration lock is enabled
- Which domains are currently forbidden
Hint: etc/[area]/routes.xml
Hint: Magento\Framework\Controller\Result\Json
Hint: Magento\Framework\App\Action\Action
Factory is an interface used to create
a new instance of a specified interface.
In Magento 2 factories are auto-generated
+ Allows creating multiple instances
of the same interface
+ Handles DI preferences seamless
- Additional class is being generated
The most common use-case for Factories are stateful classes:
$product = $this->productFactory->create();
$product->setTypeId('simple')
->setName('Simple Product')
->setSku('simple-product')
->setVisibility(Visibility::VISIBILITY_BOTH)
->setStatus(Status::STATUS_ENABLED)
->setStockData([
'use_config_manage_stock' => 1,
'qty' => 100,
'is_qty_decimal' => 0,
'is_in_stock' => 1,
]);
$this->productRepository->save($product);
Example: Creating new product
- Controller Action Response
- New Entities
- Collections
XML file located in Module's view/frontend/layout
It's name follows {route id}_{action path}_{action name} pattern
<page layout="2columns-left" xmlns:xsi="..."
xsi:noNamespaceSchemaLocation="...">
<body>
<referenceContainer name="root">
...
</referenceContainer>
</body>
</page>
1column
2columns-left
2columns-right
3columns
Blocks are using Template, which is usually PHTML file
located in Module's view/frontend/template.
Templates have access to $block variable - which is reference to block class
<block class="M2Coach\MyModule\Block\Data" name="myBlock"
template="M2Coach_MyModule::template/file.phtml"/>
Block declaration in layout
Block needs to inherit \Magento\Framework\View\Element\Template
https://devdocs.magento.com/guides/v2.3/frontend-dev-guide/layouts/xml-instructions.html
vendor/bin/phpunit -c dev/tests/unit/phpunit.xml.dist
State-based testing
Interaction testing
Given Testing conditions
When Performing actions
Then Verification of results
Given Testing conditions
Expect Specified actions
When Performing action
Stub
Mock
Prepared response
Interaction verifying
$this->user->method('getUsername')
->willReturn('some@e-mail.com');
$this->user->expects($this->once())
->method('save')
->with($userStub);
Current time / date
System specific (eg. \n vs \r\n)
File-based
vendor/bin/phpunit -c $(pwd)/dev/tests/integration/phpunit.xml
All the necessary files are located in
dev/tests/integration/
... but configure them first!
Use dev/tests/integration/phpunit.xml.dist as a template.
<const name="TESTS_MEM_USAGE_LIMIT" value="1024M"/>
Recommended memory limit is 8G
(enough to run most of Magento Core tests)
<const name="TESTS_MAGENTO_MODE" value="developer"/>
Run your tests both in both modes
(with dynamic class generation and pre-generated classes)
etc/install-config-mysql.php
That configuration file should contain all the settings necessary to run your test environment. Options are compatible with `bin/magento setup:install` options.
etc/config-global.php
This is your container for configuration used for the test runtime.
Its role is the same that app/etc/config.php
Notice: Database, cache and queue configration should be different from your development environment. Otherwise, Integration tests are going to wipeout your workspace.
/**
* @magentoDataFixture Magento/Customer/_files/customer_confirmation_config_enable.php
* @magentoDataFixture Magento/Customer/_files/unconfirmed_customer.php
*
* @return void
*/
public function testSubscribeUnconfirmedCustomerWithoutSubscription(): void
{
$customer = $this->customerRepository->get('unconfirmedcustomer@example.com');
$subscriber = $this->subscriberFactory->create();
$subscriber->subscribeCustomerById($customer->getId());
$this->assertEquals(Subscriber::STATUS_UNCONFIRMED, $subscriber->getStatus());
}
Each and every module should define it's own Service Contract:
Set of public interfaces fulfilling the business needs.
Service Contract should be located in Api/ directory
• Data Interface
• Service Interface
• Repository Interface
• Management Interface (common practice to use with Repository Interface)
https://twitter.com/VinaiKopp/status/1008998146985349120/photo/1
<preference for="M2coach\Module\Api\Data\StoreInterface"
type="M2coach\Module\Model\Store"/>
namespace M2Coach\Module\Api\Data;
interface StoreInterface {
public function getName(): string;
public function setName(string $name): void;
}
etc/di.xml
Api/Data/StoreInterface.php
namespace M2Coach\Module\Model;
class Store implements \M2Coach\Api\Data\StoreInterface {
public function getName(): string {
return $this->getData('name');
}
public function setName(string $name): void {
$this->setData('name', $name);
}
}
Api/Data/StoreInterface.php
🔗 https://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html
$ curl -v google.com
> GET / HTTP/1.1
> Host: google.com
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 301 Moved Permanently
< Location: http://www.google.com/
< Content-Type: text/html; charset=UTF-8
< Date: Mon, 18 Jun 2018 22:29:54 GMT
< Expires: Wed, 18 Jul 2018 22:29:54 GMT
< Cache-Control: public, max-age=2592000
< Server: gws
< Content-Length: 219
<
<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>301 Moved</TITLE></HEAD><BODY>
<H1>301 Moved</H1>
The document has moved
<A HREF="http://www.google.com/">here</A>.
</BODY></HTML>
Method, Request URI
Server name
User Agent
Expected response format
Request
Response header
Status Code + description
Response body
🔗 https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
1** Informational
2** Successful
3** Redirection
4** Client Error
5** Server Error
200 OK
301 Moved Permanently
302 Found
304 Not Modified
400 Bad Request
401 Unauthorized
403 Forbidden
404 Not Found
405 Method Not Allowed
408 Request Timeout
413 Request Entity Too Large
500 Internal Server Error
502 Bad Gateway
503 Service Unavailable
🔗 https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html
GET
Fetch the remote resource
Parameters are passed with url query (np. ?x=5&y=10)
POST
Creates new resource on remote server.
Parameters are passed in a request body.
PUT
Update of existing resource on server.
Parameters are passed in a request body.
DELETE
Removes resource on a remote server. Disabled by default.
HEAD
Similar to GET. Returns only headers with response body.
Generated based on docblocks
https://devdocs.magento.com/guides/v2.3/javascript-dev-guide/javascript/js_init.html
1
<div data-mage-init='{"Vendor_Module/script-name": {"argument": "value"}}'></div>
Apply JS for specified element
2
<script type="text/x-magento-init">
{
// components initialized on the element defined by selector
"<element_selector>": {
"<js_component>": ...
},
// components initialized without binding to an element
"*": {
"<js_component3>": ...
}
}
</script>
Apply globally or using CSS selector
https://devdocs.magento.com/guides/v2.3/javascript-dev-guide/javascript/js_init.html
Should be located in view/{area}/web/js/
require([
// Dependencies to be used
'jquery',
], function ($) {
$(function () {
console.log('Page is loaded!');
})
});
https://devdocs.magento.com/guides/v2.3/javascript-dev-guide/javascript/js_init.html
$.ajax({
url: 'https://www.example.com',
method: 'POST',
data: {
id: '1'
},
success: function (data) {
$('.example-element').html(data)
.trigger('contentUpdated')
}
});
Interfaces to implement (\Magento\Framework\Setup\...)
In etc/db_schema.xml files
<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
<table name="catalog_product_entity" resource="default" engine="innodb" comment="Catalog Product Table">
<column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true"
comment="Entity ID"/>
...
<constraint xsi:type="foreign" referenceId="CAT_PRD_ENTT_DEC_ENTT_ID_CAT_PRD_ENTT_ENTT_ID"
table="catalog_product_entity_decimal" column="entity_id"
referenceTable="catalog_product_entity" referenceColumn="entity_id" onDelete="CASCADE"/>
</table>
</schema>
https://devdocs.magento.com/guides/v2.3/extension-dev-guide/declarative-schema/db-schema.html
Create a database table which contains information
about physical stores and it's locations.
Fields we need so far
- ID
- City
- Street
- Phone
https://devdocs.magento.com/guides/v2.3/extension-dev-guide/declarative-schema/migration-commands.html
bin/magento setup:install --convert-old-scripts=1 bin/magento setup:upgrade --convert-old-scripts=1
Patch\DataPatchInterface (DataPatchInterface)
Patch\SchemaPatchInterface (SchemaPatchInterface)
Patch\PatchRevertableInterface (PatchRevertableInterface)
Interfaces to implement (\Magento\Framework\Setup\Patch\...)
https://devdocs.magento.com/guides/v2.3/extension-dev-guide/declarative-schema/data-patches.html
https://devdocs.magento.com/guides/v2.3/extension-dev-guide/declarative-schema/data-patches.html
bin/magento setup:db-declaration:generate-patch
[options] <module-name> <patch-name>
bin/magento setup:db-declaration:generate-patch M2Coach_MyModule SomePatch
Using Data Patch - install 5 sample stores
to the table you have just created.
bin/magento module:uninstall Vendor_ModuleName
or
bin/magento module:uninstall --non-composer Vendor_ModuleName
--safe-mode=1
Creates a data dump during the installation or upgrade process.
--data-restore=1
(Used with the setup:upgrade command only.) Performs a rollback. Before you rollback, you must first check out code to the previous version of Magento. Then run setup:upgrade --data-restore=1.
https://devdocs.magento.com/guides/v2.3/extension-dev-guide/declarative-schema/migration-commands.html
Data is stored vertically in RDB
1. Attributes are declared in eav_attribute table.
2. The way of storing values depends on backend_type.
• static - Column in main entity table (eg. w product_entity)
• In all other cases - saved as a row in a table specific for type
(eg. product_entity_varchar for backend_type: varchar)
• Customer
• Customer Address
• Catalog Product
• Catalog Category
• Order
• Invoice
• Credit Memo
• Shipment
$eavSetup = $this->eavSetupFactory->create(['setup' => $setup]);
$eavSetup->addAttribute(
\Magento\Catalog\Model\Product::ENTITY,
'sample_attribute',
[
'type' => 'text',
'backend' => 'Magento\Eav\Model\Entity\Attribute\Backend\ArrayBackend',
'frontend' => '',
'label' => 'Sample Atrribute',
'input' => 'multiselect',
'option' => ['values' => ["Cokolwiek","Innekolwiek"]]
'class' => '',
'source' => '',
'global' => \Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface::SCOPE_GLOBAL,
'visible' => true,
'required' => true,
'user_defined' => false,
'default' => '',
'searchable' => false,
'filterable' => false,
'comparable' => false,
'visible_on_front' => false,
'used_in_product_listing' => true,
'unique' => false,
'apply_to' => ''
]
);
https://devdocs.magento.com/guides/v2.3/extension-dev-guide/attributes.html
$eavSetup = $this->eavSetupFactory->create(['setup' => $setup]);
$eavSetup->addAttribute(
\Magento\Customer\Model\Customer::ENTITY,
'sample_attribute',
[
'type' => 'varchar',
'label' => 'Sample Attribute',
'input' => 'text',
'required' => false,
'visible' => true,
'user_defined' => true,
'position' => 999,
'system' => 0,
]
);
$sampleAttribute = $this->eavConfig->getAttribute(Customer::ENTITY, 'sample_attribute');
// more used_in_forms
// ['adminhtml_checkout','adminhtml_customer','adminhtml_customer_address','customer_account_edit',
// 'customer_address_edit','customer_register_address']
$sampleAttribute->setData(
'used_in_forms',
['adminhtml_customer']
);
$sampleAttribute->save();
https://devdocs.magento.com/guides/v2.3/extension-dev-guide/attributes.html
Flat Tables are enables you to improve the application performance
by generating tables for entities that are based on EAV models with attributes.
When it comes to products, depending on the settings
and the size of index (SKU amount), flat tables are updated:
- up to 50.000 SKU - immeditely
- over 50.000 SKU - with Cron jobs
namespace M2Coach\Module\Model\ResourceModel;
class MyEntity extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb
{
public const PRIMARY_KEY = 'my_entity_id';
public const TABLE = 'my_entity_table';
protected function _construct() {
$this->_init(self::TABLE, self::PRIMARY_KEY);
}
}
namespace M2Coach\Module\Model\ResourceModel\MyEntity;
use Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection;
class Collection extends AbstractCollection
{
protected $_idFieldName = 'my_entity_id';
protected function _construct() {
$this->_init(
'M2Coach\Module\Model\MyEntity',
'M2Coach\Module\Model\ResourceModel\MyEntity'
);
}
}
You already created table that contains Physical Stores data.
Build Collection that enables you to fetch the data from table.
https://devdocs.magento.com/guides/v2.3/extension-dev-guide/attributes.html
Repository is an interface that fulfills very narrow area of business logic.
According to the best practices, these interfaces should contain
very simple access methods - for example:
• getItems(SearchCriteria $criteria)
• getById(int $entityId)
• save(EntityInterface $entity)
• delete(EntityInterface $entity)
Create set of interfaces for physical stores:
- Single Store data interface
- Physical Stores repository interface
Implement these methods
Hint: Best practices can be found at Vinai's blog
http://vinaikopp.com/2017/02/18/magento2_repositories_interfaces_and_webapi/
Builder is an interface used to create
an instance based on provided criteria.
In Magento builders are not generated,
you need to implement them.
+ Reusability of construction code when
building various representations
+ Single Responsibility: Isolate complex
construction code from the business logic
- Increased code complexity due to
need of creating new instances of classes
$this->searchCriteriaBuilder->addFilter(OrderItemInterface::ORDER_ID, $searchForIds, 'in');
$searchCriteria = $this->searchCriteriaBuilder->create();
https://devdocs.magento.com/guides/v2.3/extension-dev-guide/searching-with-repositories.html
https://devdocs.magento.com/guides/v2.3/extension-dev-guide/searching-with-repositories.html
"eq" => equalValue "neq" => notEqualValue "like" => likeValue "nlike" => notLikeValue "is" => isValue "in" => inValues "nin" => notInValues "notnull" => valueIsNotNull "null" => valueIsNull "moreq" => moreOrEqualValue "gt" => greaterValue "lt" => lessValue "gteq" => greaterOrEqualValue "lteq" => lessOrEqualValue "finset" => valueInSet "from" => fromValue, "to" => toValue
Use SearchCriteriaBuilder to find the stores
that are active (status = 1) and are located in Warsaw or Glasgow.
Connection endpoint:
https://magento2.test/graphql
{
products(
filter: { sku: { eq: "24-WB01" } }
) {
items {
name
sku
image {
url
}
categories {
name
}
}
}
}
Recommended tool for handling GraphQL requests is Chrome iQL
Request
{
"data": {
"products": {
"items": [
{
"name": "Voyage Yoga Bag",
"sku": "24-WB01",
"categories": [
{
"name": "Gear",
"url_path": "gear"
},
{
"name": "Bags",
"url_path": "gear/bags"
}
]
}
]
}
}
}
Response
The mechanism responsible for running repetitive tasks on schedule.
bin/magento cron:install
bin/magento cron:install --non-optional
Starting from Magento 2.3.4
Performance overhead
bin/magento cron:run [--group="<cron group name>"]
https://devdocs.magento.com/guides/v2.3/config-guide/cli/config-cli-subcommands-cron.html
https://devdocs.magento.com/guides/v2.3/config-guide/cron/custom-cron-tut.html
etc/cron.xml
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Cron:etc/crontab.xsd">
<group id="default">
<job name="m2coach_own_task"
instance="M2Coach\Module\Cron\DoSomething" method="execute">
<schedule>* * * * *</schedule>
</job>
</group>
</config>
https://devdocs.magento.com/guides/v2.3/config-guide/cron/custom-cron-tut.html
namespace M2Coach\MyModule\Cron;
use Magento\Framework\Lock\LockManagerInterface;
class CustomTask
{
public function execute(): void
{
// Do your job!
}
}
\Magento\Framework\Lock\LockManagerInterface
Avoid tasks overlapping
Module LESS file location: view/frontend/web/css/source/_module.less
https://devdocs.magento.com/guides/v2.3/frontend-dev-guide/responsive-web-design/rwd_css.html
Including files (eg. from subdirectory)
@import "_feature/_header";
Using predefined variables
color: @theme-primary-color;
Predefined variables can be found at
lib/web/css/source/lib/variables/
.lib-icon-font(
@_icon-font-content: @icon-search,
@_icon-font-size: 35px,
@_icon-font-line-height: 33px,
@_icon-font-color: @header-icons-color,
@_icon-font-color-hover: @header-icons-color-hover,
@_icon-font-color-active: @header-icons-color-hover,
@_icon-font-text-hide: true
);
Using LESS libraries
https://devdocs.magento.com/guides/v2.3/frontend-dev-guide/responsive-web-design/rwd_css.html
Breakpoint | Width |
---|---|
@screen__xxs | 320px |
@screen__xs | 480px |
@screen__s | 640px |
@screen__m | 768px |
@screen__l | 1024px |
@screen__xl | 1440px |
& when (@media-common = true) {
// your code
}
Common design
.media-width(@extremum, @break)
when (@extremum = 'max')
and (@break = @screen__m) {
// your code
}
Mobile ( < 768px)
I'm not a great programmer; I'm just a good programmer with great habits
Kent Beck
Technical Guidelines
https://devdocs.magento.com/guides/v2.3/coding-standards/technical-guidelines.html
Programming Best Practices
https://devdocs.magento.com/guides/v2.3/ext-best-practices/extension-coding/common-programming-bp.html
Vinai Kopp
vinaikopp.com/blog and mage2.tv
Martin Fowler design rules
https://martinfowler.com/bliki/BeckDesignRules.html
Generate Magento 2 code
https://mage2gen.com
N98 Magerun
github.com/netz98/n98-magerun2
Magento 2 File Templates
https://github.com/lfolco/phpstorm-m2-filetemplates
Magento 2 Cache Cleaner
https://github.com/mage2tv/magento-cache-clean
Settings > Editor > File and Code Templates
Mage2gen.com as reference-code generator